Architecture
Module overview, data flow, and key design decisions.
Data flow
CLI args
→ parse targets
→ resolve inventory (load .ts file via dynamic import)
→ load recipe (.ts file via dynamic import)
→ for each host (bounded parallelism):
create SystemSSHConnection (ssh/scp via Bun.spawn)
→ ping (verify connectivity)
→ probe host facts (OS, pkg manager, init system, arch)
→ create ExecutionContext (one per host per run)
→ execute recipe(ctx)
→ accumulate results into RunSummary
→ format output
→ exit code (0 = success, 1 = failures)Module overview
src/core/
The engine.
| File | Purpose |
|---|---|
types.ts | All core interfaces and types |
resource.ts | executeResource() — the check-then-apply lifecycle engine |
context.ts | ExecutionContextImpl — per-host execution state |
runner.ts | runRecipe() — multi-host orchestration |
errors.ts | Tagged error hierarchy |
cache.ts | Check result caching (memory and file) |
facts.ts | Host platform detection |
registry.ts | Resource registry and schema generation |
serialize.ts | Deterministic JSON and field redaction |
src/ssh/
SSH transport.
| File | Purpose |
|---|---|
types.ts | Transport interface, capabilities, config types |
connection.ts | SystemSSHConnection — shells out to ssh/scp via Bun.spawn |
src/resources/
Built-in resources.
| File | Purpose |
|---|---|
index.ts | createResources() factory |
exec.ts | Imperative command execution |
file.ts | Declarative file management (SHA-256 comparison) |
apt.ts | Declarative package management |
service.ts | Systemd service management (mixed) |
directory.ts | Declarative directory management |
src/recipe/ and src/inventory/
| File | Purpose |
|---|---|
recipe/loader.ts | Dynamic import of recipe .ts files |
recipe/types.ts | RecipeFunction, RecipeMeta |
inventory/loader.ts | Dynamic import of inventory files, target resolution |
inventory/types.ts | Inventory, Host, HostGroup, ResolvedHost |
src/output/
Output and observability.
| File | Purpose |
|---|---|
events.ts | EventBus, event types, NdjsonStream |
reporter.ts | PrettyReporter (spinners, colors) and QuietReporter |
formats.ts | JsonFormatter and MinimalFormatter |
log_sink.ts | FileLogSink for NDJSON audit logs |
spinner.ts | Terminal spinner |
stderr.ts | Stderr writer for PrettyReporter |
src/dashboard/
Web dashboard.
| File | Purpose |
|---|---|
server.ts | DashboardServer — HTTP + WebSocket, run history |
client.ts | DashboardClient — CLI-side WebSocket producer |
dev-server.ts | Mock event server for UI development |
assets.ts | Embedded dashboard HTML (generated) |
ui/ | React SPA (Vite + vite-plugin-singlefile) |
src/cli/ and src/lib/
CLI and utilities. The CLI uses the incur framework.
| File | Purpose |
|---|---|
cli.ts | CLI entry point |
cli/index.ts | Root Cli.create() + command registration |
cli/logger.ts | Logger middleware for human-readable stderr output |
cli/commands/run.ts | run command (includes --check for dry-runs) |
cli/commands/init.ts | init command |
cli/commands/inventory.ts | inventory command |
cli/commands/dashboard.ts | dashboard command |
cli/commands/schema.ts | schema command group |
lib/types.ts | Shared CLI option types |
lib/parsers.ts | Dashboard address parser |
lib/config.ts | Config file loading and validation |
lib/errors.ts | User-facing error rendering utilities |
Key design decisions
No state tracking
Every run queries actual server state via SSH. No state files, lock files, or drift databases. This simplifies the mental model — there's nothing to get out of sync. The trade-off is SSH round-trips for every check, mitigated by the optional TTL-based cache.
System SSH over native libraries
Ignition shells out to the system ssh and scp binaries instead of using a native SSH library. This avoids native dependencies, leverages the user's existing SSH config and agent, and requires no additional setup. The trade-off is subprocess overhead per command, mitigated by SSH multiplexing (ControlMaster).
TypeScript recipes via dynamic import
Recipes are full TypeScript files loaded with import(). This gives recipes the full power of the language — loops, conditionals, helper functions, npm packages — with type safety and IDE support. The trade-off is no sandboxing (recipes have full system access).
Capability-driven transport
The Transport interface defines four capabilities (exec, transfer, fetch, ping). Resources declare which capabilities they need. This allows future alternative transports (Docker exec, local exec) without rewriting resources.
EventBus architecture
The dashboard and NDJSON log sinks consume a shared lifecycle event stream when an EventBus is attached. Terminal reporters are still called directly by the runner. This keeps telemetry optional without forcing the TTY output path through the event bus.
Extension points
| Extension point | Interface | Description |
|---|---|---|
| Transport | Transport | Add new connection types (Docker, local, native SSH) |
| Resources | ResourceDefinition | Add custom resources |
| Registry | ResourceRegistry | Manage resource collections |
| Event consumers | EventBus.on() | Custom monitoring, alerting, logging |
| Output formats | Reporter | Custom output rendering |