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), ]) ); ```