Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@suaveplan/api-plugin - Plugin system foundation for the Suaveplan API — typed PluginDescriptor factory, PluginRegistry with 4-phase lifecycle (onRegister → onInit → onReady / onClose in reverse), deterministic Kahn topological sort with alphabetical tie-break, AbortSignal-enforced initTimeout/shutdownTimeout, type-safe ServiceRegistry via module augmentation, priority-ordered HookExecutor for non-lifecycle hooks, defineCodes-typed PluginError with ten PLUGIN.* codes. Universal — runs unchanged in Bun, Node, and the browser.
Plugin-system foundation for the Suaveplan API. Universal — runs unchanged in Bun, Node, and the browser.
Documentation tree
- context — The
contextmodule ships the runtime object every plugin lifecycle hook receives.PluginContext<TConfig>carries th…- context — The
contextmodule ships the runtime object every plugin lifecycle hook receives.PluginContext<TConfig>carries th…
- context — The
- define — The
definemodule exports a single function —definePlugin<TConfig>(descriptor)— that is the canonical entry point…- define — The
definemodule exports a single function —definePlugin<TConfig>(descriptor)— that is the canonical entry point…
- define — The
- error-codes — The
error-codesmodule is the single source of truth for every failure mode the@suaveplan/api-pluginpackage can r…- error-codes — The
error-codesmodule is the single source of truth for every failure mode the@suaveplan/api-pluginpackage can r…
- error-codes — The
- hook-executor — The
hook-executormodule ships a small, priority-ordered dispatcher for non-lifecycle plugin hooks — custom event…- hook-executor — The
hook-executormodule ships a small, priority-ordered dispatcher for non-lifecycle plugin hooks — custom event…
- hook-executor — The
- registry — The
registrymodule ownsPluginRegistry, the single canonical orchestrator for the plugin system. Its job is to:- registry — The
registrymodule ownsPluginRegistry, the single canonical orchestrator for the plugin system. Its job is to:
- registry — The
- service-registry — The
service-registrymodule defines the single shared interface that plugins augment to publish typed services to oth…- service-registry — The
service-registrymodule defines the single shared interface that plugins augment to publish typed services to oth…
- service-registry — The
- topo-sort — The
topo-sortmodule owns the deterministic dependency-ordering primitive thePluginRegistryuses to schedule lifec…- topo-sort — The
topo-sortmodule owns the deterministic dependency-ordering primitive thePluginRegistryuses to schedule lifec…
- topo-sort — The
- types — The
typesmodule holds every shared shape used across the api-plugin package: the immutable `PluginDescriptor<TConfig…- api-plugin — The
typesmodule holds every shared shape used across the api-plugin package: the immutable `PluginDescriptor<TConfig…
- api-plugin — The
What you get
definePlugin<TConfig>(descriptor)— typed plugin-descriptor factory.PluginRegistry— 4-phase lifecycle (onRegister → onInit → onReadyforward,onClosereverse) with deterministic dep ordering,initTimeout/shutdownTimeoutenforcement viaAbortSignal, OTel span per hook.topologicalSort— Kahn's algorithm with alphabetical tie-break + DFS cycle detection.ServiceRegistry— empty interface designed for module augmentation. Plugins declare typed service slots; consumers get fully-typedgetService<K>/registerService<K>with no casts.HookExecutor— priority-ordered, non-lifecycle hook dispatcher (custom + sibling-package request/tenant/db hooks). Rejects registration of the four lifecycle hook names — those are registry-owned.PluginError+PLUGIN_CODES— 10 stable error codes viadefineCodes.
Quick-start
import { createPluginRegistry, definePlugin } from "@suaveplan/api-plugin";
import z from "zod";
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry { db: { query: (sql: string) => unknown } }
}
const db = definePlugin({
name: "db",
version: "0.1.0",
onInit(ctx) {
ctx.registerService("db", { query: (sql) => ({ sql }) });
},
});
const api = definePlugin<{ port: number }>({
name: "api",
version: "0.1.0",
dependencies: ["db"],
configSchema: z.object({ port: z.number().int().positive() }),
defaults: { port: 3000 },
async onReady(ctx) {
const { port } = ctx.getOwnConfig();
const db = ctx.getService("db");
ctx.logger.info("api ready", { port, hasDb: !!db });
},
});
const registry = createPluginRegistry({ initTimeout: 5_000 });
registry.registerAll([db, api]);
await registry.initialize();
// later …
await registry.shutdown();
Modules
| Subpath | Exports |
|---|---|
@suaveplan/api-plugin |
barrel — everything below |
@suaveplan/api-plugin/define |
definePlugin |
@suaveplan/api-plugin/registry |
PluginRegistry, createPluginRegistry, PluginRegistryOptions |
@suaveplan/api-plugin/context |
createPluginContext, PluginContext |
@suaveplan/api-plugin/service-registry |
ServiceRegistry (augment me), ServiceKey |
@suaveplan/api-plugin/hook-executor |
HookExecutor, createHookExecutor, LIFECYCLE_HOOK_NAMES |
@suaveplan/api-plugin/topo-sort |
topologicalSort, validateDependencies |
@suaveplan/api-plugin/error-codes |
PLUGIN_CODES, PluginError, PluginErrorCode |
@suaveplan/api-plugin/types |
PluginDescriptor, PluginInstance, PluginNode, PluginState, PluginLifecycleHook, TopologicalSortResult |
Augmenting ServiceRegistry
ServiceRegistry ships empty. Plugins augment it to publish typed services:
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry {
cache: CacheAdapter;
auth: AuthAdapter;
}
}
After that:
ctx.getService("cache"); // CacheAdapter | undefined
ctx.getService("nope"); // type error — not augmented
Error codes
PLUGIN_CODES defines ten codes (PLUGIN.NOT_FOUND, PLUGIN.ALREADY_REGISTERED, PLUGIN.INIT_FAILED, PLUGIN.DEP_MISSING, PLUGIN.CIRCULAR_DEPENDENCY, PLUGIN.INIT_TIMEOUT, PLUGIN.SHUTDOWN_FAILED, PLUGIN.INVALID_CONFIG, PLUGIN.HOOK_FAILED, PLUGIN.ABORTED). Every code is raised by at least one production call site — the spec-conformance test pins this invariant.
design decisions
- Unified lifecycle runtime —
PluginRegistrycallsonRegister/onInit/onReady/onClosedirectly; theHookExecutorrejects registration of these four names. Upstream's dual-runtime hazard is eliminated by construction. initTimeout/shutdownTimeoutare enforced — every hook runs with anAbortController-boundctx.signalthat fires when the budget is exceeded.getService<K>is type-safe viaServiceRegistrymodule augmentation. No moregetService<MyType>(name)casts.- Topological sort is deterministic — alphabetical tie-break for equal-in-degree nodes makes boot logs reproducible across runs.
- No
applyMigrations— migrations live in@suaveplan/api-migrations. The plugin registry knows nothing about databases. - No ALS singletons (
getCurrentApp, etc.). Callers threadctxexplicitly.