@ariestools/cli-kit
Reusable, instance-scoped actor lifecycle primitives for command-line applications.
This package is intentionally independent of XL1 and xyo-chain. The neutral
@ariestools/actor-system package owns config parsing, provider resolution,
provisioning, actor construction, and the resolved system session. Cli-kit
re-exports actor-system's sequentialActorSystemSupervision and supplies
launchCliSystem, which makes registration-order startup/readiness and
reverse-order shutdown the default CLI policy.
import {
ActorSystemCatalog,
createActorSystemProviderRegistry,
} from '@ariestools/actor-system'
import {
launchCliSystem,
runCliSystemUntilInterrupt,
} from '@ariestools/cli-kit'
import { nodeProcessHost } from '@ariestools/cli-kit-node'
const session = await launchCliSystem({
actorRegistry: new ActorSystemCatalog(),
config,
providerRegistry: createActorSystemProviderRegistry(),
})
await runCliSystemUntilInterrupt(nodeProcessHost, session)
Compilation can remain explicit when a CLI must inspect or retain its resolved
plan before provisioning:
import { compileActorSystem } from '@ariestools/actor-system'
import { launchCompiledCliSystem } from '@ariestools/cli-kit'
const compiled = compileActorSystem({ actorRegistry, config, providerRegistry })
const session = await launchCompiledCliSystem({ actorRegistry, compiled })
Each actor receives its own actor config over the same resolved provider
system: the session's config, plan, and provider instances, reached through a
locator view limited to that actor's declared dependencies. Only the session
holds the owner locator, so an actor cannot register or destroy providers.
Existing ManagedActor, ActorSupervisor, and ActorBuilderCatalog
APIs remain available for callback-built and pre-configured CLI applications.
The package also provides structural ProcessHost and ProcessIO boundaries for adapting an application's concrete process runtime without importing Node APIs into the core. Arguments, environment values, terminal width, output, prompts, interrupts, and exits are all supplied by the host. Use exitProcess at exit points that occur before the end of a command: production hosts may terminate immediately, while recording hosts receive a ProcessExitError that stops control flow deterministically. RuntimeSession owns one application-supplied stop operation and preserves fail-fast shutdown behavior while making repeated stop requests idempotent. It can also own one structural interrupt binding, coalesce repeated interrupts while the application listener settles, and dispose the binding at the correct manual or interrupt-driven shutdown boundary.
runProcessApplication owns the outermost application failure boundary. It absorbs standardized exit signals that have already been routed through a ProcessHost, hides unexpected error details outside development, and requests exit code one for unhandled failures. An optional mapFailureToExitCode mapper lets the application select a different exit code per failure; it receives the error plus a FailureOrigin ('parse' or 'handler'), returning undefined retains the default one, and intentional ProcessExitError exits are never offered to the mapper. The mapper stays inside the failure boundary: a mapper that throws, or returns a code outside the integer range 0–255, falls back to exit code one instead of escaping the boundary. Concrete process adapters remain separate packages; Node applications can use @ariestools/cli-kit-node.
SYS_EXITS supplies the BSD sysexits.h exit-code vocabulary used across applications (ok, usage, dataErr, noInput, software, ioErr, config), with SysExitCode as the derived literal union. Use it with exitProcess or a mapFailureToExitCode mapper instead of repeating numeric literals.
createCommandCatalog registers ordered command factories against a structural CliApplicationContext. The command type is supplied by the application, so parser-specific types and domain-specific configuration remain outside the core. A catalog is immutable and reusable; each application invocation materializes fresh command objects with its own context.
runServiceUntilInterrupt blocks a long-running command handler until the
first host interrupt, then awaits one application-supplied stop cleanup.
runCliSystemUntilInterrupt joins either an interrupt or terminal actor failure
to one actor-system stop. Successful joins return without requesting exit 0,
so caller-owned cleanup after the join can run. Ordinary failures retain the
caller’s mapper path; actor and distinct cleanup failures aggregate.
Both helpers accept an optional third InterruptJoinOptions argument. Cleanup
has a default 15-second watchdog, starting when the first interrupt or actor
failure begins shutdown, including cleanup after listener-registration failure.
Set shutdownTimeoutMs: 0 to disable it; other values must be finite and between
0 and 2,147,483,647 milliseconds. The timer bounds cooperative waits; it cannot
preempt synchronous blocking, and scheduling stalls can extend elapsed time.
Full host or explicit exitHost | Retained until the join settles | Request exit 130 | Request exit 1 |
Minimal actor host, only onInterrupt | Retained until settlement/escalation | Reject with InterruptJoinEscalationError | Reject with InterruptJoinEscalationError |
Minimal service host, only onInterrupt | Disposed before stop, preserving native behavior | Host’s restored default action | Reject with InterruptJoinEscalationError |
An available synchronous host.exit enables process escalation automatically;
exitHost supplies that capability when the interrupt host is minimal. Recording
exit boundaries receive the request and the join rejects with ProcessExitError
to prevent normal continuation, even if the exit callback completes or rejects
cleanup. A minimal host cannot terminate remaining resources: its ordinary
InterruptJoinEscalationError.exitCode is suggested status metadata (130 for
repeat interruption, 1 for timeout), and the outer failure mapper controls the
actual exit. On Node, minimal service disposal restores OS-default signal
termination, whose status differs by signal; a second SIGTERM is not explicit
exit 130.
await runServiceUntilInterrupt(nodeProcessHost, () => server.close(), {
shutdownTimeoutMs: 30_000,
})
await runCliSystemUntilInterrupt(minimalHost, session, {
exitHost: nodeProcessHost,
shutdownTimeoutMs: 30_000,
})
A deadline or repeat interrupt ends waiting; it does not cancel cleanup. Late
failures remain observed. Hard exit can cut off admitted work, database flushes,
and buffered output. The budget covers the supplied stop, not earlier startup
or cleanup performed by the caller after return. Bound those phases separately
or include required cleanup in the supplied stop. A minimal service disposer
failure rejects immediately while cleanup is still attempted and observed; its
watchdog ends with that early join failure. The join policy owns its binding
without changing RuntimeSession’s separate interrupt-coalescing contract.
handleRuntimeInterrupt accepts a synchronous or asynchronous onBeforeStop
hook. It awaits cancellation before calling stop, under one shared shutdown
watchdog. A thrown/rejected hook is reported and cleanup continues while time
remains. Hook time reduces the budget available to cleanup. If the hook hangs
past the deadline, exit 1 is requested; late hook settlement is observed and
does not start cleanup after that timeout. Disabling the watchdog awaits both
phases without a deadline. Timers cannot preempt synchronous event-loop blocking.
GlobalErrorEscalation distinguishes a retained startup notification from
fatal-owned cleanup. If an external orderly shutdown starts first, activation
or watchdog expiry discards the retained notification. External shutdown must
have its own deadline; the escalation interface cannot observe its stop promise
immediately. Once fatal cleanup starts, stopping state or a shutdown predicate
does not disable the watchdog. Observed fatal-stop settlement clears the timer;
stop and exit requests are shared per escalation instance. Exit failures are
reported safely without detached rejections or implicit retry. An awaited
activation still rejects if its exit request fails.
stopUniqueResource resolves a resource or its creation promise and invokes
teardown once across a shared seen set. It preserves method precedence:
stop, then destroy, then shutdown when neither earlier method exists.
Shutdown-only cleanup is awaited and its failures propagate. Repeated aliases
are skipped; the helper does not share the first teardown's completion promise
with later callers. Coordinate ownership when resources also have an explicit
cleanup path.
redactConfig masks common credential keys including mnemonic, connection
string, API key, access/refresh/session/auth tokens, and authorization, plus
private-key/secret/password suffixes. Default names and suffixes ignore case
and snake/hyphen separators. Extra keyNames use the same normalization;
replacement suffix patterns see original key spelling and run without global
or sticky state, leaving the supplied regex untouched. Public seed and
token metadata remain visible unless explicitly selected. Plain objects and
arrays are copied; non-plain objects remain outside the traversal.
String values now remove the entire URL userinfo component by default,
including under arbitrary keys and inside arrays. For example,
https://user:password@HOST/path?keep=1 becomes https://HOST/path?keep=1.
The original configuration is unchanged; retain it for connections and use
the returned copy only for diagnostics. This deliberately hides usernames as
well as passwords. { urlCredentials: 'preserve' } opts into the former string
behavior while keeping credential-key masks.
redactUrlCredentials(value) exposes that diagnostic string transformation
separately. It handles complete hierarchical scheme://authority and
//authority values, preserving the remaining bytes without URL
canonicalization. Ambiguous credential-bearing authorities and noncanonical
network-scheme values containing @ are masked entirely. It does not validate
URLs or sanitize embedded prose, opaque URIs, query/fragment/path secrets,
object keys, or non-plain URL instances. Credential-looking malformed values
can lose all diagnostic detail. URL work scans strings during config output;
no performance benchmark is claimed.
Application shell pattern (reference)
The public package @xyo-network/lifehash-indexer is the golden template for a product CLI on this kit:
- Bin / process boundary —
runProcessApplication + createNodeProcessHost (or nodeProcessHost) so the library bundle stays free of process side effects.
- Yargs adapter —
runYargsApplication with an app-owned configure* helper that applies rejectUnknownCommands, .strictOptions(), help/version, and host.io.columns wrapping.
- Command catalog —
createCommandCatalog over a CliApplicationContext that at least carries host, so tests inject a recording ProcessHost without touching process.
- Failure policy — a
FailureExitCodeMapper that maps parse vs handler failures onto SYS_EXITS (usage / config / software, …).
- Domain commands — yargs
CommandModule factories that take context and never import Node process globals for env/argv/exit.
Sibling shells that follow the same shape include @xyo-network/webble-cli and @xyo-network/webble-dapp. Actor-heavy CLIs (for example @xyo-network/xl1-cli-lib) layer createActorBuilderCatalog, ManagedActor, and RuntimeSession on top of the same process boundary.
Lifecycle guarantees
- Config-driven launches resolve and provision one provider system shared by every actor context.
launchCliSystem defaults resident actors to registration-order startup/readiness and reverse-order shutdown.
runCliSystemUntilInterrupt stops once on process interrupt or resident actor failure, returns on clean interrupt cleanup, and retains actor/cleanup errors unless policy escalation ends the join first.
- Actors start sequentially in registration order.
- A failed startup first invokes the failing actor's optional, status-independent
shutdown, then rolls back every actor that already started in reverse order.
- Readiness is observed in registration order with an optional supervisor-wide timeout.
- Shutdown runs in reverse startup order and is idempotent.
- A failed normal
stop uses the actor's optional shutdown fallback.
- Shutdown attempts every started actor before reporting aggregated failures.
- A runtime session invokes its stop delegate once and shares one stop promise.
- Runtime stop failures retain and rethrow the original value without wrapping.
- A runtime session accepts at most one interrupt binding while active.
- Repeated interrupts share one listener promise and retain the binding until that listener settles.
- Manual shutdown disposes its interrupt binding before cleanup begins.
- Process applications absorb host-routed exits and map unexpected failures to exit code one, or to the code selected by an application-supplied mapper.
- Command catalogs preserve registration order and reject blank or duplicate ids.
- Command factories receive the exact per-application context and are invoked again for each materialization.
- Service-until-interrupt disposes its host listener before stop and surfaces stop failures to the caller.
License
LGPL-3.0-only