Architecture
Overview
Section titled “Overview”Sigil has three layers:
┌─────────────────────────────────┐│ 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”Orchestrator
Section titled “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.
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<void> // Sequential start stopAll(): Promise<void> // 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”src/core/container.ts
Thin wrapper around dockerode for Docker operations.
class ContainerManager { create(config: ContainerConfig): Promise<Docker.Container> start(container: Docker.Container): Promise<void> stop(container: Docker.Container): Promise<void> remove(container: Docker.Container, force?: boolean): Promise<void> isRunning(container: Docker.Container): Promise<boolean> pullImage(image: string): Promise<void>}Used internally by the Postgres entity to manage its Docker container.
State Management
Section titled “State Management”src/core/state.ts
Tracks running sigil instances on disk at .sigil/instances/<name>.json. This enables sigil down (in a different terminal) to find and signal the sigil up process.
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”sigil up
Section titled “sigil up”- Resolve config path → dynamic
import()→ getEnvironment - Write initial state file (PID, config path, timestamp)
- Register SIGINT/SIGTERM handlers for graceful shutdown
- Call
env.up()→ Orchestrator starts entities sequentially - Update state file with entity statuses
- Block forever (
await new Promise(() => {})) - On signal:
env.down()→ remove state file → exit
sigil down
Section titled “sigil down”- Read state file for the named instance
- Check PID liveness
- Send SIGTERM to the
sigil upprocess - Poll for process exit (500ms intervals, 30s timeout)
- Fallback to SIGKILL if still alive
- 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 (abstract)├── name, status, config├── interfaces: EntityInterface[]├── start() / stop()│├── Postgres → Docker container├── Service → Bun subprocess└── Browser → (coming soon) PuppeteerEach 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.