Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions docs/ai/design/2026-06-11-feature-plugin-foundation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
phase: design
title: System Design & Architecture
description: Define the technical architecture, components, and data models
---

# System Design & Architecture

## Architecture Overview
**What is the high-level system structure?**

```mermaid
flowchart TD
User[User] -->|ai-devkit plugin add pkg| PluginCmd[plugin command]
PluginCmd --> GlobalNpm[~/.ai-devkit/npm]
PluginCmd --> GlobalConfig[~/.ai-devkit/.ai-devkit.json]
GlobalNpm --> NodeModules[~/.ai-devkit/npm/node_modules]

User -->|ai-devkit hello-devkit| Cli[AI DevKit CLI]
Cli --> Builtins[Register built-in commands]
Cli --> PluginLoader[Global plugin loader]
PluginLoader --> GlobalConfig
PluginLoader --> Resolver[Resolve package from global npm root]
Resolver --> PackageJson[Plugin package.json]
PackageJson --> Manifest[aiDevkit.commands manifest]
Manifest --> Entrypoint[Dynamic import command entrypoint]
Entrypoint --> Commander[Register Commander command]
Commander --> Runtime[AI DevKit runtime]
Commander --> PluginAction[Plugin action]
```

AI DevKit remains the host CLI. Plugins are global npm packages installed into an AI DevKit-managed npm project at `~/.ai-devkit/npm`. The global AI DevKit config lists enabled plugins. At startup, the CLI registers built-ins, discovers globally configured plugins, validates each manifest, and lets each plugin register contributed commands with Commander.

### Key Components
| Component | Responsibility |
|---|---|
| `plugin` CLI command | Add, remove, and list global npm plugins. |
| Global config manager | Read/write `~/.ai-devkit/.ai-devkit.json` and its `plugins` array. |
| Plugin npm manager | Ensure `~/.ai-devkit/npm`, run npm install/remove, and report package status. |
| Plugin manifest loader | Resolve package manifests from `~/.ai-devkit/npm/node_modules` and validate `aiDevkit.commands`. |
| Plugin command loader | Import plugin entrypoints and register contributed Commander commands. |
| AI DevKit runtime | Stable host context passed to plugin registration. |

## Data Models
**What data do we need to manage?**

### Global Config
```typescript
interface GlobalAiDevkitConfig {
version?: string;
plugins?: string[];
memory?: {
path?: string;
};
[key: string]: unknown;
}
```

### Plugin Manifest
The MVP keeps the manifest in `package.json` so plugin authors do not need an extra file. Command entries must point to JavaScript files importable by Node at runtime. Plugin authors may build those files from TypeScript, but the host does not execute `.ts` entrypoints in the MVP.

```json
{
"name": "@example/hello-ai-devkit",
"type": "module",
"aiDevkit": {
"commands": [
{
"name": "hello-devkit",
"description": "Print a plugin test message",
"entry": "./dist/command.js"
}
]
}
}
```

```typescript
interface AiDevkitPluginManifest {
commands: AiDevkitPluginCommand[];
}

interface AiDevkitPluginCommand {
name: string;
description?: string;
entry: string;
}
```

### Runtime Contract
```typescript
interface AiDevkitRuntime {
cwd: string;
homeDir: string;
configPath: string;
getConfig(): Promise<GlobalAiDevkitConfig>;
getMemoryDbPath(): Promise<string>;
logger: {
info(message: string): void;
warn(message: string): void;
error(message: string): void;
};
}
```

## API Design
**How do components communicate?**

### CLI Surface
```bash
ai-devkit plugin add <npm-package>
ai-devkit plugin remove <npm-package>
ai-devkit plugin list
ai-devkit <plugin-command> [...args]
```

### Plugin Entrypoint
Each command entrypoint exports `register`.

```typescript
import type { Command } from 'commander';

export async function register(command: Command, runtime: AiDevkitRuntime): Promise<void> | void {
command
.description('Print a plugin test message')
.option('--name <name>', 'Name to greet')
.action(async options => {
runtime.logger.info(`Hello ${options.name ?? 'AI DevKit'}`);
});
}
```

