How Pi‑Web Plugin Management Uses pi-coding-agent's SettingsManager and DefaultPackageManager
Pi‑Web's plugin system delegates configuration persistence to SettingsManager and package operations to DefaultPackageManager, both imported from the pi-coding-agent library, creating a clean separation between settings storage and filesystem mutations.
The @earendel-works/pi-coding-agent package provides the foundation for Pi‑Web's extensible architecture. Rather than reimplementing configuration management and package discovery, Pi‑Web orchestrates two battle-tested classes—SettingsManager and DefaultPackageManager—to handle plugin lifecycle operations. This article examines how these classes integrate in app/api/plugins/route.ts to power the GET and POST /api/plugins endpoints.
The Core Classes
Pi‑Web instantiates these classes in tandem for every plugin-related request:
| Class | Responsibility | Creation Location |
|---|---|---|
SettingsManager |
Reads and writes global (~/.pi/agent/settings.json) and project-specific configuration; tracks disabled packages; persists all changes. |
Lines [7‑9] (GET) and [20‑25] (POST) in /app/api/plugins/route.ts |
DefaultPackageManager |
Resolves configured packages, discovers their resources (extensions, skills, prompts, themes), and performs install/remove/update operations. | Lines [10‑14] (GET) and [32‑36] (POST) in /app/api/plugins/route.ts |
The DefaultPackageManager receives the SettingsManager instance as a constructor dependency, ensuring it reads from the same configuration state that the API manipulates.
Reading Plugin State (GET /api/plugins)
The read flow demonstrates how Pi‑Web plugin management uses SettingsManager and DefaultPackageManager together to assemble a complete plugin inventory.
Initialization Sequence
// From app/api/plugins/route.ts, lines 7-14
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
The SettingsManager.create() factory accepts:
cwd: Current working directory for project-scoped operationsagentDir: Pi‑Web's agent directory fromgetAgentDir()projectTrusted: Boolean flag from trust verification
Resolution and Resource Collection
With both managers initialized, the handler calls packageManager.resolve():
const resolved = await packageManager.resolve(/* ... */);
const { countsByPackage, resourcesByPackage, totals } = collectResources(resolved);
The resolve() method walks every package declared in settings, locates its on-disk resources, and returns a ResolvedPaths object. Packages configured but not installed generate warning diagnostics.
Disabled Package Detection
The handler queries disabled status separately via getDisabledPackages, which inspects both global and project settings:
// Lines 55-63: checks if all resource arrays are empty
const globalDisabled = settingsManager.getGlobalSettings().packages
.filter(p => p.extensions.length === 0 && p.skills.length === 0 /* ... */);
A package is considered disabled when its extensions, skills, prompts, and themes arrays are all empty—achieved by replacing resources with empty arrays rather than removing the entry entirely.
Modifying Plugin State (POST /api/plugins)
The write path reuses the same initialization pattern, then branches based on action type.
Trust Validation
Before any mutation, Pi‑Web enforces security boundaries:
// Lines 26-31
if (scope === 'project' && !projectTrusted) {
return new Response('Untrusted', { status: 403 });
}
Project-scope changes require explicit trust; global changes proceed regardless.
Action Routing
| Action | Handler Method | Configuration Impact |
|---|---|---|
install |
packageManager.installAndPersist(source, { local: true }) |
Adds package to settings, writes files to disk |
remove |
packageManager.removeAndPersist(source) |
Removes from settings, deletes files |
update |
packageManager.update(source) |
Updates on-disk package, settings unchanged |
disable / enable |
setPackageDisabled(settingsManager, source, scope, boolean) followed by settingsManager.flush() |
Mutates settings only; no filesystem changes |
Disable/Enable Implementation
The setPackageDisabled helper (lines 66-88) demonstrates direct SettingsManager manipulation:
// Disable by replacing resources with empty arrays
const entry = scope === 'global'
? settingsManager.getGlobalSettings().packages.find(/* ... */)
: settingsManager.getProjectSettings().packages.find(/* ... */);
entry.extensions = [];
entry.skills = [];
entry.prompts = [];
entry.themes = [];
// Persist changes
await settingsManager.flush(); // Lines 50-55
Notice that DefaultPackageManager is not involved in disable/enable operations—only SettingsManager methods are used. This architectural choice keeps filesystem and configuration concerns separate.
Key Design Patterns
Single Source of Truth
All configuration lives in SettingsManager. The DefaultPackageManager reads settings but never writes them directly. When installing or removing packages, installAndPersist and removeAndPersist internally coordinate with the provided SettingsManager, ensuring atomic updates to both configuration and disk state.
Scope Awareness
Both classes accept scope parameters ('global' | 'project'), enabling Pi‑Web to:
- Apply global plugins across all projects
- Isolate project-specific extensions
- Respect trust boundaries per-scope
Separation of Concerns
| Layer | Responsibility |
|---|---|
API route (route.ts) |
HTTP handling, validation, response formatting |
SettingsManager |
JSON schema, file I/O, merge logic for global + project settings |
DefaultPackageManager |
npm/git operations, resource discovery, path resolution |
This layering prevents the API from concerning itself with npm internals or JSON merging algorithms.
Practical Examples
Fetching Current Plugins
GET /api/plugins?cwd=/home/user/my-project
Handler execution:
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
const resolved = await packageManager.resolve(/* ... */);
Response includes per-package status: loaded, installed, missing, or disabled.
Installing a Package
POST /api/plugins
Content-Type: application/json
{
"action": "install",
"source": "npm:my-pi-plugin",
"scope": "project",
"cwd": "/home/user/my-project"
}
await packageManager.installAndPersist(source, { local: true });
// Both npm install AND settings update occur atomically
Disabling Without Removal
POST /api/plugins
Content-Type: application/json
{
"action": "disable",
"source": "npm:my-pi-plugin",
"scope": "global",
"cwd": "/home/user/my-project"
}
setPackageDisabled(settingsManager, source, "global", true);
await settingsManager.flush();
// Package remains installed but inactive; re-enable by restoring arrays
Summary
-
SettingsManagerprovides the configuration backbone: JSON persistence, global/project merge logic, and disabled-package tracking. Created fresh per-request inroute.tslines 7-9 and 20-25. -
DefaultPackageManagerhandles package discovery and filesystem operations, receivingSettingsManageras a constructor dependency to ensure configuration consistency. Instantiated immediately afterSettingsManagerin both GET and POST flows. -
Separation of concerns keeps the API layer thin: configuration mutations go through
SettingsManagermethods; package operations go throughDefaultPackageManagermethods. -
Trust enforcement blocks project-scope modifications for untrusted projects before either manager performs work.
-
Disable/enable is a pure settings operation, requiring only
settingsManager.flush()after mutation—no package manager involvement.
Frequently Asked Questions
How does Pi‑Web distinguish between global and project plugins?
The SettingsManager maintains separate configuration objects for each scope. When created, it loads ~/.pi/agent/settings.json (global) and {cwd}/.pi/settings.json (project), merging them for reads while keeping mutations scope-specific. The DefaultPackageManager uses this merged view to resolve resources from both locations.
Why disable a plugin instead of removing it?
Disabling preserves the package entry with empty resource arrays, allowing quick re-enabling without re-downloading. This design supports workflows where developers temporarily deactivate features. The getDisabledPackages helper detects disabled status by checking for all-empty resource arrays in either global or project settings.
What happens if installAndPersist fails?
The DefaultPackageManager implements atomic semantics: if the npm/git installation fails, no settings are written. Conversely, if settings persistence fails, the installed package can be cleaned up. This prevents the "installed but unconfigured" or "configured but missing" drift states that plague simpler plugin systems.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →