# How Pi‑Web Plugin Management Uses pi-coding-agent's SettingsManager and DefaultPackageManager

> Discover how Pi-Web plugin management leverages pi-coding-agent's SettingsManager and DefaultPackageManager for efficient configuration and package operations. Learn about clean separation.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-08-16

---

**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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main//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`](https://github.com/agegr/pi-web/blob/main//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

```typescript
// 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 operations
- `agentDir`: Pi‑Web's agent directory from `getAgentDir()`
- `projectTrusted`: Boolean flag from trust verification

### Resolution and Resource Collection

With both managers initialized, the handler calls `packageManager.resolve()`:

```typescript
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:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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`](https://github.com/agegr/pi-web/blob/main/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

```http
GET /api/plugins?cwd=/home/user/my-project

```

Handler execution:

```typescript
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

```http
POST /api/plugins
Content-Type: application/json

{
  "action": "install",
  "source": "npm:my-pi-plugin",
  "scope": "project",
  "cwd": "/home/user/my-project"
}

```

```typescript
await packageManager.installAndPersist(source, { local: true });
// Both npm install AND settings update occur atomically

```

### Disabling Without Removal

```http
POST /api/plugins
Content-Type: application/json

{
  "action": "disable",
  "source": "npm:my-pi-plugin",
  "scope": "global",
  "cwd": "/home/user/my-project"
}

```

```typescript
setPackageDisabled(settingsManager, source, "global", true);
await settingsManager.flush();
// Package remains installed but inactive; re-enable by restoring arrays

```

## Summary

- **`SettingsManager`** provides the configuration backbone: JSON persistence, global/project merge logic, and disabled-package tracking. Created fresh per-request in [`route.ts`](https://github.com/agegr/pi-web/blob/main/route.ts) lines 7-9 and 20-25.

- **`DefaultPackageManager`** handles package discovery and filesystem operations, receiving `SettingsManager` as a constructor dependency to ensure configuration consistency. Instantiated immediately after `SettingsManager` in both GET and POST flows.

- **Separation of concerns** keeps the API layer thin: configuration mutations go through `SettingsManager` methods; package operations go through `DefaultPackageManager` methods.

- **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.