# Building a Plugin

Plugins are self-contained modules that extend Officer with new features. They live under `src/workspaces/plugins/` and are auto-discovered at startup — no registration step required.

## Directory Structure

```
src/workspaces/plugins/
├── package.json            # Workspace exports (update when adding a plugin)
└── MyPlugin/
    ├── index.ts            # Metadata + re-exports
    ├── server/
    │   ├── index.ts        # Exports router and apiPath
    │   └── router.ts       # Hono API routes
    └── client/
        ├── index.ts        # Exports Widget and/or Screen
        ├── Widget.tsx       # Compact component (dashboard home)
        ├── Screen/
        │   └── index.tsx    # Full-page component (dedicated route)
        └── state/
            └── useMyPlugin.ts  # API client hook
```

A plugin can be server-only, client-only, or both. The discovery system checks for the presence of `server/index.ts` and `client/index.ts` to determine what the plugin provides.

## Step-by-Step

### 1. Create the plugin directory

```
mkdir -p src/workspaces/plugins/MyPlugin/{server,client/state,client/Screen}
```

### 2. Plugin metadata — `MyPlugin/index.ts`

Every plugin must export a `plugin` object with metadata. This is read by the settings UI and the `/plugins` API.

```ts
export { Widget, Screen } from './client';
export { router, apiPath } from './server';

export const plugin = {
  id: 'MyPlugin',       // Must match directory name
  name: 'My Plugin',    // Display name in settings
  description: 'What this plugin does',
};
```

If your plugin is server-only, omit the client export. If client-only, omit the server export.

### 3. Server router — `MyPlugin/server/`

**`server/index.ts`** — Exports the router instance and the API path prefix:

```ts
export * from './router';

export const apiPath = 'my-plugin';
```

The `apiPath` determines the URL prefix. This router gets mounted at `/api/my-plugin`.

**`server/router.ts`** — Define your API endpoints:

```ts
import { createRouter } from '@@/create-router';
import * as errors from '@@/custom-errors';

export const router = createRouter();

router.get('/items', async (ctx) => {
  const user = ctx.get('user');  // Authenticated user
  // ...
  return ctx.json({ items: [] });
});

router.post('/items', async (ctx) => {
  const body = ctx.get('body');  // Parsed request body
  if (!body.name) throw errors.BAD_REQUEST('Name is required');
  // ...
  return ctx.json({ created: true });
});
```

Key points:
- `createRouter()` from `@@/create-router` gives you a typed Hono router
- All plugin routes are **protected** — user authentication is enforced automatically
- Access the authenticated user with `ctx.get('user')` (returns `User` from types)
- Access parsed body with `ctx.get('body')`
- Throw `CustomError` instances for error responses — they're caught by the global error handler

**Available error helpers** (`@@/custom-errors`):
- `BAD_REQUEST(msg?)` — 400
- `UNAUTHORIZED(msg?)` — 401
- `FORBIDDEN(msg?)` — 403
- `NOT_FOUND(msg?)` — 404
- `CONFLICT(msg?)` — 409
- `INTERNAL_SERVER_ERROR(msg?)` — 500
- `TOO_MANY_REQUESTS(msg?, retryAfter?)` — 429

**User data helpers** (`@@/data-path`):
- `DATA_PATH` — base data directory
- `getHomeDir(email)` — user's home directory

### 4. Client components — `MyPlugin/client/`

**`client/index.ts`** — Export your components with standardized names:

```ts
export { MyPluginWidget as Widget } from './Widget';
export { MyPluginScreen as Screen } from './Screen';
```

**`client/state/useMyPlugin.ts`** — API client hook:

```ts
import { useClient } from 'hooks/useClient';

type Item = {
  id: string;
  name: string;
};

export const useMyPlugin = () => {
  const client = useClient();

  return {
    listItems: () => client.get<Item[]>('/my-plugin/items'),
    createItem: (name: string) => client.post('/my-plugin/items', { name }),
  };
};
```

The `useClient()` hook provides an authenticated HTTP client. The path must match your `apiPath` from the server.

**`client/Widget.tsx`** — Compact component for the dashboard home:

