This is the full developer documentation for Sigil
# Sigil
> Define, orchestrate, and simulate real application stacks locally — built for coding agents.
## One config. Full stack.
[Section titled “One config. Full stack.”](#one-config-full-stack)
* sigil.config.ts
```typescript
import { Environment, Postgres, Service, APIInterface } from "sigil";
const env = new Environment("my-app");
const db = env.add(
new Postgres("db", {
database: "myapp",
initSql: "./schema.sql",
seedSql: "./seed.sql",
})
);
env.add(
new Service("api", {
command: ["node", "server.js"],
env: { DATABASE_URL: db.connectionString },
readyCheck: { url: "http://localhost:3000/health" },
}, [new APIInterface(3000)])
);
export default env;
```
* Terminal
```bash
$ sigil up
[sigil] Starting environment "my-app"...
[sigil] Starting entity: db
[sigil] Pulling image postgres:16...
[sigil] Container sigil-db started on port 5432
[sigil] Starting entity: api
[api] Server listening on port 3000
[sigil] All entities started. Press Ctrl+C to stop.
$ sigil status
Instance: default (PID 42069, running, started 3m ago)
Config: /app/sigil.config.ts
Entities:
db running
api running
$ sigil down
[sigil] Stopping instance "default"...
[sigil] Instance stopped
```
***
## How it works
[Section titled “How it works”](#how-it-works)
1. **Define** your stack in a `sigil.config.ts` — databases, services, frontends
2. **Run** `sigil up` — Sigil starts everything in dependency order, wires connection strings, and waits for readiness
3. **Develop** against real infrastructure — Docker Postgres, live processes, actual ports
4. **Tear down** with `sigil down` or Ctrl+C — clean shutdown in reverse order
***
## Built different
[Section titled “Built different”](#built-different)
Declarative
TypeScript config, not YAML. Full type safety, IDE autocomplete, and the ability to compute values like connection strings at definition time.
Docker-native
Databases run in real Docker containers. Services run as native processes with `Bun.spawn`. No emulation, no mocks — real infrastructure.
Agent-first
Coding agents need realistic stacks to test against. Sigil gives them Postgres with real data, APIs with health checks, and full-stack environments — not toy sandboxes.
LLM-friendly
These docs ship [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt) for single-fetch ingestion. Your agent can read the entire Sigil API in one request.
***
## Dive in
[Section titled “Dive in”](#dive-in)
[Quickstart ](/getting-started/quickstart/)Your first environment in 5 minutes
[Full-Stack Example ](/guides/full-stack-example/)Postgres + FastAPI + React — the complete walkthrough
[CLI Reference ](/reference/cli/)Every command, flag, and exit code
[Concepts ](/getting-started/concepts/)Entities, Interfaces, and the lifecycle model
# Architecture
> How Sigil's internals work — Orchestrator, ContainerManager, and state management
## Overview
[Section titled “Overview”](#overview)
Sigil has three layers:
```plaintext
┌─────────────────────────────────┐
│ CLI (bunli) │ sigil up / down / status
├─────────────────────────────────┤
│ SDK (Environment) │ sigil.config.ts API surface
├─────────────────────────────────┤
│ Core (Orchestrator, Docker, │ Lifecycle, containers, state
│ State) │
└─────────────────────────────────┘
```
## Core Components
[Section titled “Core Components”](#core-components)
### Orchestrator
[Section titled “Orchestrator”](#orchestrator)
`src/core/orchestrator.ts`
Manages entity lifecycle. Entities are started **sequentially** in registration order (so dependencies like databases start before the services that use them) and stopped in **reverse order**.
```typescript
class Orchestrator {
register(entity: Entity): void // Adds entity (throws on duplicate name)
get(name: string): Entity // Retrieve by name
getAll(): Entity[] // All entities
startAll(): Promise // Sequential start
stopAll(): Promise // Reverse-order stop
getStatus(): Array<{ name: string; status: EntityStatus }>
}
```
Key design choice: `stopAll()` does not short-circuit on errors. If stopping one entity fails, it continues stopping the rest and logs the error.
### ContainerManager
[Section titled “ContainerManager”](#containermanager)
`src/core/container.ts`
Thin wrapper around [dockerode](https://github.com/apocas/dockerode) for Docker operations.
```typescript
class ContainerManager {
create(config: ContainerConfig): Promise
start(container: Docker.Container): Promise
stop(container: Docker.Container): Promise
remove(container: Docker.Container, force?: boolean): Promise
isRunning(container: Docker.Container): Promise
pullImage(image: string): Promise
}
```
Used internally by the `Postgres` entity to manage its Docker container.
### State Management
[Section titled “State Management”](#state-management)
`src/core/state.ts`
Tracks running sigil instances on disk at `.sigil/instances/.json`. This enables `sigil down` (in a different terminal) to find and signal the `sigil up` process.
```typescript
interface InstanceState {
name: string // Instance name (e.g., "default")
configPath: string // Absolute path to sigil.config.ts
pid: number // Process ID of the `sigil up` process
startedAt: string // ISO 8601 timestamp
entities: Array<{ name: string; status: string }>
}
```
PID liveness is checked via `process.kill(pid, 0)` — this sends no signal but throws if the process doesn’t exist.
## Lifecycle Flow
[Section titled “Lifecycle Flow”](#lifecycle-flow)
### `sigil up`
[Section titled “sigil up”](#sigil-up)
1. Resolve config path → dynamic `import()` → get `Environment`
2. Write initial state file (PID, config path, timestamp)
3. Register SIGINT/SIGTERM handlers for graceful shutdown
4. Call `env.up()` → Orchestrator starts entities sequentially
5. Update state file with entity statuses
6. Block forever (`await new Promise(() => {})`)
7. On signal: `env.down()` → remove state file → exit
### `sigil down`
[Section titled “sigil down”](#sigil-down)
1. Read state file for the named instance
2. Check PID liveness
3. Send SIGTERM to the `sigil up` process
4. Poll for process exit (500ms intervals, 30s timeout)
5. Fallback to SIGKILL if still alive
6. Remove state file
The `down` command does **not** re-import the config. It signals the `up` process, which handles its own teardown.
## Entity Model
[Section titled “Entity Model”](#entity-model)
```plaintext
Entity (abstract)
├── name, status, config
├── interfaces: EntityInterface[]
├── start() / stop()
│
├── Postgres → Docker container
├── Service → Bun subprocess
└── Browser → (coming soon) Puppeteer
```
Each entity can hold one or more **interfaces** — objects describing how the entity is exposed (protocol, port, connection string). For example, `Postgres` exposes a `PostgresInterface` with a `postgresql://` connection string.
# Contributing
> Development setup, project structure, and how to add new entities
## Development Setup
[Section titled “Development Setup”](#development-setup)
```bash
# Clone and install
git clone https://github.com/pcutter1/sigil.git
cd sigil
bun install
# Link globally (makes `sigil` CLI and `import "sigil"` work)
bun link
# Type check
bun run typecheck
# Run tests
bun test
```
## Project Structure
[Section titled “Project Structure”](#project-structure)
```plaintext
sigil/
├── src/
│ ├── cli/ # CLI layer (bunli)
│ │ ├── index.ts # Entry point, command registration
│ │ └── commands/
│ │ ├── up.ts # sigil up [name]
│ │ ├── down.ts # sigil down [name]
│ │ ├── status.ts # sigil status [name]
│ │ └── init.ts # sigil init (stub)
│ ├── sdk/ # Public API surface
│ │ ├── index.ts # Re-exports everything
│ │ └── environment.ts # Environment class
│ ├── core/ # Internal orchestration
│ │ ├── index.ts
│ │ ├── orchestrator.ts # Entity lifecycle
│ │ ├── container.ts # Docker wrapper
│ │ └── state.ts # Instance state tracking
│ └── entities/ # Entity/Interface system
│ ├── entity.ts # Base Entity class
│ ├── interface.ts # Base EntityInterface class
│ └── primitives/
│ ├── index.ts
│ ├── postgres.ts # Docker-based Postgres
│ ├── service.ts # Process-based service
│ ├── browser.ts # Browser entity (stub)
│ └── api.ts # API service entity (stub)
├── docs/ # This documentation (Starlight)
├── examples/
│ └── shopping_list/ # Full-stack example app
├── package.json
├── tsconfig.json
└── bunli.config.ts
```
## Adding a New Entity
[Section titled “Adding a New Entity”](#adding-a-new-entity)
To add a new entity primitive (e.g., Redis, MySQL):
### 1. Create the entity file
[Section titled “1. Create the entity file”](#1-create-the-entity-file)
src/entities/primitives/redis.ts
```typescript
import { Entity, type EntityConfig } from "../entity";
import { EntityInterface, type InterfaceConfig } from "../interface";
export interface RedisConfig extends EntityConfig {
port?: number;
version?: string;
}
export class RedisInterface extends EntityInterface {
constructor(port: number) {
super({ protocol: "tcp", port });
}
getConnectionString(): string {
return `redis://${this.host}:${this.port}`;
}
async isReady(): Promise {
// Implement readiness check
return true;
}
}
export class Redis extends Entity {
private _redisInterface: RedisInterface;
constructor(name: string, config?: Partial>) {
const port = config?.port ?? 6379;
super({ name, port, ...config });
this._redisInterface = new RedisInterface(port);
this.addInterface(this._redisInterface);
}
get connectionString(): string {
return this._redisInterface.getConnectionString();
}
async start(): Promise {
this._status = "starting";
// Use ContainerManager to create/start Docker container
this._status = "running";
}
async stop(): Promise {
this._status = "stopping";
// Stop and remove Docker container
this._status = "stopped";
}
}
```
### 2. Export from primitives index
[Section titled “2. Export from primitives index”](#2-export-from-primitives-index)
src/entities/primitives/index.ts
```typescript
export { Redis, RedisInterface } from "./redis";
```
### 3. Export from SDK
[Section titled “3. Export from SDK”](#3-export-from-sdk)
src/sdk/index.ts
```typescript
export { Redis, RedisInterface } from "../entities/primitives/redis";
```
### 4. Test it
[Section titled “4. Test it”](#4-test-it)
Create a simple config that uses the new entity and run `sigil up`.
## Conventions
[Section titled “Conventions”](#conventions)
* Entity names should be lowercase and descriptive
* Config interfaces extend `EntityConfig` and use optional fields with sensible defaults
* Each entity creates its own interfaces in the constructor
* `start()` must set `_status` to `"starting"` then `"running"` (or `"error"`)
* `stop()` must set `_status` to `"stopping"` then `"stopped"`
* Docker containers should be prefixed with `sigil-` (e.g., `sigil-redis`)
# Concepts
> Core concepts — Entities, Interfaces, Environments, and the lifecycle model
## Environment
[Section titled “Environment”](#environment)
An `Environment` is the top-level container. It holds a collection of entities that together form your local stack.
```typescript
const env = new Environment("my-app");
```
When you call `env.up()`, all entities start sequentially in the order they were added. When you call `env.down()`, they stop in reverse order.
## Entities
[Section titled “Entities”](#entities)
An **Entity** is anything that runs — a database, a service process, a browser instance. Each entity has:
* A **name** (unique within the environment)
* A **status** lifecycle: `pending` → `starting` → `running` → `stopping` → `stopped` (or `error`)
* A `start()` and `stop()` method
* Zero or more **interfaces**
### Built-in entity types
[Section titled “Built-in entity types”](#built-in-entity-types)
| Entity | What it does | Backed by |
| ---------- | ----------------------------- | ------------------------- |
| `Postgres` | Runs a PostgreSQL database | Docker container |
| `Service` | Runs any command as a process | `Bun.spawn` subprocess |
| `Browser` | Launches a browser instance | Puppeteer *(coming soon)* |
## Interfaces
[Section titled “Interfaces”](#interfaces)
An **Interface** describes how an entity is exposed — its protocol, port, and connection details.
```typescript
// Postgres exposes a PostgresInterface
db.connectionString // "postgresql://postgres:postgres@localhost:5432/myapp"
// A Service can hold any interface
new Service("frontend", config, [new BrowserInterface(3000)])
new Service("backend", config, [new APIInterface(8000)])
```
Interfaces provide:
* `protocol` — `"http"`, `"tcp"`, `"ws"`, or `"grpc"`
* `port` — The port number
* `host` — Defaults to `"localhost"`
* `getConnectionString()` — A URL for connecting
* `isReady()` — An async readiness check
## Startup Order
[Section titled “Startup Order”](#startup-order)
Entities start **sequentially** in the order you call `env.add()`. This matters because later entities often depend on earlier ones:
```typescript
// db starts first
const db = env.add(new Postgres("db", { ... }));
// backend starts after db is running — can use db.connectionString
const backend = env.add(new Service("backend", {
env: { DATABASE_URL: db.connectionString },
...
}));
// frontend starts after backend is running
env.add(new Service("frontend", { ... }));
```
Shutdown happens in reverse order: frontend → backend → db.
## Instance Namespacing
[Section titled “Instance Namespacing”](#instance-namespacing)
Each `sigil up` creates a **named instance** (default name: `"default"`). Instance state is stored at `.sigil/instances/.json` in your project directory.
This means:
* Multiple instances can run simultaneously with different names
* `sigil down` finds the right process to stop via the state file
* `sigil status` shows all running instances
* Stale state files (from crashed processes) are automatically cleaned up
## Config File
[Section titled “Config File”](#config-file)
A `sigil.config.ts` file exports an `Environment` as its default export. Sigil dynamically imports this file at runtime — no build step needed.
```typescript
import { Environment, Postgres, Service } from "sigil";
const env = new Environment("my-app");
// ... add entities ...
export default env;
```
See the [Config File Reference](/reference/config-file/) for full details.
# Installation
> Prerequisites and how to install Sigil
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* [Bun](https://bun.sh) v1.0+ — Sigil runs on Bun natively (no build step)
* [Docker](https://www.docker.com) — Required for database entities (Postgres, etc.)
## Install
[Section titled “Install”](#install)
### As a project dependency
[Section titled “As a project dependency”](#as-a-project-dependency)
```bash
bun add sigil
```
### Globally (makes `sigil` CLI available everywhere)
[Section titled “Globally (makes sigil CLI available everywhere)”](#globally-makes-sigil-cli-available-everywhere)
```bash
bun add -g sigil
```
### For development (from source)
[Section titled “For development (from source)”](#for-development-from-source)
```bash
git clone https://github.com/pcutter1/sigil.git
cd sigil
bun install
bun link
```
`bun link` registers the package globally — both the `sigil` CLI binary and the `import "sigil"` SDK resolve from your local clone.
## Verify
[Section titled “Verify”](#verify)
```bash
sigil --help
```
You should see the available commands: `up`, `down`, `status`, `init`.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Head to the [Quickstart](/getting-started/quickstart/) to create your first environment.
# Quickstart
> Create and run your first Sigil environment in 5 minutes
This guide walks you through creating a minimal Sigil environment with a Postgres database and a service that connects to it.
## 1. Create a config file
[Section titled “1. Create a config file”](#1-create-a-config-file)
Create `sigil.config.ts` in your project root:
```typescript
import { Environment, Postgres, Service } from "sigil";
import path from "path";
const root = import.meta.dir;
const env = new Environment("my-app");
// Add a Postgres database
const db = env.add(
new Postgres("db", {
port: 5432,
database: "myapp",
username: "postgres",
password: "postgres",
})
);
// Add your backend service
env.add(
new Service("api", {
command: ["node", "server.js"],
cwd: path.join(root, "backend"),
env: {
DATABASE_URL: db.connectionString,
},
readyCheck: {
url: "http://localhost:3000/health",
timeout: 30000,
},
})
);
export default env;
```
## 2. Start the environment
[Section titled “2. Start the environment”](#2-start-the-environment)
```bash
sigil up
```
Sigil will:
1. Pull the Postgres Docker image (first run only)
2. Start a Postgres container
3. Wait for the database to be ready
4. Start your backend service with `DATABASE_URL` injected
5. Poll the health endpoint until the service is ready
Output looks like:
```plaintext
[sigil] Starting environment "my-app"...
[sigil] Starting entity: db
[sigil] Pulling image postgres:16...
[sigil] Container sigil-db started on port 5432
[sigil] Starting entity: api
[api] Server listening on port 3000
[sigil] All entities started. Press Ctrl+C to stop.
```
## 3. Check status
[Section titled “3. Check status”](#3-check-status)
In another terminal:
```bash
sigil status
```
```plaintext
Instance: default (PID 12345, running, started 2m ago)
Config: /path/to/sigil.config.ts
Entities:
db running
api running
```
## 4. Tear down
[Section titled “4. Tear down”](#4-tear-down)
```bash
sigil down
```
Or press `Ctrl+C` in the terminal running `sigil up`.
## Named instances
[Section titled “Named instances”](#named-instances)
You can run multiple instances simultaneously:
```bash
sigil up dev # starts instance named "dev"
sigil up test # starts instance named "test"
sigil status # shows both
sigil down dev # stops only "dev"
```
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Concepts](/getting-started/concepts/) — Understand entities, interfaces, and the lifecycle model
* [Full-Stack Example](/guides/full-stack-example/) — A complete app with Postgres + FastAPI + React
# Custom Services
> How to wrap any process as a Sigil service — Python, Node, Go, or anything else
The `Service` entity wraps any command-line process. It works with any language or framework — if you can run it in a terminal, you can run it in Sigil.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
```typescript
import { Service } from "sigil";
env.add(
new Service("my-api", {
command: ["node", "server.js"],
cwd: "./backend",
})
);
```
## With environment variables
[Section titled “With environment variables”](#with-environment-variables)
Pass environment variables that get merged with the parent process environment:
```typescript
env.add(
new Service("my-api", {
command: ["python", "-m", "uvicorn", "main:app", "--port", "8000"],
cwd: "./backend",
env: {
DATABASE_URL: db.connectionString,
REDIS_URL: "redis://localhost:6379",
NODE_ENV: "development",
},
})
);
```
## With a ready check
[Section titled “With a ready check”](#with-a-ready-check)
Without a ready check, Sigil marks the service as “running” as soon as the process spawns. With a ready check, it polls an HTTP endpoint until it responds:
```typescript
env.add(
new Service("my-api", {
command: ["node", "server.js"],
readyCheck: {
url: "http://localhost:3000/health",
interval: 1000, // poll every 1s (default)
timeout: 30000, // fail after 30s (default)
},
})
);
```
This is important when other services depend on this one being fully ready before they start.
## Attaching interfaces
[Section titled “Attaching interfaces”](#attaching-interfaces)
Interfaces are metadata describing what the service exposes:
```typescript
import { Service, APIInterface, BrowserInterface } from "sigil";
// An API service
env.add(
new Service("backend", config, [new APIInterface(8000)])
);
// A web frontend
env.add(
new Service("frontend", config, [new BrowserInterface(3000)])
);
```
## Common patterns
[Section titled “Common patterns”](#common-patterns)
### Python with virtualenv
[Section titled “Python with virtualenv”](#python-with-virtualenv)
```typescript
import path from "path";
const root = import.meta.dir;
env.add(
new Service("api", {
command: [
path.join(root, "backend/.venv/bin/uvicorn"),
"main:app",
"--host", "0.0.0.0",
"--port", "8000",
],
cwd: path.join(root, "backend"),
})
);
```
### Go binary
[Section titled “Go binary”](#go-binary)
```typescript
env.add(
new Service("api", {
command: ["go", "run", "."],
cwd: "./backend",
env: { PORT: "8080" },
readyCheck: { url: "http://localhost:8080/healthz" },
})
);
```
### Bun/Node dev server
[Section titled “Bun/Node dev server”](#bunnode-dev-server)
```typescript
env.add(
new Service("frontend", {
command: ["bun", "run", "dev"],
cwd: "./frontend",
readyCheck: { url: "http://localhost:5173" },
})
);
```
## Stdout/stderr
[Section titled “Stdout/stderr”](#stdoutstderr)
Service output is streamed to the terminal with a name prefix:
```plaintext
[my-api] Server listening on port 8000
[my-api] Connected to database
[frontend] VITE v5.0.0 ready in 200ms
```
## Lifecycle
[Section titled “Lifecycle”](#lifecycle)
* **Start**: Process is spawned via `Bun.spawn()`. If a `readyCheck` is configured, Sigil polls until the endpoint responds with a 2xx status.
* **Stop**: Process receives `SIGKILL`. The service status transitions to `"stopped"`.
# Database Setup
> Using the Postgres entity — initialization, seeding, custom configuration
The `Postgres` entity runs a real PostgreSQL instance in a Docker container. It handles image pulling, container lifecycle, readiness checks, and SQL initialization.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
```typescript
import { Postgres } from "sigil";
const db = env.add(new Postgres("my-db"));
```
This starts Postgres 16 on port 5432 with default credentials (`postgres`/`postgres`).
## Custom configuration
[Section titled “Custom configuration”](#custom-configuration)
```typescript
const db = env.add(
new Postgres("my-db", {
port: 5433, // host port (default: 5432)
database: "myapp", // database name (default: "postgres")
username: "admin", // (default: "postgres")
password: "secret", // (default: "postgres")
version: "15", // Postgres version (default: "16")
})
);
```
## Schema initialization
[Section titled “Schema initialization”](#schema-initialization)
Pass a path to a SQL file that creates your tables:
```typescript
const db = env.add(
new Postgres("my-db", {
database: "myapp",
initSql: path.join(root, "db/schema.sql"),
})
);
```
Example `schema.sql`:
```sql
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
title TEXT NOT NULL,
body TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
```
## Seed data
[Section titled “Seed data”](#seed-data)
Load test data after the schema is created:
```typescript
const db = env.add(
new Postgres("my-db", {
database: "myapp",
initSql: path.join(root, "db/schema.sql"),
seedSql: path.join(root, "db/seed.sql"),
})
);
```
Example `seed.sql`:
```sql
INSERT INTO users (email, name) VALUES
('alice@example.com', 'Alice'),
('bob@example.com', 'Bob');
INSERT INTO posts (user_id, title, body) VALUES
(1, 'Hello World', 'My first post'),
(2, 'Getting Started', 'How I set up my project');
```
Execution order: `initSql` runs first, then `seedSql`.
## Connection string
[Section titled “Connection string”](#connection-string)
The Postgres entity exposes a standard connection string:
```typescript
const db = env.add(new Postgres("my-db", { port: 5433, database: "myapp" }));
console.log(db.connectionString);
// "postgresql://postgres:postgres@localhost:5433/myapp"
```
Pass this to other services:
```typescript
env.add(
new Service("backend", {
command: ["node", "server.js"],
env: {
DATABASE_URL: db.connectionString,
},
})
);
```
## How it works
[Section titled “How it works”](#how-it-works)
1. **Image pull** — On first run, Sigil pulls `postgres:` from Docker Hub
2. **Container creation** — Creates a container named `sigil-` with the specified port mapping and credentials
3. **Readiness check** — Polls the database using `psql -c 'SELECT 1'` inside the container until it succeeds (not `pg_isready`, which can return true before the target database exists)
4. **SQL execution** — Runs `initSql` then `seedSql` by piping the file contents into `psql` inside the container
5. **Cleanup** — On `stop()`, the container is stopped and removed
## Docker container naming
[Section titled “Docker container naming”](#docker-container-naming)
Containers are named `sigil-`. If a container with that name already exists (from a previous crashed run), Sigil will handle it during startup.
# Full-Stack Example
> Walk through a complete app — Postgres + FastAPI + React SPA — orchestrated by Sigil
This guide walks through the `examples/shopping_list/` project that ships with Sigil. It’s a shopping list app with three layers: a Postgres database, a Python FastAPI backend, and a React frontend.
## Project Structure
[Section titled “Project Structure”](#project-structure)
```plaintext
shopping_list/
├── sigil.config.ts # Sigil environment definition
├── db/
│ ├── schema.sql # Table definitions
│ └── seed.sql # Sample data
├── backend/
│ ├── main.py # FastAPI application
│ └── requirements.txt
└── frontend/
├── src/App.tsx # React shopping list UI
├── package.json
└── vite.config.ts
```
## The Config File
[Section titled “The Config File”](#the-config-file)
```typescript
import { Environment, Postgres, Service, APIInterface, BrowserInterface } from "sigil";
import path from "path";
const root = import.meta.dir;
const env = new Environment("shopping-list");
// 1. Database — starts first
const db = env.add(
new Postgres("shopping-db", {
port: 5433,
database: "shopping_list",
username: "postgres",
password: "postgres",
version: "16",
initSql: path.join(root, "db/schema.sql"),
seedSql: path.join(root, "db/seed.sql"),
})
);
// 2. Backend — starts after DB is ready
const backend = env.add(
new Service(
"backend",
{
command: [
path.join(root, "backend/.venv/bin/uvicorn"),
"main:app",
"--host", "0.0.0.0",
"--port", "8000",
],
cwd: path.join(root, "backend"),
env: {
DATABASE_URL: db.connectionString,
},
readyCheck: {
url: "http://localhost:8000/health",
interval: 1000,
timeout: 30000,
},
},
[new APIInterface(8000)]
)
);
// 3. Frontend — starts after backend is ready
const frontend = env.add(
new Service(
"frontend",
{
command: ["bun", "run", "dev"],
cwd: path.join(root, "frontend"),
env: {
VITE_API_URL: "http://localhost:8000",
},
readyCheck: {
url: "http://localhost:3000",
interval: 1000,
timeout: 30000,
},
},
[new BrowserInterface(3000)]
)
);
export default env;
```
## Key Patterns
[Section titled “Key Patterns”](#key-patterns)
### Dependency wiring via connection strings
[Section titled “Dependency wiring via connection strings”](#dependency-wiring-via-connection-strings)
The Postgres entity exposes `db.connectionString` which returns `postgresql://postgres:postgres@localhost:5433/shopping_list`. This is injected into the backend’s environment variables:
```typescript
env: {
DATABASE_URL: db.connectionString,
}
```
The backend reads `DATABASE_URL` from its environment and connects to the database. No hardcoded connection details in the application code.
### Schema initialization and seeding
[Section titled “Schema initialization and seeding”](#schema-initialization-and-seeding)
```typescript
initSql: path.join(root, "db/schema.sql"), // CREATE TABLE ...
seedSql: path.join(root, "db/seed.sql"), // INSERT INTO ...
```
Sigil runs `initSql` first (for DDL), then `seedSql` (for test data) — both executed inside the Docker container after Postgres reports ready.
### Ready checks
[Section titled “Ready checks”](#ready-checks)
Each service has a `readyCheck` that polls an HTTP endpoint:
```typescript
readyCheck: {
url: "http://localhost:8000/health",
interval: 1000, // poll every second
timeout: 30000, // give up after 30s
}
```
Sigil won’t move to the next entity until the current one passes its ready check. This ensures the frontend doesn’t start before the backend is actually serving requests.
### Interfaces as metadata
[Section titled “Interfaces as metadata”](#interfaces-as-metadata)
Attaching interfaces to services is declarative metadata about what the service exposes:
```typescript
[new APIInterface(8000)] // "this service exposes an HTTP API on port 8000"
[new BrowserInterface(3000)] // "this service exposes a web UI on port 3000"
```
This is currently used for status reporting and will be used for future features like automatic API discovery and browser automation.
## Running It
[Section titled “Running It”](#running-it)
```bash
cd examples/shopping_list
sigil up
```
After startup completes:
* Postgres is running on port 5433 with the `shopping_list` database
* FastAPI backend is serving on `http://localhost:8000`
* React frontend is serving on `http://localhost:3000`
Open `http://localhost:3000` to see the shopping list UI.
# CLI Reference
> All Sigil CLI commands — up, down, status, init
Sigil’s CLI is built on [bunli](https://github.com/nicholasgasior/bunli) and runs via Bun.
## `sigil up [name]`
[Section titled “sigil up \[name\]”](#sigil-up-name)
Start an environment from a config file.
| Argument | Description | Default |
| -------- | ------------- | ----------- |
| `name` | Instance name | `"default"` |
| Flag | Description | Default |
| -------------- | ------------------- | ------------------- |
| `-c, --config` | Path to config file | `"sigil.config.ts"` |
**Behavior:**
1. Checks if an instance with this name is already running. Errors if so.
2. Dynamically imports the config file (must export an `Environment` as default)
3. Writes instance state to `.sigil/instances/.json`
4. Starts all entities sequentially via `env.up()`
5. Keeps the process alive until interrupted
**Signal handling:** On SIGINT (Ctrl+C) or SIGTERM, calls `env.down()`, removes the state file, and exits cleanly.
**Exit codes:**
* `0` — Clean shutdown
* `1` — Error (instance already running, config load failure, startup failure)
## `sigil down [name]`
[Section titled “sigil down \[name\]”](#sigil-down-name)
Stop a running environment.
| Argument | Description | Default |
| -------- | ------------- | ----------- |
| `name` | Instance name | `"default"` |
**Behavior:**
1. Reads instance state from `.sigil/instances/.json`
2. If the process is dead, cleans up the stale state file
3. If alive, sends `SIGTERM` to the `sigil up` process
4. Polls for process exit (500ms intervals, 30s timeout)
5. Falls back to `SIGKILL` if the process doesn’t exit
6. Removes the state file
The `down` command does **not** re-import the config. It signals the `up` process, which handles its own teardown.
## `sigil status [name]`
[Section titled “sigil status \[name\]”](#sigil-status-name)
Show running instances.
| Argument | Description | Default |
| -------- | ------------------------ | -------- |
| `name` | Instance name (optional) | Show all |
**Output format:**
```plaintext
Instance: default (PID 12345, running, started 5m ago)
Config: /path/to/sigil.config.ts
Entities:
db running
backend running
frontend running
```
**Status values:**
* `running` — PID is alive
* `stale` — PID is dead (state file is automatically cleaned up)
## `sigil init`
[Section titled “sigil init”](#sigil-init)
Initialize a new Sigil project in the current directory.
Note
This command is currently a stub and will be implemented in a future release.
## Global Flags
[Section titled “Global Flags”](#global-flags)
| Flag | Description |
| ----------- | ------------ |
| `--help` | Show help |
| `--version` | Show version |
# Config File
> How sigil.config.ts works — structure, imports, and conventions
## Overview
[Section titled “Overview”](#overview)
A Sigil config file is a TypeScript module that exports an `Environment` as its default export. Sigil dynamically imports this file at runtime using Bun’s native TypeScript support — no build step required.
## File location
[Section titled “File location”](#file-location)
By default, Sigil looks for `sigil.config.ts` in the current working directory. Override with the `--config` flag:
```bash
sigil up --config ./configs/staging.config.ts
```
## Structure
[Section titled “Structure”](#structure)
```typescript
import { Environment, Postgres, Service } from "sigil";
import path from "path";
const root = import.meta.dir;
const env = new Environment("my-app");
// Add entities in dependency order
const db = env.add(new Postgres("db", { ... }));
env.add(new Service("api", { ... }));
// Must be the default export
export default env;
```
## Key patterns
[Section titled “Key patterns”](#key-patterns)
### `import.meta.dir`
[Section titled “import.meta.dir”](#importmetadir)
Use `import.meta.dir` to get the directory containing the config file. This makes paths work correctly regardless of where `sigil up` is run from:
```typescript
const root = import.meta.dir;
new Postgres("db", {
initSql: path.join(root, "db/schema.sql"),
});
new Service("api", {
cwd: path.join(root, "backend"),
});
```
### Dependency wiring
[Section titled “Dependency wiring”](#dependency-wiring)
Entities that are added first start first. Use references to earlier entities to wire up dependencies:
```typescript
const db = env.add(new Postgres("db", { ... }));
// db.connectionString is available immediately (computed from config)
env.add(new Service("api", {
env: { DATABASE_URL: db.connectionString },
}));
```
### Multiple configs
[Section titled “Multiple configs”](#multiple-configs)
You can have multiple config files for different scenarios:
```plaintext
sigil.config.ts # full stack
sigil.config.db-only.ts # just the database
sigil.config.test.ts # test environment with different ports
```
```bash
sigil up --config sigil.config.db-only.ts
```
## What gets imported
[Section titled “What gets imported”](#what-gets-imported)
Sigil does:
```typescript
const mod = await import(configPath);
const env = mod.default; // must be an Environment
```
Side effects in your config file (like `console.log`) will execute when the config is imported.
# Browser
> The Browser entity — headless Chromium for testing and automation
The `Browser` entity launches a headless Chromium browser via [Playwright](https://playwright.dev), exposing a WebSocket endpoint for remote automation.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
Install the Chromium binary (one-time setup):
```bash
bunx playwright install chromium
```
## Import
[Section titled “Import”](#import)
```typescript
import { Browser } from "sigil";
```
## Constructor
[Section titled “Constructor”](#constructor)
```typescript
new Browser(name: string, config?: Partial>)
```
## Config
[Section titled “Config”](#config)
```typescript
interface BrowserConfig {
name: string
headless?: boolean // default: true
viewport?: {
width: number // default: 1280
height: number // default: 720
}
}
```
## Properties
[Section titled “Properties”](#properties)
### `browserInterface`
[Section titled “browserInterface”](#browserinterface)
```typescript
browser.browserInterface: BrowserInterface
```
The WebSocket interface. Provides the WebSocket endpoint for connecting automation tools.
### `wsEndpoint`
[Section titled “wsEndpoint”](#wsendpoint)
```typescript
browser.wsEndpoint: string
```
Convenience getter for the WebSocket URL. Returns an empty string before `start()` is called.
### `status`
[Section titled “status”](#status)
Inherited from `Entity`. One of: `"pending"`, `"starting"`, `"running"`, `"stopping"`, `"stopped"`, `"error"`.
## Lifecycle
[Section titled “Lifecycle”](#lifecycle)
### `start()`
[Section titled “start()”](#start)
1. Calls `chromium.launchServer()` from `playwright-core`
2. Sets the WebSocket endpoint on the `BrowserInterface`
3. Logs the WS URL for connection
If Chromium is not installed, prints a helpful error message directing you to run `bunx playwright install chromium`.
### `stop()`
[Section titled “stop()”](#stop)
Closes the browser server and releases all resources.
## Examples
[Section titled “Examples”](#examples)
### Basic usage
[Section titled “Basic usage”](#basic-usage)
```typescript
const browser = env.add(
new Browser("chrome", { headless: true })
);
// After env.up(), the WS endpoint is available:
console.log(browser.wsEndpoint);
// "ws://localhost:54321/abc123..."
```
### Connecting from Playwright
[Section titled “Connecting from Playwright”](#connecting-from-playwright)
```typescript
import { chromium } from "playwright-core";
// Connect to the Sigil-managed browser
const browser = await chromium.connect(sigilBrowser.wsEndpoint);
const page = await browser.newPage();
await page.goto("http://localhost:3000");
```
### In a full-stack environment
[Section titled “In a full-stack environment”](#in-a-full-stack-environment)
```typescript
import { Environment, Postgres, Service, Browser, APIInterface, BrowserInterface } from "sigil";
const env = new Environment("my-app");
const db = env.add(new Postgres("db", { database: "myapp" }));
env.add(
new Service("backend", {
command: ["node", "server.js"],
env: { DATABASE_URL: db.connectionString },
readyCheck: { url: "http://localhost:8000/health" },
}, [new APIInterface(8000)])
);
env.add(
new Service("frontend", {
command: ["bun", "run", "dev"],
readyCheck: { url: "http://localhost:3000" },
}, [new BrowserInterface(3000)])
);
// Browser starts last — all services are ready
env.add(new Browser("chrome", { headless: true }));
export default env;
```
# Postgres
> The Postgres entity — Docker-based PostgreSQL with auto-initialization
The `Postgres` entity runs a PostgreSQL database in a Docker container with automatic image pulling, readiness checking, and SQL initialization.
## Import
[Section titled “Import”](#import)
```typescript
import { Postgres } from "sigil";
```
## Constructor
[Section titled “Constructor”](#constructor)
```typescript
new Postgres(name: string, config?: Partial>)
```
## Config
[Section titled “Config”](#config)
```typescript
interface PostgresConfig {
name: string // Set via constructor, not config object
port?: number // Host port (default: 5432)
database?: string // Database name (default: "postgres")
username?: string // (default: "postgres")
password?: string // (default: "postgres")
version?: string // Postgres Docker image tag (default: "16")
initSql?: string // Absolute path to SQL file for schema setup
seedSql?: string // Absolute path to SQL file for test data
}
```
## Properties
[Section titled “Properties”](#properties)
### `connectionString`
[Section titled “connectionString”](#connectionstring)
```typescript
db.connectionString: string
```
Returns a standard PostgreSQL connection string:
```plaintext
postgresql://postgres:postgres@localhost:5432/myapp
```
Available immediately after construction (computed from config values, does not require the database to be running).
### `pgInterface`
[Section titled “pgInterface”](#pginterface)
```typescript
db.pgInterface: PostgresInterface
```
The TCP interface instance. Exposes the same connection string via `getConnectionString()`.
### `status`
[Section titled “status”](#status)
Inherited from `Entity`. One of: `"pending"`, `"starting"`, `"running"`, `"stopping"`, `"stopped"`, `"error"`.
## Lifecycle
[Section titled “Lifecycle”](#lifecycle)
### `start()`
[Section titled “start()”](#start)
1. Pulls `postgres:` image if not already present
2. Creates a Docker container named `sigil-`
3. Maps the specified host port to container port 5432
4. Sets `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` environment variables
5. Waits for the database to accept connections (using `psql -c 'SELECT 1'` inside the container)
6. Runs `initSql` file if provided
7. Runs `seedSql` file if provided
### `stop()`
[Section titled “stop()”](#stop)
Stops and removes the Docker container.
## Examples
[Section titled “Examples”](#examples)
### Minimal
[Section titled “Minimal”](#minimal)
```typescript
const db = env.add(new Postgres("db"));
// Postgres 16 on port 5432, database "postgres"
```
### Full configuration
[Section titled “Full configuration”](#full-configuration)
```typescript
const db = env.add(
new Postgres("shopping-db", {
port: 5433,
database: "shopping_list",
username: "admin",
password: "secret",
version: "15",
initSql: path.join(root, "db/schema.sql"),
seedSql: path.join(root, "db/seed.sql"),
})
);
```
### Wiring to a service
[Section titled “Wiring to a service”](#wiring-to-a-service)
```typescript
const db = env.add(new Postgres("db", { database: "myapp" }));
env.add(
new Service("api", {
command: ["node", "server.js"],
env: { DATABASE_URL: db.connectionString },
})
);
```
# Service
> The Service entity — run any process with environment injection and ready checks
The `Service` entity runs an arbitrary command as a subprocess using `Bun.spawn`. It supports environment variable injection, working directory configuration, HTTP readiness checks, and stdout/stderr streaming.
## Import
[Section titled “Import”](#import)
```typescript
import { Service } from "sigil";
```
## Constructor
[Section titled “Constructor”](#constructor)
```typescript
new Service(
name: string,
config: Omit,
interfaces?: EntityInterface[]
)
```
## Config
[Section titled “Config”](#config)
```typescript
interface ServiceConfig {
name: string // Set via constructor
command: string[] // [executable, ...args]
cwd?: string // Working directory
env?: Record // Environment variables (merged with process.env)
readyCheck?: {
url: string // HTTP endpoint to poll
interval?: number // Poll interval in ms (default: 1000)
timeout?: number // Max wait time in ms (default: 30000)
}
}
```
## Properties
[Section titled “Properties”](#properties)
### `process`
[Section titled “process”](#process)
```typescript
service.process: Subprocess | null
```
The Bun subprocess instance. `null` before `start()` is called.
### `status`
[Section titled “status”](#status)
Inherited from `Entity`. One of: `"pending"`, `"starting"`, `"running"`, `"stopping"`, `"stopped"`, `"error"`.
## Lifecycle
[Section titled “Lifecycle”](#lifecycle)
### `start()`
[Section titled “start()”](#start)
1. Spawns the process via `Bun.spawn(command, { cwd, env })`
2. Streams stdout and stderr to the terminal with `[name]` prefix
3. If `readyCheck` is configured, polls the URL until a 2xx response
4. Validates the process is still alive after startup
### `stop()`
[Section titled “stop()”](#stop)
Sends `SIGKILL` to the subprocess.
## Examples
[Section titled “Examples”](#examples)
### Node.js server
[Section titled “Node.js server”](#nodejs-server)
```typescript
env.add(
new Service("api", {
command: ["node", "server.js"],
cwd: "./backend",
env: { PORT: "3000" },
readyCheck: { url: "http://localhost:3000/health" },
})
);
```
### Python with virtualenv
[Section titled “Python with virtualenv”](#python-with-virtualenv)
```typescript
env.add(
new Service("api", {
command: [
path.join(root, ".venv/bin/uvicorn"),
"main:app",
"--host", "0.0.0.0",
"--port", "8000",
],
cwd: path.join(root, "backend"),
env: { DATABASE_URL: db.connectionString },
readyCheck: {
url: "http://localhost:8000/health",
interval: 1000,
timeout: 30000,
},
})
);
```
### With interfaces
[Section titled “With interfaces”](#with-interfaces)
```typescript
import { Service, APIInterface, BrowserInterface } from "sigil";
// Backend API
env.add(
new Service("backend", backendConfig, [new APIInterface(8000)])
);
// Frontend SPA
env.add(
new Service("frontend", frontendConfig, [new BrowserInterface(3000)])
);
```
## Output streaming
[Section titled “Output streaming”](#output-streaming)
Service stdout and stderr are streamed to the terminal with a name prefix:
```plaintext
[backend] INFO: Uvicorn running on http://0.0.0.0:8000
[backend] INFO: Application startup complete
[frontend] VITE v5.0.0 ready in 150ms
[frontend] ➜ Local: http://localhost:3000/
```
# Environment
> The Environment class — creating and managing entity collections
The `Environment` class is the top-level container for your Sigil configuration. It manages a collection of entities and orchestrates their lifecycle.
## Import
[Section titled “Import”](#import)
```typescript
import { Environment } from "sigil";
```
## Constructor
[Section titled “Constructor”](#constructor)
```typescript
new Environment(name: string)
```
| Parameter | Type | Description |
| --------- | -------- | ------------------------------------------------------------- |
| `name` | `string` | A name for this environment (used in logs and state tracking) |
## Properties
[Section titled “Properties”](#properties)
### `name`
[Section titled “name”](#name)
```typescript
env.name: string
```
The environment’s name, as passed to the constructor.
### `entities`
[Section titled “entities”](#entities)
```typescript
env.entities: Entity[]
```
All registered entities, in registration order.
## Methods
[Section titled “Methods”](#methods)
### `add(entity)`
[Section titled “add(entity)”](#addentity)
```typescript
env.add(entity: T): T
```
Register an entity in the environment. Returns the entity (useful for chaining or capturing a reference).
```typescript
const db = env.add(new Postgres("db", { ... }));
// db is typed as Postgres, not Entity
```
Entities are started in the order they are added, so add dependencies first.
### `get(name)`
[Section titled “get(name)”](#getname)
```typescript
env.get(name: string): Entity | undefined
```
Retrieve a registered entity by name.
```typescript
const db = env.get("db");
```
### `up()`
[Section titled “up()”](#up)
```typescript
env.up(): Promise
```
Start all entities sequentially in registration order. Each entity’s `start()` must complete before the next one begins.
Throws if any entity fails to start.
### `down()`
[Section titled “down()”](#down)
```typescript
env.down(): Promise
```
Stop all entities in **reverse** registration order. If stopping one entity throws, it logs the error and continues stopping the remaining entities.
### `status()`
[Section titled “status()”](#status)
```typescript
env.status(): Array<{ name: string; status: EntityStatus }>
```
Returns the current status of all entities.
```typescript
const statuses = env.status();
// [
// { name: "db", status: "running" },
// { name: "backend", status: "running" },
// ]
```
## Full Example
[Section titled “Full Example”](#full-example)
```typescript
import { Environment, Postgres, Service, APIInterface } from "sigil";
const env = new Environment("my-app");
const db = env.add(
new Postgres("db", {
database: "myapp",
initSql: "./schema.sql",
})
);
env.add(
new Service(
"api",
{
command: ["node", "server.js"],
env: { DATABASE_URL: db.connectionString },
readyCheck: { url: "http://localhost:3000/health" },
},
[new APIInterface(3000)]
)
);
export default env;
```
# Interfaces
> Interface types — APIInterface, BrowserInterface, PostgresInterface
Interfaces describe how an entity is exposed. They carry metadata about protocol, port, and connection details.
## Base class
[Section titled “Base class”](#base-class)
All interfaces extend `EntityInterface`:
```typescript
abstract class EntityInterface {
get protocol(): InterfaceProtocol // "http" | "tcp" | "ws" | "grpc"
get port(): number | undefined
get host(): string // default: "localhost"
abstract getConnectionString(): string
abstract isReady(): Promise
}
```
## APIInterface
[Section titled “APIInterface”](#apiinterface)
HTTP interface for REST/API services.
```typescript
import { APIInterface } from "sigil";
new APIInterface(port: number)
```
| Property | Value |
| ----------------------- | ------------------------------------------------ |
| `protocol` | `"http"` |
| `getConnectionString()` | `"http://localhost:"` |
| `isReady()` | `GET http://localhost:/health` returns 2xx |
**Usage:**
```typescript
env.add(
new Service("backend", config, [new APIInterface(8000)])
);
```
## BrowserInterface
[Section titled “BrowserInterface”](#browserinterface)
WebSocket interface for browser-based services.
```typescript
import { BrowserInterface } from "sigil";
new BrowserInterface(port?: number)
```
| Property | Value |
| ----------------------- | ----------------------------------- |
| `protocol` | `"ws"` |
| `getConnectionString()` | WebSocket endpoint (set internally) |
| `isReady()` | Returns `true` when endpoint is set |
**Usage:**
```typescript
env.add(
new Service("frontend", config, [new BrowserInterface(3000)])
);
```
## PostgresInterface
[Section titled “PostgresInterface”](#postgresinterface)
TCP interface for PostgreSQL connections. Created automatically by the `Postgres` entity — you typically don’t construct this directly.
```typescript
new PostgresInterface(port: number, database: string, username: string, password: string)
```
| Property | Value |
| ----------------------- | ------------------------------------------------- |
| `protocol` | `"tcp"` |
| `getConnectionString()` | `"postgresql://:@:/"` |
**Access via the Postgres entity:**
```typescript
const db = env.add(new Postgres("db", { database: "myapp" }));
db.connectionString; // "postgresql://postgres:postgres@localhost:5432/myapp"
db.pgInterface; // the PostgresInterface instance
```
## Attaching interfaces to services
[Section titled “Attaching interfaces to services”](#attaching-interfaces-to-services)
The `Service` constructor accepts an optional array of interfaces:
```typescript
new Service(name, config, interfaces?)
```
Interfaces on a `Service` are metadata — they describe what the service exposes but don’t affect the service’s behavior. They’re used for status reporting and will be used for future features like API discovery.
```typescript
// A service that exposes both an API and a web UI
env.add(
new Service("app", config, [
new APIInterface(8000),
new BrowserInterface(3000),
])
);
```