# @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.

```ts
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:

```ts
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.

| Host supplied to the join | Listener during cleanup | Second interrupt | Watchdog expiry |
| --- | --- | --- | --- |
| 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.

```ts
await runServiceUntilInterrupt(nodeProcessHost, () => server.close(), {
  shutdownTimeoutMs: 30_000,
})
// Or forward an exit boundary alongside a minimal structural interrupt host:
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`](https://github.com/XYOracleNetwork/lifehash/tree/main/packages/indexer) is the golden template for a product CLI on this kit:

1. **Bin / process boundary** — `runProcessApplication` + `createNodeProcessHost` (or `nodeProcessHost`) so the library bundle stays free of process side effects.
2. **Yargs adapter** — `runYargsApplication` with an app-owned `configure*` helper that applies `rejectUnknownCommands`, `.strictOptions()`, help/version, and `host.io.columns` wrapping.
3. **Command catalog** — `createCommandCatalog` over a `CliApplicationContext` that at least carries `host`, so tests inject a recording `ProcessHost` without touching `process`.
4. **Failure policy** — a `FailureExitCodeMapper` that maps parse vs handler failures onto `SYS_EXITS` (`usage` / `config` / `software`, …).
5. **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
