Typed Operational Tasks
A team that needs to backfill a column, replay an event store, or migrate a schema reaches for a one-off script. Six months later there are forty one-off scripts, no two arguments-parsing approaches the same, and the production runbook is find the right script, hope it still works. @CliTask from @frenchexdev/ddd-cli-task reifies operational tasks as typed commands exposed at the CLI boundary — the same @Command that the HTTP route dispatches, also dispatchable from the terminal.
What @CliTask Reifies
The insight is that an operational task is structurally identical to an application command. Cancel this subscription. Backfill the projection from event N. Reset the audit trail for a customer. Each is a typed command — same @Command shape as the HTTP API, same handler, same idempotency discipline. The only difference is the boundary that hydrates the command from inputs — CLI arguments parsed by a CLI framework vs. JSON body parsed by an HTTP adapter.
The pattern reifies the boundary. @CliTask declares this command is exposed as a CLI verb. The CLI framework (ddd-cli) reads the registry, registers the verb, parses arguments according to the command's typed shape, dispatches through the mediator. The handler is the same one the HTTP route uses. The audit trail, the metrics, the logging — all the same as a production HTTP dispatch.
The discipline pays off in two places. Operational tasks are auditable: every CLI invocation runs through the same handler that records audit-trail entries, so the production runbook generates the same compliance signal as the API. Operational tasks are testable: the same handler tests cover both the HTTP path and the CLI path; the CLI hydration is a thin layer.
The Runtime: ddd-cli-task
The runtime is at the M4/M5 stub milestone. The decorator surface will declare the CLI verb name and the command class it dispatches.
A sketched future shape:
// Sketch — runtime decorator not yet exported.
@CliTask({
verb: 'subscription:cancel',
dispatches: 'CancelSubscriptionCommand',
})
export class CancelSubscriptionCliTask {}
// CLI invocation:
// $ ddd subscription:cancel --subscriptionId=sub_xyz --reason="user request"
// → Parsed into CancelSubscriptionCommand
// → Dispatched through Mediator
// → Same handler as the HTTP API// Sketch — runtime decorator not yet exported.
@CliTask({
verb: 'subscription:cancel',
dispatches: 'CancelSubscriptionCommand',
})
export class CancelSubscriptionCliTask {}
// CLI invocation:
// $ ddd subscription:cancel --subscriptionId=sub_xyz --reason="user request"
// → Parsed into CancelSubscriptionCommand
// → Dispatched through Mediator
// → Same handler as the HTTP APIThe CLI framework parses arguments against the command class's constructor parameters, validates types, dispatches. The exit code reflects the Result<T, E> shape — zero for ok, non-zero with the error printed for err.
Cross-Links
- Dispatches a
@Commandthrough the@Mediator— same as HTTP and message-bus paths. - Handled by
@CommandHandler— single handler shared across all boundaries. - Hosted by the CLI Framework — the framework registers the verb, parses arguments, dispatches.
- Generates
@AuditTrailentries the same way HTTP commands do.
Back to the series index.