@enclosurejs/commands
v1.1.0
Published
Semantic command registry with execute pipeline for Enclosure apps
Downloads
245
Readme
@enclosurejs/commands — Semantic command registry with keyboard shortcut binding
[!IMPORTANT] This is a universal module that provides a semantic command registry for Enclosure apps. Commands are registered with a stable string id, human-readable title, and handler. Any surface — command palette, menu, toolbar, keyboard shortcut, test harness — invokes the same handler through
execute(id). Optional integration withShortcutTokenbinds platform keyboard shortcuts to command execution without coupling the registry to any platform.
The Problem
Desktop and web shells need a single execution path for user actions: palette search, menu clicks, toolbar buttons, and keyboard shortcuts should all funnel through the same handler. Without a registry, each app reinvents command lookup, duplicate-id detection, shortcut binding, and disposable cleanup. The raw ShortcutService in core maps accelerator → callback but has no notion of command identity, metadata for UI discovery, or structured registration/unregistration.
@enclosurejs/commands solves this with:
- Registry —
register(definition, handler)with duplicate detection (strictIds) andDisposablecleanup - Execute —
execute(id)as the single invocation path, sync and async handlers,CoreErroron unknown ids - Discovery —
list()returns sorted descriptors for palette/menu rendering - Shortcut binding —
bindShortcut(id, accelerator)delegates toShortcutTokenwhen present in DI
Architecture
┌─────────────────── Application ──────────────────┐
│ │
│ Module installs CommandsService via DI │
│ │
│ register({ id: 'app.save', title: 'Save' }, fn) │
│ │ │
│ │ ┌──────────────┐ │
│ └─▶│ Registry │◀── execute('app.save')
│ │ Map<id, fn> │ │
│ └──────┬───────┘ │
│ │ │
│ bindShortcut('app.save', 'Ctrl+S') │
│ │ │
│ ┌──────▼───────┐ │
│ │ ShortcutToken │ (optional, via DI) │
│ │ platform KB │ │
│ └──────────────┘ │
└────────────────────────────────────────────────────┘Relationship to ShortcutService
| Layer | Responsibility |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| ShortcutService (core capability) | Platform: register(accelerator, handler) — global key chord fires callback |
| @enclosurejs/commands | App: register({ id, title }, handler) + execute(id) + optional bindShortcut(id, accelerator) that delegates to ShortcutToken |
No overlap — shortcuts stay the low-level platform capability; commands add identity, discovery, and a single funnel for invocation.
Quick Start
Via Module (recommended)
import { createCommandsModule, CommandsToken } from '@enclosurejs/commands';
const app = createApp({
modules: [createCommandsModule()],
// ...
});
// After app start, resolve from DI:
const commands = context.use(CommandsToken);
const disposable = commands.register(
{ id: 'app.save', title: 'Save', category: 'File' },
async () => {
await saveDocument();
},
);
await commands.execute('app.save');
// Optional: bind a keyboard shortcut (requires ShortcutToken in DI)
commands.bindShortcut('app.save', 'Ctrl+S');
// Cleanup
disposable.dispose();Direct use (without Module)
import { CommandsServiceImpl } from '@enclosurejs/commands';
const service = new CommandsServiceImpl(context, { strictIds: false });
service.register({ id: 'edit.copy', title: 'Copy' }, () => copySelection());API
| Export | Kind | Purpose |
| ---------------------- | ------- | ----------------------------------------------------- |
| createCommandsModule | factory | Returns a Module that provides CommandsToken |
| CommandsToken | token | Resolve CommandsService from DI |
| CommandsServiceImpl | class | Direct instantiation without module system |
| createShortcutBinder | factory | Low-level bridge to ShortcutToken (used internally) |
CommandDefinition
| Field | Type | Required | Description |
| ---------- | -------- | -------- | -------------------------------------- |
| id | string | Yes | Stable identifier (e.g. 'app.save') |
| title | string | Yes | Human-readable label for palette/menu |
| category | string | No | Grouping key (e.g. 'File', 'Edit') |
CommandsOptions
| Field | Type | Default | Description |
| ----------- | --------- | ------- | ------------------------------------------------------- |
| strictIds | boolean | true | true: duplicate id throws; false: replaces silently |
CommandsService
| Member | Type | Description |
| ------------------------------- | ------ | ------------------------------------------------------------------------------------- |
| register(definition, handler) | method | Register a command. Returns Disposable that unregisters on dispose |
| unregister(id) | method | Remove by id. No-op if missing |
| execute(id) | method | Invoke the handler. Throws CoreError (COMMAND_NOT_FOUND) if unknown |
| has(id) | method | Check if a command is registered |
| list() | method | All descriptors, sorted by id |
| bindShortcut(id, accelerator) | method | Bind keyboard shortcut via ShortcutToken. Throws if token absent or command unknown |
ShortcutBinder
| Member | Type | Description |
| ------------------------------ | ------ | ----------------------------------------------------------------------- |
| bind(commandId, accelerator) | method | Register an accelerator that executes the command. Returns Disposable |
Configuration
Zero configuration files. Pass CommandsOptions to createCommandsModule() or CommandsServiceImpl constructor:
createCommandsModule({ strictIds: false });The only option is strictIds (default true) — controls duplicate command id policy.
Types Exported
| Type | Used by |
| ------------------- | ----------------------------------------------- |
| CommandsService | Consumers resolving from DI via CommandsToken |
| CommandDefinition | Command registration call sites |
| CommandDescriptor | Palette/menu renderers consuming list() |
| CommandHandler | Handler functions passed to register() |
| CommandsOptions | Module/service configuration |
| ShortcutBinder | Advanced: custom shortcut binding logic |
Safety
Error Model
All errors thrown by this package are CoreError instances (domain 'commands'):
| Code | When |
| --------------------- | ----------------------------------------------------------- |
| DUPLICATE_COMMAND | register() with an already-used id when strictIds: true |
| COMMAND_NOT_FOUND | execute() or bindShortcut() with an unknown command id |
| NO_SHORTCUT_SERVICE | bindShortcut() when ShortcutToken is not in DI |
Lifecycle Safety
register()returnsDisposable— add toDisposableGroupfor automatic cleanupunregister()is idempotent (no-op for missing ids)bindShortcut()returnsDisposablethat unbinds the keyboard shortcut
Handler Safety
- Sync and async handlers are both supported via
Promise.resolve(handler()) - Handler errors propagate through the
execute()promise — callers can catch per-command - Concurrent executions of the same command are allowed (no single-flight restriction)
Benchmarks
Not applicable. The command registry is a thin in-memory Map lookup — register, execute, has are O(1), list is O(n log n) sort. No I/O, no workers, no async overhead beyond handler execution. Performance is dominated by the handlers themselves, not the registry.
Bundle Size
| Output | File | Size |
| ------------ | ------------ | ------- |
| Runtime (JS) | index.js | 2.65 KB |
| Types (DTS) | index.d.ts | 2.51 KB |
| Total | | 5.16 KB |
Single entrypoint. Single external dependency (@enclosurejs/core) marked as external in the build.
Quality
| Metric | Value |
| --------------------- | ------------------------------------------------------------------ |
| Unit tests | 38 (all pass) |
| Test files | 3 (service, module, shortcuts-bind) |
| Source files | 5 (types, service, shortcuts-bind, module, index) |
| Dependencies | 1 (@enclosurejs/core — workspace) |
| External dependencies | 0 (devDependencies only: tsup) |
| Coverage thresholds | statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90% |
Quality Layers
Layer 1: STATIC ANALYSIS (every commit)
tsc --noEmit strict mode, zero errors
eslint ESLint 9 flat config, zero warnings
prettier --check formatting
Layer 2: UNIT TESTS (every commit)
38 tests service (23), module (8), shortcuts-bind (7)
covers register, unregister, execute, has, list,
duplicate detection, strictIds, dispose, bindShortcut,
ShortcutToken absent/present, async handlers,
concurrent execute, error propagation
v8 coverage statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90%
Layer 3: BENCHMARKS
N/A in-memory Map — no meaningful benchmark target
Layer 4: PACKAGE HEALTH
1 workspace dep @enclosurejs/core (types + CoreError + DI, externalized in build)
tsup build ESM + DTS output, single entrypointFile Structure
packages/commands/
├── src/
│ ├── index.ts Barrel: types, service, module, binder
│ ├── types.ts CommandDefinition, CommandDescriptor, CommandsService, CommandsOptions
│ ├── service.ts CommandsServiceImpl — registry, execute, list, shortcut binding
│ ├── shortcuts-bind.ts ShortcutBinder — optional bridge to ShortcutToken
│ ├── module.ts createCommandsModule(), CommandsToken
│ └── __tests__/
│ ├── service.test.ts 23 tests — register, unregister, execute, has, list, bindShortcut
│ ├── module.test.ts 8 tests — module id, install, token provision, options forwarding
│ └── shortcuts-bind.test.ts 7 tests — binder creation, bind/unbind, handler delegation
├── .prettierignore
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── spec.md
└── README.mdLicense
MIT