AI DevKit creates the top-level command from the manifest, then passes that `Command` instance to the plugin entrypoint. This keeps command naming and conflict handling in the host while letting plugins define options and actions.

## Component Breakdown
**What are the major building blocks?**

### `packages/cli/src/commands/plugin.ts`
- Registers `plugin add`, `plugin remove`, and `plugin list`.
- Delegates package work to a service layer.
- Renders install/remove/list results through existing terminal UI helpers.

### `packages/cli/src/services/plugin/plugin-package.service.ts`
- Ensures `~/.ai-devkit/npm/package.json`.
- Runs npm commands with argument arrays, not shell interpolation.
- Supports install and uninstall for npm package names only.

### `packages/cli/src/lib/GlobalConfig.ts`
- Reads/writes `~/.ai-devkit/.ai-devkit.json`.
- Adds/removes plugin names with deduplication.
- Preserves unrelated global config fields.
- Reuses the existing global config manager instead of creating a plugin-only config service.

### `packages/cli/src/services/plugin/plugin-loader.service.ts`
- Resolves plugin package manifests by reading `~/.ai-devkit/npm/node_modules/<package>/package.json` directly.
- Validates manifest shape and command names.
- Detects command conflicts before registration.
- Imports entrypoints with `pathToFileURL()`.

### `packages/cli/src/services/plugin/runtime.ts`
- Builds the `AiDevkitRuntime` passed to plugins.
- Centralizes config and memory path access.
- Keeps future host APIs behind a stable surface.

## Design Decisions
**Why did we choose this approach?**

| Decision | Choice | Rationale |
|---|---|---|
| Plugin source | npm only | Keeps install model familiar and avoids designing multiple source protocols. |
| Scope | Global only | Simplifies MVP and matches the user request. |
| Install root | `~/.ai-devkit/npm` | Stable across `npx`, global, and local AI DevKit invocations. |
| Config | `~/.ai-devkit/.ai-devkit.json` `plugins` | Keeps enabled plugin state in AI DevKit's global config. |
| Manifest location | `package.json` `aiDevkit.commands` | Easy for npm packages and avoids a second file lookup. |
| Manifest read path | Direct file read under `~/.ai-devkit/npm/node_modules` | Avoids Node package `exports` blocking access to `package.json`. |
| Command API | `register(command, runtime)` | Lets plugins use Commander options/help while host owns top-level names. |
| Runtime API | Small host-provided object | Prevents plugins from importing unstable AI DevKit internals. |
| Entrypoint file type | JavaScript only | Avoids runtime TypeScript loaders, keeps plugin execution predictable, and lets authors use normal build output. |

### Alternatives Considered
- **Install beside AI DevKit binary**: rejected because `npx`, global installs, local installs, and permissions make the binary location unstable.
- **Project-local plugins first**: deferred because it adds config precedence and project trust questions.
- **Run plugins through `npx` every time**: rejected for MVP because it makes every plugin command slower and network-dependent.
- **Hard-code optional tools into core CLI**: rejected because optional dependencies and release cadence should stay separate from core.
- **Execute TypeScript entrypoints directly**: deferred because it requires a runtime loader such as `tsx` or `ts-node` and complicates production execution.

## Non-Functional Requirements
**How should the system perform?**

### Performance
- Plugin discovery only scans configured plugins.
- Missing or invalid plugins should not prevent built-in commands from running.
- Plugin entrypoints are imported during command registration in MVP; if startup becomes too slow, later work can defer entrypoint import until command action execution.

### Security
- Installing a plugin is trusted npm package execution.
- Package names must be passed to npm as arguments, never interpolated into shell commands.
- Manifest `entry` must resolve inside the plugin package root.
- Manifest `entry` must resolve to a JavaScript file.
- Built-in command names cannot be overridden by plugins.

### Reliability
- `plugin add` validates the manifest after install and rolls back config updates if validation fails.
- `plugin add` also uninstalls the package when manifest validation fails after npm install succeeds, so invalid plugins are not left enabled or installed by default.
- `plugin remove` should remove config entries even if the package is already absent, with a warning.
- Loader errors should be clear and isolated to the affected plugin where possible.
124 changes: 124 additions & 0 deletions docs/ai/implementation/2026-06-11-feature-plugin-foundation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
phase: implementation
title: Implementation Guide
description: Technical implementation notes, patterns, and code guidelines
---

