@a16njs/engine 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ import * as fs from 'fs/promises';
2
+ import { statSync } from 'fs';
3
+ import * as path from 'path';
4
+ import { pathToFileURL, fileURLToPath } from 'url';
5
+ /** Pattern used to match plugin package directory names. */
6
+ const PLUGIN_PREFIX = 'a16n-plugin-';
7
+ /**
8
+ * Scan search paths for installed `a16n-plugin-*` packages, dynamically import
9
+ * each one, validate its default export, and return the valid plugins along
10
+ * with error info for any that failed.
11
+ *
12
+ * @param options - Optional search path overrides
13
+ * @returns Discovered plugins and any load errors
14
+ */
15
+ export async function discoverInstalledPlugins(options) {
16
+ const searchPaths = options?.searchPaths ?? getDefaultSearchPaths();
17
+ const plugins = [];
18
+ const errors = [];
19
+ const seenPluginIds = new Set();
20
+ for (const searchPath of searchPaths) {
21
+ // Read directory entries; skip if path doesn't exist
22
+ let entries;
23
+ try {
24
+ entries = await fs.readdir(searchPath);
25
+ }
26
+ catch {
27
+ // Non-existent or unreadable path — skip silently
28
+ continue;
29
+ }
30
+ // Filter for directories matching the plugin naming convention
31
+ const pluginDirs = entries.filter((name) => name.startsWith(PLUGIN_PREFIX));
32
+ for (const dirName of pluginDirs) {
33
+ const pkgPath = path.join(searchPath, dirName);
34
+ try {
35
+ // Resolve entry point from package.json main field, falling back to index.js
36
+ const entryFile = await resolvePluginEntry(pkgPath);
37
+ const moduleUrl = pathToFileURL(entryFile).href;
38
+ const mod = await import(moduleUrl);
39
+ // Extract default export (handles both `export default` and CJS interop)
40
+ const candidate = mod.default ?? mod;
41
+ if (isValidPlugin(candidate)) {
42
+ if (seenPluginIds.has(candidate.id)) {
43
+ errors.push({
44
+ packageName: dirName,
45
+ error: `Duplicate plugin id: ${candidate.id} — already discovered`,
46
+ });
47
+ }
48
+ else {
49
+ seenPluginIds.add(candidate.id);
50
+ plugins.push(candidate);
51
+ }
52
+ }
53
+ else {
54
+ errors.push({
55
+ packageName: dirName,
56
+ error: `Invalid plugin export: missing or incorrect required fields (id, name, supports, discover, emit)`,
57
+ });
58
+ }
59
+ }
60
+ catch (err) {
61
+ errors.push({
62
+ packageName: dirName,
63
+ error: err instanceof Error ? err.message : String(err),
64
+ });
65
+ }
66
+ }
67
+ }
68
+ return { plugins, errors };
69
+ }
70
+ /**
71
+ * Resolve the JavaScript entry point for a plugin package directory.
72
+ *
73
+ * Reads the `main` field from the package's `package.json` if it exists,
74
+ * otherwise falls back to `index.js` at the package root.
75
+ *
76
+ * @param pkgPath - Absolute path to the plugin package directory
77
+ * @returns Absolute path to the entry JavaScript file
78
+ */
79
+ async function resolvePluginEntry(pkgPath) {
80
+ try {
81
+ const raw = await fs.readFile(path.join(pkgPath, 'package.json'), 'utf-8');
82
+ const pkg = JSON.parse(raw);
83
+ if (typeof pkg.main === 'string' && pkg.main.length > 0) {
84
+ return path.resolve(pkgPath, pkg.main);
85
+ }
86
+ }
87
+ catch {
88
+ // No package.json or unreadable — fall through to default
89
+ }
90
+ return path.join(pkgPath, 'index.js');
91
+ }
92
+ /**
93
+ * Type-guard that checks whether an unknown value satisfies the A16nPlugin interface.
94
+ *
95
+ * Validates presence and types of: id (string), name (string), supports (array),
96
+ * discover (function), emit (function).
97
+ *
98
+ * @param obj - The value to check
99
+ * @returns True if obj is a valid A16nPlugin
100
+ */
101
+ export function isValidPlugin(obj) {
102
+ if (obj == null || typeof obj !== 'object') {
103
+ return false;
104
+ }
105
+ const candidate = obj;
106
+ return (typeof candidate.id === 'string' &&
107
+ typeof candidate.name === 'string' &&
108
+ Array.isArray(candidate.supports) &&
109
+ typeof candidate.discover === 'function' &&
110
+ typeof candidate.emit === 'function');
111
+ }
112
+ /**
113
+ * Compute the default search paths for plugin discovery.
114
+ *
115
+ * Returns paths where `a16n-plugin-*` packages might be installed:
116
+ * - The global npm `node_modules` directory (derived from this package's location)
117
+ * - The local `node_modules` in the current working directory
118
+ *
119
+ * In a global install, the engine lives inside node_modules:
120
+ * .../lib/node_modules/@a16njs/engine/dist/plugin-discovery.js
121
+ * so walking up finds the node_modules parent directly.
122
+ *
123
+ * In a monorepo (e.g. pnpm workspace), the engine lives at:
124
+ * <root>/packages/engine/dist/plugin-discovery.js
125
+ * so we also check for a node_modules child directory at each level.
126
+ *
127
+ * @returns Array of directory paths to scan
128
+ */
129
+ export function getDefaultSearchPaths() {
130
+ const paths = [];
131
+ const thisFile = fileURLToPath(import.meta.url);
132
+ let dir = path.dirname(thisFile);
133
+ while (dir !== path.dirname(dir)) {
134
+ // Global install: this file is inside a node_modules tree
135
+ if (path.basename(dir) === 'node_modules') {
136
+ paths.push(dir);
137
+ break;
138
+ }
139
+ // Monorepo / local dev: check for a sibling node_modules directory.
140
+ // Don't stop — keep walking up to find all ancestor node_modules
141
+ // (e.g. packages/engine/node_modules AND root/node_modules).
142
+ const siblingNodeModules = path.join(dir, 'node_modules');
143
+ try {
144
+ const stat = statSync(siblingNodeModules);
145
+ if (stat.isDirectory()) {
146
+ paths.push(siblingNodeModules);
147
+ }
148
+ }
149
+ catch {
150
+ // Directory doesn't exist, keep walking
151
+ }
152
+ dir = path.dirname(dir);
153
+ }
154
+ // Local node_modules in cwd
155
+ const localNodeModules = path.join(process.cwd(), 'node_modules');
156
+ if (!paths.includes(localNodeModules)) {
157
+ paths.push(localNodeModules);
158
+ }
159
+ return paths;
160
+ }
161
+ //# sourceMappingURL=plugin-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-discovery.js","sourceRoot":"","sources":["../src/plugin-discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ import type { A16nPlugin } from '@a16njs/models';
2
+ import type { PluginRegistrationInput } from './plugin-registry.js';
3
+ import { PluginRegistry } from './plugin-registry.js';
4
+ import { type PluginDiscoveryOptions } from './plugin-discovery.js';
5
+ /**
6
+ * Strategy for resolving conflicts when a discovered plugin has the
7
+ * same ID as an already-registered plugin.
8
+ */
9
+ export declare enum PluginConflictStrategy {
10
+ /** Keep the existing (bundled) plugin, skip the discovered one. This is the default and matches current behavior. */
11
+ PREFER_BUNDLED = "prefer-bundled",
12
+ /** Replace the existing plugin with the discovered (installed) one. */
13
+ PREFER_INSTALLED = "prefer-installed",
14
+ /** Throw an error when a conflict is detected. */
15
+ FAIL = "fail"
16
+ }
17
+ /**
18
+ * Information about a plugin that was skipped during conflict resolution.
19
+ */
20
+ export interface SkippedPlugin {
21
+ /** The plugin that was skipped */
22
+ plugin: A16nPlugin;
23
+ /** Human-readable reason for skipping */
24
+ reason: string;
25
+ /** ID of the plugin it conflicted with */
26
+ conflictsWith: string;
27
+ }
28
+ /**
29
+ * Result of loading and resolving plugins.
30
+ */
31
+ export interface PluginLoadResult {
32
+ /** Plugins that passed conflict resolution and are ready for registration */
33
+ loaded: PluginRegistrationInput[];
34
+ /** Plugins that were skipped due to conflicts */
35
+ skipped: SkippedPlugin[];
36
+ /** Errors encountered during discovery */
37
+ errors: Array<{
38
+ packageName: string;
39
+ error: string;
40
+ }>;
41
+ }
42
+ /**
43
+ * Orchestrates plugin loading by coordinating discovery and conflict
44
+ * resolution as separate, testable phases.
45
+ *
46
+ * Separates three distinct concerns:
47
+ * 1. **Discovery** - Finding plugins in node_modules (delegated to discoverInstalledPlugins)
48
+ * 2. **Conflict Resolution** - Deciding what to do when discovered plugins conflict with existing ones
49
+ * 3. **Registration** - Adding resolved plugins to the registry (done by the caller)
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const loader = new PluginLoader(PluginConflictStrategy.PREFER_BUNDLED);
54
+ * const candidates = await loader.loadInstalled({ searchPaths: ['/path/to/node_modules'] });
55
+ * const resolved = loader.resolveConflicts(registry, candidates);
56
+ * for (const reg of resolved.loaded) {
57
+ * registry.register(reg);
58
+ * }
59
+ * ```
60
+ */
61
+ export declare class PluginLoader {
62
+ private readonly _conflictStrategy;
63
+ /**
64
+ * Create a new PluginLoader with the given conflict resolution strategy.
65
+ *
66
+ * @param conflictStrategy - How to handle plugin ID conflicts (default: PREFER_BUNDLED)
67
+ */
68
+ constructor(_conflictStrategy?: PluginConflictStrategy);
69
+ /**
70
+ * The conflict resolution strategy in use.
71
+ */
72
+ get conflictStrategy(): PluginConflictStrategy;
73
+ /**
74
+ * Discover installed plugins from node_modules and wrap them
75
+ * as PluginRegistrationInput candidates with source='installed'.
76
+ *
77
+ * This is Phase 1 (Discovery) of the loading pipeline.
78
+ *
79
+ * @param options - Plugin discovery options (search paths, etc.)
80
+ * @returns Load result with candidates ready for conflict resolution
81
+ */
82
+ loadInstalled(options?: PluginDiscoveryOptions): Promise<PluginLoadResult>;
83
+ /**
84
+ * Resolve conflicts between discovered plugin candidates and
85
+ * already-registered plugins using the configured strategy.
86
+ *
87
+ * This is Phase 2 (Conflict Resolution) of the loading pipeline.
88
+ * Pure function: does not modify the registry.
89
+ *
90
+ * @param existing - The current plugin registry to check for conflicts
91
+ * @param candidates - The load result from loadInstalled()
92
+ * @returns Resolved load result with loaded (ready to register) and skipped plugins
93
+ */
94
+ resolveConflicts(existing: PluginRegistry, candidates: PluginLoadResult): PluginLoadResult;
95
+ }
96
+ //# sourceMappingURL=plugin-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-loader.d.ts","sourceRoot":"","sources":["../src/plugin-loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAsB,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,oBAAY,sBAAsB;IAChC,qHAAqH;IACrH,cAAc,mBAAmB;IACjC,uEAAuE;IACvE,gBAAgB,qBAAqB;IACrC,kDAAkD;IAClD,IAAI,SAAS;CACd;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,MAAM,EAAE,UAAU,CAAC;IACnB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,MAAM,EAAE,uBAAuB,EAAE,CAAC;IAClC,iDAAiD;IACjD,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,0CAA0C;IAC1C,MAAM,EAAE,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,YAAY;IAOrB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IANpC;;;;OAIG;gBAEgB,iBAAiB,GAAE,sBAA8D;IAGpG;;OAEG;IACH,IAAI,gBAAgB,IAAI,sBAAsB,CAE7C;IAED;;;;;;;;OAQG;IACG,aAAa,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAahF;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,QAAQ,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,GAAG,gBAAgB;CAoC3F"}
@@ -0,0 +1,112 @@
1
+ import { discoverInstalledPlugins, } from './plugin-discovery.js';
2
+ /**
3
+ * Strategy for resolving conflicts when a discovered plugin has the
4
+ * same ID as an already-registered plugin.
5
+ */
6
+ export var PluginConflictStrategy;
7
+ (function (PluginConflictStrategy) {
8
+ /** Keep the existing (bundled) plugin, skip the discovered one. This is the default and matches current behavior. */
9
+ PluginConflictStrategy["PREFER_BUNDLED"] = "prefer-bundled";
10
+ /** Replace the existing plugin with the discovered (installed) one. */
11
+ PluginConflictStrategy["PREFER_INSTALLED"] = "prefer-installed";
12
+ /** Throw an error when a conflict is detected. */
13
+ PluginConflictStrategy["FAIL"] = "fail";
14
+ })(PluginConflictStrategy || (PluginConflictStrategy = {}));
15
+ /**
16
+ * Orchestrates plugin loading by coordinating discovery and conflict
17
+ * resolution as separate, testable phases.
18
+ *
19
+ * Separates three distinct concerns:
20
+ * 1. **Discovery** - Finding plugins in node_modules (delegated to discoverInstalledPlugins)
21
+ * 2. **Conflict Resolution** - Deciding what to do when discovered plugins conflict with existing ones
22
+ * 3. **Registration** - Adding resolved plugins to the registry (done by the caller)
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const loader = new PluginLoader(PluginConflictStrategy.PREFER_BUNDLED);
27
+ * const candidates = await loader.loadInstalled({ searchPaths: ['/path/to/node_modules'] });
28
+ * const resolved = loader.resolveConflicts(registry, candidates);
29
+ * for (const reg of resolved.loaded) {
30
+ * registry.register(reg);
31
+ * }
32
+ * ```
33
+ */
34
+ export class PluginLoader {
35
+ _conflictStrategy;
36
+ /**
37
+ * Create a new PluginLoader with the given conflict resolution strategy.
38
+ *
39
+ * @param conflictStrategy - How to handle plugin ID conflicts (default: PREFER_BUNDLED)
40
+ */
41
+ constructor(_conflictStrategy = PluginConflictStrategy.PREFER_BUNDLED) {
42
+ this._conflictStrategy = _conflictStrategy;
43
+ }
44
+ /**
45
+ * The conflict resolution strategy in use.
46
+ */
47
+ get conflictStrategy() {
48
+ return this._conflictStrategy;
49
+ }
50
+ /**
51
+ * Discover installed plugins from node_modules and wrap them
52
+ * as PluginRegistrationInput candidates with source='installed'.
53
+ *
54
+ * This is Phase 1 (Discovery) of the loading pipeline.
55
+ *
56
+ * @param options - Plugin discovery options (search paths, etc.)
57
+ * @returns Load result with candidates ready for conflict resolution
58
+ */
59
+ async loadInstalled(options) {
60
+ const discovered = await discoverInstalledPlugins(options);
61
+ return {
62
+ loaded: discovered.plugins.map((plugin) => ({
63
+ plugin,
64
+ source: 'installed',
65
+ })),
66
+ skipped: [],
67
+ errors: discovered.errors,
68
+ };
69
+ }
70
+ /**
71
+ * Resolve conflicts between discovered plugin candidates and
72
+ * already-registered plugins using the configured strategy.
73
+ *
74
+ * This is Phase 2 (Conflict Resolution) of the loading pipeline.
75
+ * Pure function: does not modify the registry.
76
+ *
77
+ * @param existing - The current plugin registry to check for conflicts
78
+ * @param candidates - The load result from loadInstalled()
79
+ * @returns Resolved load result with loaded (ready to register) and skipped plugins
80
+ */
81
+ resolveConflicts(existing, candidates) {
82
+ const loaded = [];
83
+ const skipped = [...candidates.skipped];
84
+ for (const candidate of candidates.loaded) {
85
+ const existingReg = existing.get(candidate.plugin.id);
86
+ if (!existingReg) {
87
+ loaded.push(candidate);
88
+ continue;
89
+ }
90
+ switch (this._conflictStrategy) {
91
+ case PluginConflictStrategy.PREFER_BUNDLED:
92
+ skipped.push({
93
+ plugin: candidate.plugin,
94
+ reason: `Conflict: ${existingReg.source} plugin '${existingReg.plugin.id}' already registered`,
95
+ conflictsWith: candidate.plugin.id,
96
+ });
97
+ break;
98
+ case PluginConflictStrategy.PREFER_INSTALLED:
99
+ loaded.push(candidate);
100
+ break;
101
+ case PluginConflictStrategy.FAIL:
102
+ throw new Error(`Plugin conflict: '${candidate.plugin.id}' is already registered`);
103
+ }
104
+ }
105
+ return {
106
+ loaded,
107
+ skipped,
108
+ errors: candidates.errors,
109
+ };
110
+ }
111
+ }
112
+ //# sourceMappingURL=plugin-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-loader.js","sourceRoot":"","sources":["../src/plugin-loader.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,wBAAwB,GAEzB,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,MAAM,CAAN,IAAY,sBAOX;AAPD,WAAY,sBAAsB;IAChC,qHAAqH;IACrH,2DAAiC,CAAA;IACjC,uEAAuE;IACvE,+DAAqC,CAAA;IACrC,kDAAkD;IAClD,uCAAa,CAAA;AACf,CAAC,EAPW,sBAAsB,KAAtB,sBAAsB,QAOjC;AA0BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,YAAY;IAOJ;IANnB;;;;OAIG;IACH,YACmB,oBAA4C,sBAAsB,CAAC,cAAc;QAAjF,sBAAiB,GAAjB,iBAAiB,CAAgE;IACjG,CAAC;IAEJ;;OAEG;IACH,IAAI,gBAAgB;QAClB,OAAO,IAAI,CAAC,iBAAiB,CAAC;IAChC,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,aAAa,CAAC,OAAgC;QAClD,MAAM,UAAU,GAAG,MAAM,wBAAwB,CAAC,OAAO,CAAC,CAAC;QAE3D,OAAO;YACL,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;gBAC1C,MAAM;gBACN,MAAM,EAAE,WAAoB;aAC7B,CAAC,CAAC;YACH,OAAO,EAAE,EAAE;YACX,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC;IACJ,CAAC;IAED;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,QAAwB,EAAE,UAA4B;QACrE,MAAM,MAAM,GAA8B,EAAE,CAAC;QAC7C,MAAM,OAAO,GAAoB,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;QAEzD,KAAK,MAAM,SAAS,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;YAC1C,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAEtD,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACvB,SAAS;YACX,CAAC;YAED,QAAQ,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBAC/B,KAAK,sBAAsB,CAAC,cAAc;oBACxC,OAAO,CAAC,IAAI,CAAC;wBACX,MAAM,EAAE,SAAS,CAAC,MAAM;wBACxB,MAAM,EAAE,aAAa,WAAW,CAAC,MAAM,YAAY,WAAW,CAAC,MAAM,CAAC,EAAE,sBAAsB;wBAC9F,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE;qBACnC,CAAC,CAAC;oBACH,MAAM;gBAER,KAAK,sBAAsB,CAAC,gBAAgB;oBAC1C,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;oBACvB,MAAM;gBAER,KAAK,sBAAsB,CAAC,IAAI;oBAC9B,MAAM,IAAI,KAAK,CAAC,qBAAqB,SAAS,CAAC,MAAM,CAAC,EAAE,yBAAyB,CAAC,CAAC;YACvF,CAAC;QACH,CAAC;QAED,OAAO;YACL,MAAM;YACN,OAAO;YACP,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,94 @@
1
+ import type { A16nPlugin } from '@a16njs/models';
2
+ /**
3
+ * Full metadata for a registered plugin.
4
+ * Wraps the plugin instance with registration metadata such as
5
+ * source (bundled vs installed), registration timestamp, and
6
+ * optional diagnostic information.
7
+ */
8
+ export interface PluginRegistration {
9
+ /** The plugin instance */
10
+ plugin: A16nPlugin;
11
+ /** Whether this plugin was bundled with the engine or installed from node_modules */
12
+ source: 'bundled' | 'installed';
13
+ /** When this plugin was registered */
14
+ registeredAt: Date;
15
+ /** Semantic version of the installed plugin (for installed plugins) */
16
+ version?: string;
17
+ /** Filesystem path where the plugin was installed from (for diagnostics) */
18
+ installPath?: string;
19
+ }
20
+ /**
21
+ * Input for registering a plugin. Omits `registeredAt` which is
22
+ * automatically set by the registry.
23
+ */
24
+ export type PluginRegistrationInput = Omit<PluginRegistration, 'registeredAt'>;
25
+ /**
26
+ * Unified plugin registry that serves as the single source of truth
27
+ * for all plugin metadata. Replaces the dual-Map pattern
28
+ * (plugins Map + pluginSources Map) with a single registry that
29
+ * tracks all plugin information in one place.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const registry = new PluginRegistry();
34
+ * registry.register({ plugin: cursorPlugin, source: 'bundled' });
35
+ * registry.register({ plugin: claudePlugin, source: 'installed', version: '1.0.0' });
36
+ *
37
+ * const cursor = registry.getPlugin('cursor');
38
+ * const installed = registry.listBySource('installed');
39
+ * ```
40
+ */
41
+ export declare class PluginRegistry {
42
+ private registrations;
43
+ /**
44
+ * Register a plugin with the registry.
45
+ * If a plugin with the same ID is already registered, it will be overwritten.
46
+ *
47
+ * @param input - Plugin and metadata to register (registeredAt is set automatically)
48
+ */
49
+ register(input: PluginRegistrationInput): void;
50
+ /**
51
+ * Get the full registration for a plugin by its ID.
52
+ *
53
+ * @param id - The plugin ID to look up
54
+ * @returns The full PluginRegistration, or undefined if not found
55
+ */
56
+ get(id: string): PluginRegistration | undefined;
57
+ /**
58
+ * Get just the plugin instance by its ID.
59
+ * Convenience method equivalent to `registry.get(id)?.plugin`.
60
+ *
61
+ * @param id - The plugin ID to look up
62
+ * @returns The A16nPlugin instance, or undefined if not found
63
+ */
64
+ getPlugin(id: string): A16nPlugin | undefined;
65
+ /**
66
+ * Check whether a plugin with the given ID is registered.
67
+ *
68
+ * @param id - The plugin ID to check
69
+ * @returns true if the plugin is registered, false otherwise
70
+ */
71
+ has(id: string): boolean;
72
+ /**
73
+ * List all plugin registrations.
74
+ *
75
+ * @returns Array of all PluginRegistration objects in insertion order
76
+ */
77
+ list(): PluginRegistration[];
78
+ /**
79
+ * List plugin registrations filtered by source.
80
+ *
81
+ * @param source - The source to filter by ('bundled' or 'installed')
82
+ * @returns Array of matching PluginRegistration objects
83
+ */
84
+ listBySource(source: 'bundled' | 'installed'): PluginRegistration[];
85
+ /**
86
+ * The number of registered plugins.
87
+ */
88
+ get size(): number;
89
+ /**
90
+ * Remove all plugin registrations.
91
+ */
92
+ clear(): void;
93
+ }
94
+ //# sourceMappingURL=plugin-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-registry.d.ts","sourceRoot":"","sources":["../src/plugin-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,qFAAqF;IACrF,MAAM,EAAE,SAAS,GAAG,WAAW,CAAC;IAChC,sCAAsC;IACtC,YAAY,EAAE,IAAI,CAAC;IACnB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,kBAAkB,EAAE,cAAc,CAAC,CAAC;AAE/E;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAA8C;IAEnE;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI;IAO9C;;;;;OAKG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAI/C;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAI7C;;;;;OAKG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB;;;;OAIG;IACH,IAAI,IAAI,kBAAkB,EAAE;IAI5B;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,kBAAkB,EAAE;IAInE;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Unified plugin registry that serves as the single source of truth
3
+ * for all plugin metadata. Replaces the dual-Map pattern
4
+ * (plugins Map + pluginSources Map) with a single registry that
5
+ * tracks all plugin information in one place.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const registry = new PluginRegistry();
10
+ * registry.register({ plugin: cursorPlugin, source: 'bundled' });
11
+ * registry.register({ plugin: claudePlugin, source: 'installed', version: '1.0.0' });
12
+ *
13
+ * const cursor = registry.getPlugin('cursor');
14
+ * const installed = registry.listBySource('installed');
15
+ * ```
16
+ */
17
+ export class PluginRegistry {
18
+ registrations = new Map();
19
+ /**
20
+ * Register a plugin with the registry.
21
+ * If a plugin with the same ID is already registered, it will be overwritten.
22
+ *
23
+ * @param input - Plugin and metadata to register (registeredAt is set automatically)
24
+ */
25
+ register(input) {
26
+ this.registrations.set(input.plugin.id, {
27
+ ...input,
28
+ registeredAt: new Date(),
29
+ });
30
+ }
31
+ /**
32
+ * Get the full registration for a plugin by its ID.
33
+ *
34
+ * @param id - The plugin ID to look up
35
+ * @returns The full PluginRegistration, or undefined if not found
36
+ */
37
+ get(id) {
38
+ return this.registrations.get(id);
39
+ }
40
+ /**
41
+ * Get just the plugin instance by its ID.
42
+ * Convenience method equivalent to `registry.get(id)?.plugin`.
43
+ *
44
+ * @param id - The plugin ID to look up
45
+ * @returns The A16nPlugin instance, or undefined if not found
46
+ */
47
+ getPlugin(id) {
48
+ return this.registrations.get(id)?.plugin;
49
+ }
50
+ /**
51
+ * Check whether a plugin with the given ID is registered.
52
+ *
53
+ * @param id - The plugin ID to check
54
+ * @returns true if the plugin is registered, false otherwise
55
+ */
56
+ has(id) {
57
+ return this.registrations.has(id);
58
+ }
59
+ /**
60
+ * List all plugin registrations.
61
+ *
62
+ * @returns Array of all PluginRegistration objects in insertion order
63
+ */
64
+ list() {
65
+ return Array.from(this.registrations.values());
66
+ }
67
+ /**
68
+ * List plugin registrations filtered by source.
69
+ *
70
+ * @param source - The source to filter by ('bundled' or 'installed')
71
+ * @returns Array of matching PluginRegistration objects
72
+ */
73
+ listBySource(source) {
74
+ return this.list().filter((r) => r.source === source);
75
+ }
76
+ /**
77
+ * The number of registered plugins.
78
+ */
79
+ get size() {
80
+ return this.registrations.size;
81
+ }
82
+ /**
83
+ * Remove all plugin registrations.
84
+ */
85
+ clear() {
86
+ this.registrations.clear();
87
+ }
88
+ }
89
+ //# sourceMappingURL=plugin-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-registry.js","sourceRoot":"","sources":["../src/plugin-registry.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,cAAc;IACjB,aAAa,GAAoC,IAAI,GAAG,EAAE,CAAC;IAEnE;;;;;OAKG;IACH,QAAQ,CAAC,KAA8B;QACrC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE;YACtC,GAAG,KAAK;YACR,YAAY,EAAE,IAAI,IAAI,EAAE;SACzB,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,EAAU;QAClB,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAC5C,CAAC;IAED;;;;;OAKG;IACH,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,IAAI;QACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC;IACjD,CAAC;IAED;;;;;OAKG;IACH,YAAY,CAAC,MAA+B;QAC1C,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;IACxD,CAAC;IAED;;OAEG;IACH,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;IACjC,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;IAC7B,CAAC;CACF"}
@@ -0,0 +1,93 @@
1
+ import type { AgentCustomization, A16nPlugin, EmitResult, Warning } from '@a16njs/models';
2
+ /**
3
+ * Context provided to a content transformation during the conversion pipeline.
4
+ */
5
+ export interface TransformationContext {
6
+ /** The items to transform */
7
+ items: AgentCustomization[];
8
+ /** The source plugin */
9
+ sourcePlugin: A16nPlugin;
10
+ /** The target plugin */
11
+ targetPlugin: A16nPlugin;
12
+ /** Root directory for source (discovery) */
13
+ sourceRoot: string;
14
+ /** Root directory for target (emission) */
15
+ targetRoot: string;
16
+ /**
17
+ * Perform a trial emission (dry-run) to discover file mapping.
18
+ * Only available when dryRun is false; stateful transformations
19
+ * like path rewriting use this to know target paths without
20
+ * actually writing files.
21
+ */
22
+ trialEmit: (items: AgentCustomization[]) => Promise<EmitResult>;
23
+ }
24
+ /**
25
+ * Result of applying a content transformation.
26
+ */
27
+ export interface TransformationResult {
28
+ /** The transformed items */
29
+ items: AgentCustomization[];
30
+ /** Warnings produced by this transformation */
31
+ warnings: Warning[];
32
+ }
33
+ /**
34
+ * A composable content transformation in the conversion pipeline.
35
+ *
36
+ * Transformations receive discovered items and produce transformed items.
37
+ * They run in sequence between discovery and final emission, enabling
38
+ * composable, extensible processing without double emission.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const transform: ContentTransformation = {
43
+ * id: 'path-rewriting',
44
+ * name: 'Path Reference Rewriting',
45
+ * transform: async (context) => {
46
+ * // Transform items...
47
+ * return { items: transformedItems, warnings: [] };
48
+ * },
49
+ * };
50
+ * ```
51
+ */
52
+ export interface ContentTransformation {
53
+ /** Unique identifier for this transformation */
54
+ id: string;
55
+ /** Human-readable name */
56
+ name: string;
57
+ /**
58
+ * Apply this transformation to the items.
59
+ * @param context - The transformation context with items, plugins, and trial emit
60
+ * @returns Transformed items and any warnings
61
+ */
62
+ transform(context: TransformationContext): Promise<TransformationResult>;
63
+ }
64
+ /**
65
+ * Path rewriting transformation.
66
+ *
67
+ * Rewrites file path references in content during format conversion.
68
+ * Uses a trial emission to discover the source-to-target path mapping,
69
+ * then rewrites all path references in content. Also detects orphan
70
+ * references (paths that weren't converted) using plugin-provided
71
+ * path patterns.
72
+ *
73
+ * This replaces the hardcoded path rewriting logic that was previously
74
+ * embedded in the engine's convert() method, eliminating the need for
75
+ * double emission and removing hardcoded plugin knowledge.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * const transform = new PathRewritingTransformation();
80
+ * engine.convert({
81
+ * source: 'cursor',
82
+ * target: 'claude',
83
+ * root: '/project',
84
+ * transformations: [transform],
85
+ * });
86
+ * ```
87
+ */
88
+ export declare class PathRewritingTransformation implements ContentTransformation {
89
+ readonly id = "path-rewriting";
90
+ readonly name = "Path Reference Rewriting";
91
+ transform(context: TransformationContext): Promise<TransformationResult>;
92
+ }
93
+ //# sourceMappingURL=transformation.d.ts.map