```tsx
import { useState, useEffect } from 'react';
import { useMyPlugin } from './state/useMyPlugin';

export const MyPluginWidget = () => {
  // Widget implementation
};
```

**`client/Screen/index.tsx`** — Full-page component:

```tsx
import { DashboardLayout } from '@/Screens/Dashboard/Layout';

export const MyPluginScreen = () => {
  return (
    <DashboardLayout>
      {/* Screen implementation */}
    </DashboardLayout>
  );
};
```

### 5. Register workspace exports — `plugins/package.json`

Add subpath exports so the monorepo can import your plugin:

```json
{
  "exports": {
    "./FileBrowser": "./FileBrowser/index.ts",
    "./FileBrowser/client": "./FileBrowser/client/index.ts",
    "./FileBrowser/server": "./FileBrowser/server/index.ts",
    "./MyPlugin": "./MyPlugin/index.ts",
    "./MyPlugin/client": "./MyPlugin/client/index.ts",
    "./MyPlugin/server": "./MyPlugin/server/index.ts"
  }
}
```

This lets other code import your plugin as:

```ts
import { Widget, Screen } from 'plugins/MyPlugin/client';
import { router, apiPath } from 'plugins/MyPlugin/server';
import { plugin } from 'plugins/MyPlugin';
```

### 6. Wire into the app (optional)

The server router is auto-discovered and mounted — no changes needed. But if your plugin has client components that should appear in the main app, you'll need to add them manually:

**Route** — `src/apps/officer-web/App.tsx`:

```tsx
import { Screen as MyPluginScreen } from 'plugins/MyPlugin/client';

// Inside the authenticated routes:
{plugins?.MyPlugin !== false && <Route path="/my-plugin" element={<MyPluginScreen />} />}
```

**Nav item** — `src/apps/officer-web/Screens/Dashboard/Layout.tsx`:

```tsx
{plugins?.MyPlugin !== false && (
  <DropdownMenuItem asChild className="cursor-pointer">
    <Link to="/my-plugin">
      <SomeIcon className="mr-2 h-4 w-4" />
      My Plugin
    </Link>
  </DropdownMenuItem>
)}
```

**Home widget** — `src/apps/officer-web/Screens/Dashboard/Home/index.tsx`:

```tsx
import { Widget as MyPluginWidget } from 'plugins/MyPlugin/client';

// Inside the layout:
{plugins?.MyPlugin !== false && <MyPluginWidget />}
```

The `plugins` object comes from `useServerSettings()` and is `undefined` when no overrides exist — so `plugins?.MyPlugin !== false` defaults to showing the plugin (opt-out model).

## Enable/Disable

Plugins are **enabled by default**. Admins can disable them in Server Settings > Plugins, which writes to `~/.config/officer.dev/server-settings.json`:

```json
{
  "plugins": {
    "MyPlugin": false
  }
}
```

When disabled:
- **Server-side**: The plugin router is not mounted (requires server restart)
- **Client-side**: Routes, nav items, and widgets are hidden immediately

## Import Aliases

| Alias | Resolves to | Use in |
|-------|-------------|--------|
| `@@/` | `src/servers/` | Server code (`createRouter`, `custom-errors`, `data-path`) |
| `@/` | `src/apps/officer-web/` | Client code (`components/ui/*`, `Screens/*`, `state/*`) |
| `hooks/` | `src/workspaces/hooks/src/` | Both (`useClient`, `useAuth`) |
| `types` | `src/workspaces/types/` | Both |
| `config` | `src/workspaces/config/` | Client code |
| `plugins/` | `src/workspaces/plugins/` | Both |

## Checklist

- [ ] `MyPlugin/index.ts` exports `plugin` metadata with `id` matching directory name
- [ ] `MyPlugin/server/index.ts` exports `router` and `apiPath`
- [ ] `MyPlugin/client/index.ts` exports `Widget` and/or `Screen`
- [ ] `plugins/package.json` has subpath exports for the new plugin
- [ ] App routes gated with `plugins?.MyPlugin !== false`
- [ ] Nav items gated with `plugins?.MyPlugin !== false`
- [ ] Home widgets gated with `plugins?.MyPlugin !== false`
- [ ] `bunx tsgo` — no new type errors