# Implementation Guide

## Development Setup
**How do we get started?**

- Work in the feature worktree `feature-plugin-foundation`.
- Use npm as the workspace package manager.
- Keep implementation in `packages/cli` for the host plugin system.
- Use temporary home directories in tests so global plugin state does not touch the developer's real `~/.ai-devkit`.

## Code Structure
**How is the code organized?**

Expected new/changed areas:

```text
packages/cli/src/commands/plugin.ts
packages/cli/src/services/plugin/
plugin-package.service.ts
plugin-manifest.service.ts
plugin-loader.service.ts
plugin-manager.service.ts
runtime.ts
packages/cli/src/lib/GlobalConfig.ts
packages/cli/src/cli.ts
packages/cli/src/types.ts
packages/cli/src/__tests__/commands/plugin.test.ts
packages/cli/src/__tests__/services/plugin/
```

Implemented so far:
- `packages/cli/src/types.ts`: added `plugins?: string[]` to `GlobalDevKitConfig`.
- `packages/cli/src/lib/GlobalConfig.ts`: added `getPlugins()`, `addPlugin()`, and `removePlugin()` for global plugin config entries.
- `packages/cli/src/__tests__/lib/GlobalConfig.test.ts`: added plugin config tests for read, add, dedupe, and remove behavior.
- `packages/cli/src/services/plugin/plugin-package.service.ts`: added global npm root resolution, npm project initialization, install/uninstall execution with argument arrays, and npm-package-only validation.
- `packages/cli/src/__tests__/services/plugin/plugin-package.service.test.ts`: added package service tests for npm root, package.json initialization, install/uninstall args, and invalid package names.
- `packages/cli/src/services/plugin/plugin-manifest.service.ts`: added manifest shape validation, duplicate-command rejection, built-in conflict rejection, JavaScript-only entrypoint checks, and package-root containment checks.
- `packages/cli/src/__tests__/services/plugin/plugin-manifest.service.test.ts`: added manifest validation tests for valid manifests and invalid manifest/error paths.
- `packages/cli/src/services/plugin/plugin-loader.service.ts`: added installed plugin manifest validation using package resolution from `~/.ai-devkit/npm`.
- `packages/cli/src/services/plugin/plugin-manager.service.ts`: added add/remove/list orchestration, including uninstall rollback on manifest validation failure.
- `packages/cli/src/__tests__/services/plugin/plugin-manager.service.test.ts`: added service tests for add ordering, rollback, remove, and list status.
- `packages/cli/src/services/plugin/plugin-loader.service.ts`: expanded to register configured plugin commands with Commander and dynamically import command entrypoints.
- `packages/cli/src/services/plugin/runtime.ts`: added the initial `AiDevkitRuntime` with cwd, homeDir, configPath, config access, memory path resolution, and logger methods.
- `packages/cli/src/__tests__/services/plugin/plugin-loader.service.test.ts`: added plugin command registration tests and invalid plugin warning coverage.
- `packages/cli/src/__tests__/services/plugin/runtime.test.ts`: added runtime config and memory path tests.
- `web/content/docs/14-plugins.md`: added user and plugin-author documentation for global npm plugins.
- `web/content/docs/11-configuration-file.md`: documented the global `plugins` config field and plugin commands.
- `packages/cli/src/commands/plugin.ts`: added `plugin add`, `plugin remove`, and `plugin list` commands.
- `packages/cli/src/cli.ts`: registered the new `plugin` command group.
- `packages/cli/src/__tests__/commands/plugin.test.ts`: added command-level tests for add/remove/list behavior.
- `packages/cli/src/services/plugin/plugin-loader.service.ts`: changed package manifest lookup to read `~/.ai-devkit/npm/node_modules/<package>/package.json` directly so package `exports` cannot block manifest access.
- `packages/cli/src/services/plugin/plugin-package.service.ts`: tightened plugin package validation to reject version specs and unsafe path-shaped names before npm install or manifest lookup.
- `packages/cli/src/services/plugin/plugin-loader.service.ts`: added installed entrypoint existence validation so `plugin add` does not persist packages whose manifest points at missing JavaScript files.
- `packages/cli/src/services/plugin/plugin-loader.service.ts`: added a startup guard that skips plugin commands conflicting with already registered Commander commands.
- `packages/cli/src/services/plugin/plugin-manifest.service.ts`: restricted plugin command names to lowercase letters, numbers, and hyphens so manifest names cannot inject Commander argument/option grammar.

