Execution Model
SysopKit propagates an ExecutionContext tree across async boundaries with AsyncLocalStorage. Every execution runs within a context that carries a reporter, connector, abort signal, typed variables, verbosity level, and dry-run flag.
Context Tree
Section titled “Context Tree”Contexts form a child-parent chain:
root ⇐ connector ⇐ [middleware] ⇐ task / utility ⇐ …Each context inherits from its parent:
- Reporter — Shared across the tree for unified output
- Connector — The transport layer for command execution
- AbortSignal — Composed with parent signals for cascading cancellation
- Vars — Typed context variables with parent-chain lookup
- Dry-run flag
Context Constructors
Section titled “Context Constructors”start()creates a root contextapply()creates connector contexts for each target hosttask()andutility()create child contexts for basic operationsmiddleware()wraps the connector and creates a middleware context
Context Lifecycle
Section titled “Context Lifecycle”- Each context calls
reporter.ctxStart()on entry andreporter.ctxEnd()on exit - On error,
reporter.ctxError()is called andAbortControlleris triggered
Logging: use the reporter, not console
Section titled “Logging: use the reporter, not console”Inside start() (anywhere an ambient context exists), never use console.log/warn/error. Take the ctx callback param instead:
await start(async (ctx) => { ctx.info('nginx installed'); ctx.warn('legacy config detected'); ctx.error(`failed on ${name}: ${err}`);});task, utility, and apply callbacks receive ctx the same way — prefer the param; reach for context() only in helpers that can’t take it.
ctx.info/warn/error buffer per context and flush on completion with the hierarchical prefix, respect verbosity filtering, route info to stdout vs warn/error to stderr, cooperate with the live TUI footer, and honor custom Reporter implementations passed to start({ reporter }). Raw console.* bypasses all of that.
console.* is only appropriate outside start() (e.g. handling the StartResult after start() returns), where no context exists.
Task vs Utility
Section titled “Task vs Utility”| Task | Utility | |
|---|---|---|
| Purpose | User-facing operations | Internal helpers |
| Verbosity | Normal (configurable) | Debug |
| Examples | Installing packages, managing files | Retries, timeouts, event listeners |
import { task, utility } from 'sysopkit';
await task('install nginx', async () => { await sh('apt install -y nginx');});
await utility('check state', async () => { // debug-level logging, not shown at normal verbosity});Accessing Context
Section titled “Accessing Context”Call context() to retrieve the current context from AsyncLocalStorage:
import { context } from 'sysopkit';
const ctx = context();const isDryRun: boolean = ctx.dryRun; // is this a dry run?const taskName: string = ctx.name; // current task/utility nameAlternatively, receive the context as the closure argument of task, utility, or apply:
import { context } from 'sysopkit';
await task('install nginx', async (ctx) => { if (!ctx.dryRun) { await sh('apt install -y nginx'); }});