src — extensions
src — extensions
The src/extensions/extension-loader.ts module is responsible for discovering, loading, and managing the lifecycle of extensions within the application. It provides a robust mechanism for dynamically integrating new functionalities, such as channels, tools, providers, and integrations, based on a defined manifest and lifecycle hooks.
Purpose and Core Responsibilities
The primary goal of the ExtensionLoader is to enable a pluggable architecture. It handles:
- Discovery: Locating extension directories within predefined search paths.
- Parsing: Reading and validating
extension.jsonmanifest files. - Loading: Instantiating extensions and making them available.
- Lifecycle Management: Orchestrating activation, deactivation, and disposal of extensions, including dependency resolution and configuration validation.
- State Tracking: Maintaining the current status of each loaded extension.
This module acts as the central registry and orchestrator for all extensions, ensuring they adhere to a common structure and can be managed programmatically.
Core Concepts
Extension Manifest (ExtensionManifest)
The ExtensionManifest interface defines the metadata required for every extension. This data is read from an extension.json file located in the extension's root directory.
export interface ExtensionManifest {
name: string; class="hl-cmt">// Unique identifier for the extension
version: string; class="hl-cmt">// Semantic versioning
description: string; class="hl-cmt">// Brief description
author?: string; class="hl-cmt">// Optional author information
type: 39;channel39; | 39;tool39; | 39;provider39; | 39;integration39;; class="hl-cmt">// Categorization
entryPoint: string; class="hl-cmt">// Path to the main module file (relative to extension root)
configSchema?: Record<string, { type: string; required?: boolean; default?: unknown; description?: string }>; class="hl-cmt">// Schema for configuration
dependencies?: string[]; class="hl-cmt">// List of other extension names this extension depends on
}
Key fields like name, version, type, and entryPoint are mandatory. The configSchema allows extensions to declare their expected configuration parameters, enabling automatic validation.
Extension Instance (ExtensionInstance)
An ExtensionInstance represents a loaded extension at runtime. It encapsulates the manifest, its file system path, and its current operational status.
export interface ExtensionInstance {
manifest: ExtensionManifest;
path: string;
status: 39;loaded39; | 39;active39; | 39;error39; | 39;disabled39;; class="hl-cmt">// Current state
error?: string; class="hl-cmt">// Details if status is 39;error39;
loadedAt?: number; class="hl-cmt">// Timestamp of loading
}
Extension Lifecycle Hooks (ExtensionLifecycle)
Extensions can define optional lifecycle methods that the ExtensionLoader will call at specific points. These methods allow extensions to perform setup, teardown, and configuration tasks.
export interface ExtensionLifecycle {
onLoad?(): Promise<void>; class="hl-cmt">// Called immediately after the extension module is loaded (not yet implemented for actual module loading)
onActivate?(config: Record<string, unknown>): Promise<void>; class="hl-cmt">// Called when the extension is activated, receiving its configuration
onDeactivate?(): Promise<void>; class="hl-cmt">// Called when the extension is deactivated
onDispose?(): Promise<void>; class="hl-cmt">// Called when the extension loader is shutting down or the extension is being removed
}
Note: While onLoad is defined in the interface, the current ExtensionLoader implementation primarily focuses on onActivate, onDeactivate, and onDispose for runtime interaction. The actual module loading and instantiation of the ExtensionLifecycle object is an implicit future step not fully detailed in the provided code.
Extension Search Paths
The ExtensionLoader looks for extensions in a predefined set of directories. By default, these include:
.codebuddy/extensionsin the current working directory..codebuddy/extensionsin the user's home directory.
These paths can be customized during the ExtensionLoader's instantiation.
The ExtensionLoader Class
The ExtensionLoader class is the central component of this module. It extends EventEmitter to broadcast significant events during the extension lifecycle.
Constructor
constructor(searchPaths?: string[])
Initializes the loader with an optional array of searchPaths. If not provided, it defaults to the standard paths.
Static Method: parseManifest
static parseManifest(dir: string): ExtensionManifest | null
This static helper method is responsible for reading and validating the extension.json file within a given directory. It performs checks for:
- File existence.
- Valid JSON format.
- Presence of all
REQUIRED_MANIFEST_FIELDS(name,version,type,entryPoint). - Valid
typeagainstVALID_TYPES.
It returns the parsed ExtensionManifest or null if any validation fails.
Instance Methods
Discovery
discover(): ExtensionManifest[]
Iterates through all configured searchPaths, finds subdirectories, and attempts to parse an extension.json manifest in each. It returns a list of all valid ExtensionManifest objects found.
Loading
load(name: string): ExtensionInstance | { error: string }
Attempts to load a specific extension by its name. It searches through the configured paths for a directory matching the name, parses its manifest, and creates an ExtensionInstance. It performs basic validation on the name to prevent path traversal issues (e.g., name.includes('..')).
Upon successful loading, it stores the ExtensionInstance internally and emits a loaded event.
loadAll(): ExtensionInstance[]
Combines discover() and load(). It first discovers all available manifests and then attempts to load each one. It returns an array of all successfully loaded ExtensionInstance objects.
Management
get(name: string): ExtensionInstance | undefined
Retrieves a specific ExtensionInstance by its name if it has been loaded.
list(type?: ExtensionManifest['type']): ExtensionInstance[]
Returns an array of all currently loaded ExtensionInstance objects. Optionally, it can filter by type (e.g., 'channel', 'tool').
Lifecycle Management
async activate(name: string, config?: Record): Promise
Activates a loaded extension. Before calling the extension's onActivate hook, it performs:
- Configuration Validation: Uses
validateConfig()against theconfigSchemadefined in the manifest. - Dependency Checking: Uses
checkDependencies()to ensure all declared dependencies are loaded.
If validation or dependency checks fail, or if the onActivate hook throws an error, the extension's status is set to 'error'. On success, the status becomes 'active', and an activated event is emitted.
async deactivate(name: string): Promise
Deactivates an active extension. It calls the extension's onDeactivate hook. On success, the status becomes 'disabled', and a deactivated event is emitted. If the hook throws an error, the status is set to 'error'.
async dispose(): Promise
Performs a best-effort cleanup for all loaded extensions. It iterates through all extensions and calls their onDispose hook. Finally, it clears all internal maps and emits a disposed event.
Validation & Dependencies
validateConfig(name: string, config: Record): { valid: boolean; errors: string[] }
Validates a given config object against the configSchema defined in the extension's manifest. It checks for required fields and type correctness.
checkDependencies(name: string): { satisfied: boolean; missing: string[] }
Checks if all extensions listed in the dependencies array of the specified extension's manifest are currently loaded by the ExtensionLoader.
Events
The ExtensionLoader extends EventEmitter and emits the following events:
loaded: Emitted when an extension is successfully loaded.activated: Emitted when an extension is successfully activated.deactivated: Emitted when an extension is successfully deactivated.disposed: Emitted when theExtensionLoaderis disposed.
Extension Lifecycle Workflow
The following diagram illustrates the typical state transitions for an ExtensionInstance managed by the ExtensionLoader.
stateDiagram
direction LR
[*] --> Discovered: parseManifest()
Discovered --> Loaded: load()
Loaded --> Active: activate()
Active --> Disabled: deactivate()
Disabled --> Active: activate()
Loaded --> Error: validation/dependency/activation error
Active --> Error: deactivation error
Disabled --> Error: activation error
Error --> Disabled: deactivate() (attempt)
Error --> Active: activate() (attempt)
Loaded --> [*]: dispose()
Active --> [*]: dispose()
Disabled --> [*]: dispose()
Error --> [*]: dispose()
Error Handling and Validation
The ExtensionLoader incorporates several layers of validation and error handling:
- Manifest Parsing:
parseManifestrigorously checks for file existence, JSON validity, and required fields. - Extension Name Validation: The
loadmethod uses a regular expression (/^[a-zA-Z0-9_@][a-zA-Z0-9_\-./]*$/.test(name)) to validate extension names, preventing malicious path traversal attempts. - Configuration Schema Validation:
validateConfigensures that provided runtime configuration adheres to the schema defined in theextension.json. - Dependency Resolution:
checkDependenciesverifies that all declared extension dependencies are loaded before activation. - Lifecycle Hook Error Handling:
activate,deactivate, anddisposemethods wrap calls to extension lifecycle hooks intry...catchblocks, setting the extension status to'error'if an exception occurs.
Integration and Usage
A typical usage pattern for the ExtensionLoader would involve:
- Instantiation: Create an instance of
ExtensionLoader. - Loading: Call
loadAll()to discover and load all available extensions. - Activation: Iterate through loaded extensions and call
activate()for those that should be active, potentially providing configuration. - Management: Use
get()orlist()to retrieve specific extensions or groups of extensions. - Deactivation/Disposal: Call
deactivate()for individual extensions ordispose()for a full shutdown.
import { ExtensionLoader } from 39;./extension-loader39;;
async function initializeExtensions() {
const loader = new ExtensionLoader();
class="hl-cmt">// Discover and load all extensions
const loadedInstances = loader.loadAll();
console.log(`Loaded ${loadedInstances.length} extensions.`);
class="hl-cmt">// Activate a specific extension with configuration
const myToolConfig = { apiKey: 39;abc-12339;, endpoint: 39;https:class="hl-cmt">//api.example.com39; };
const activated = await loader.activate(39;my-tool-extension39;, myToolConfig);
if (activated) {
console.log(39;my-tool-extension activated successfully.39;);
} else {
const instance = loader.get(39;my-tool-extension39;);
console.error(`Failed to activate my-tool-extension: ${instance?.error}`);
}
class="hl-cmt">// List all active 39;channel39; type extensions
const activeChannels = loader.list(39;channel39;).filter(ext => ext.status === 39;active39;);
console.log(`Active channels: ${activeChannels.map(c => c.manifest.name).join(39;, 39;)}`);
class="hl-cmt">// ... later, when shutting down or removing an extension
await loader.deactivate(39;my-tool-extension39;);
console.log(39;my-tool-extension deactivated.39;);
await loader.dispose();
console.log(39;Extension loader disposed.39;);
}
initializeExtensions();
The ExtensionLoader is designed to be a standalone component that other parts of the application can depend on to manage their extension ecosystem. Its primary interaction points are through its public methods and emitted events.