Extensions are TypeScript files that hook into fp's issue-tracking lifecycle. They run in both the CLI (Bun) and the desktop app (Node/Electron). Each extension exports an `init` function that receives the `fp` context object.

## Quick Start

```ts
import type { ExtensionInit } from "@fiberplane/extensions";

const init: ExtensionInit = (fp) => {
  fp.on("issue:created", ({ issue }) => {
    fp.log.info(`New issue: ${issue.title}`);
  });
};

export default init;
```

The `ExtensionInit` signature is `(fp: FpExtensionContext) => void | Promise<void>`. The default export is the entry point.

When your init function needs to `await` calls like `registerProperty`, make it async:

```ts
const init: ExtensionInit = async (fp) => {
  await fp.issues.registerProperty("environment", {
    label: "Environment",
    icon: "globe",
    display: fp.ui.properties.select(
      fp.ui.properties.option("staging", { label: "Staging", color: "yellow" }),
      fp.ui.properties.option("production", { label: "Production", color: "red" }),
    ),
  });
};

export default init;
```

Always use the type-only import:

```ts
import type { ... } from "@fiberplane/extensions";
```

## Extension Discovery

fp loads extensions from two locations, checked in order:

1. **Project extensions** -- `.fp/extensions/` in your project root (takes precedence)
2. **Global extensions** -- `~/.fiberplane/extensions/`

Supported file types: `.ts`, `.js`, `.mts`, `.mjs`. An extension can be a single file or a directory with an `index.*` entry point. Files ending in `.d.ts` are skipped.

When both locations contain an extension with the same name, the project-level extension wins.

## The `fp` Context Object

Every extension receives a single `fp` argument of type `FpExtensionContext`:

| Property     | Type                                | Purpose                               |
|--------------|-------------------------------------|---------------------------------------|
| `issues`     | `ExtensionIssueContextAccessPromise`| CRUD operations + property registration |
| `comments`   | `ExtensionCommentAccessPromise`     | Create, list, delete comments         |
| `secrets`    | `ExtensionSecretsAccessPromise`     | OS keychain secret storage            |
| `ui`         | `ExtensionUiAccessPromise`          | Actions, notifications, property helpers |
| `config`     | `ExtensionConfigAccess`             | Read extension config values          |
| `log`        | `ExtensionLogger`                   | Structured logging                    |
| `on`         | Hook registration function          | Subscribe to lifecycle events         |
| `runtime`    | `ExtensionRuntime`                  | `"cli"` or `"desktop"`               |
| `projectDir` | `string`                            | Absolute path to project root         |

## Runtime Compatibility

Extensions must run in both the CLI (Bun runtime) and the desktop app (Node/Electron). Write Node-compatible code only.

### Allowed APIs

- `node:child_process` -- `spawn`, `execFile`
- `node:fs/promises` -- `readFile`, `writeFile`, `mkdir`, `rm`
- `node:path` -- `resolve`, `join`

### Avoid

- Bun global APIs (`Bun.spawn`, `Bun.file`, `Bun.env`)
- Bun-only modules (`bun:ffi`, `bun:sqlite`)
- Any API not present on the `FpExtensionContext` object

`fp.runtime` returns `"cli"` or `"desktop"` if you need conditional behavior, but prefer writing universal code.