## Implementation Notes
**Key technical details to remember:**

### Core Features
- `plugin add`: ensure global npm root, run npm install, validate manifest, then update global config. If validation fails after install, run npm uninstall before returning the validation error.
- `plugin remove`: run npm uninstall and remove the plugin from global config. If one side is already absent, warn but complete the other cleanup.
- `plugin list`: show configured package name, installed status, and manifest status.
- Plugin loading: read global config, resolve package manifests from `~/.ai-devkit/npm/node_modules`, create Commander commands from manifest entries, import entrypoints, and call `register(command, runtime)`.

### Patterns & Best Practices
- Use `execFile`/spawn-style APIs with argument arrays for npm.
- Read plugin `package.json` directly from `~/.ai-devkit/npm/node_modules/<package>/package.json`; do not rely on Node package `exports` for manifest access.
- Use `pathToFileURL()` for dynamic ESM imports.
- Accept JavaScript plugin entrypoints only; TypeScript source must be built before plugin execution.
- Keep plugin runtime APIs stable and intentionally small.
- Preserve existing built-in CLI behavior when plugin loading fails.

## Integration Points
**How do pieces connect?**

- `packages/cli/src/cli.ts` registers built-ins, then invokes the plugin loader.
- Plugin loader uses the global config service and manifest service.
- Plugin entrypoints receive Commander command instances and the AI DevKit runtime.
- Plugins can use `runtime.getMemoryDbPath()` when they need to locate the configured memory database.

## Error Handling
**How do we handle failures?**

- Missing global config should mean no plugins configured, except inside explicit `plugin add/remove/list` flows where the service can create config as needed.
- Invalid configured plugins should produce targeted warnings during startup and not break built-in commands.
- `plugin add` should fail if the installed package lacks a valid manifest.
- If `plugin add` fails manifest validation after install, attempt rollback uninstall and include rollback failure details if uninstall also fails.
- Entry paths outside the plugin root are invalid.
- Non-JavaScript entrypoint paths are invalid in the MVP.
- Built-in command conflicts are invalid.

## Performance Considerations
**How do we keep it fast?**

- Only inspect plugins listed in global config.
- Avoid filesystem recursion; resolve direct package manifests only.
- Measure startup before deciding whether to switch from eager `register()` imports to lazy action imports.

## Security Notes
**What security measures are in place?**

- Installing and running plugins executes trusted npm package code.
- Validate package names and never interpolate them into shell commands.
- Keep manifest entrypoint resolution inside the package root.
- Do not expose secrets through the runtime.
- Do not let plugins override built-in commands.

## Phase 6 Implementation Check

- Alignment: implementation matches the approved global npm-only plugin host design.
- Intentional deviation: reused the existing `GlobalConfigManager` for plugin config instead of adding a separate `plugin-config.service.ts`; this keeps all global config behavior in one place and preserves unrelated fields.
- Correctness fix found during implementation check: plugin manifests are now read directly from the managed npm install root instead of using `require.resolve("<plugin>/package.json")`, because package `exports` can block `package.json` resolution.
- Code review fix: package names are now constrained to bare npm names (`name` or `@scope/name`) so version specs and path-shaped values cannot be persisted or used to build manifest paths.
- Phase 8 review fix: installed plugin commands now validate that declared entrypoint files exist before the plugin is considered valid.
- Phase 8 review fix: plugin command registration now tracks already registered command names and skips later conflicts with a warning.
- Final security review fix: plugin command names now reject spaces, argument placeholders, options, uppercase names, and other Commander grammar before registration.
- Remaining follow-ups: manual smoke with a real or packed plugin package, startup timing measurement with configured plugins, and later concrete first-party plugin package implementations.
Loading
Loading