Files
skills/plugins/document-navigator/docs/api-stack/api/api-plugin
admin 9bf56a806b Move docs/ package mirror into document-navigator plugin
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>
2026-05-30 01:10:00 +01:00
..

@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 context module ships the runtime object every plugin lifecycle hook receives. PluginContext<TConfig> carries th…
    • context — The context module ships the runtime object every plugin lifecycle hook receives. PluginContext<TConfig> carries th…
  • define — The define module exports a single function — definePlugin<TConfig>(descriptor) — that is the canonical entry point…
    • define — The define module exports a single function — definePlugin<TConfig>(descriptor) — that is the canonical entry point…
  • error-codes — The error-codes module is the single source of truth for every failure mode the @suaveplan/api-plugin package can r…
    • error-codes — The error-codes module is the single source of truth for every failure mode the @suaveplan/api-plugin package can r…
  • hook-executor — The hook-executor module ships a small, priority-ordered dispatcher for non-lifecycle plugin hooks — custom event…
    • hook-executor — The hook-executor module ships a small, priority-ordered dispatcher for non-lifecycle plugin hooks — custom event…
  • registry — The registry module owns PluginRegistry, the single canonical orchestrator for the plugin system. Its job is to:
    • registry — The registry module owns PluginRegistry, the single canonical orchestrator for the plugin system. Its job is to:
  • service-registry — The service-registry module defines the single shared interface that plugins augment to publish typed services to oth…
    • service-registry — The service-registry module defines the single shared interface that plugins augment to publish typed services to oth…
  • topo-sort — The topo-sort module owns the deterministic dependency-ordering primitive the PluginRegistry uses to schedule lifec…
    • topo-sort — The topo-sort module owns the deterministic dependency-ordering primitive the PluginRegistry uses to schedule lifec…
  • types — The types module holds every shared shape used across the api-plugin package: the immutable `PluginDescriptor<TConfig…
    • api-plugin — The types module holds every shared shape used across the api-plugin package: the immutable `PluginDescriptor<TConfig…

What you get

  • definePlugin<TConfig>(descriptor) — typed plugin-descriptor factory.
  • PluginRegistry — 4-phase lifecycle (onRegister → onInit → onReady forward, onClose reverse) with deterministic dep ordering, initTimeout / shutdownTimeout enforcement via AbortSignal, 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-typed getService<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 via defineCodes.

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 — PluginRegistry calls onRegister/onInit/onReady/onClose directly; the HookExecutor rejects registration of these four names. Upstream's dual-runtime hazard is eliminated by construction.
  • initTimeout/shutdownTimeout are enforced — every hook runs with an AbortController-bound ctx.signal that fires when the budget is exceeded.
  • getService<K> is type-safe via ServiceRegistry module augmentation. No more getService<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 thread ctx explicitly.