npm package: @stritti/vitepress-plugin-openspec
Description: A VitePress plugin that integrates OpenSpec documentation into your VitePress site
Author: Stephan Strittmatter
Latest version: 0.7.0

# @stritti/vitepress-plugin-openspec

A [VitePress](https://vitepress.dev/) plugin that renders your project's [OpenSpec](https://openspec.dev/) folder as structured documentation pages — specs, changes, and artifacts — with sidebar and nav integration.

[![npm version](https://img.shields.io/npm/v/@stritti/vitepress-plugin-openspec?style=flat-square&color=cb3837&logo=npm)](https://www.npmjs.com/package/@stritti/vitepress-plugin-openspec)
[![npm downloads](https://img.shields.io/npm/dm/@stritti/vitepress-plugin-openspec?style=flat-square&color=cb3837&logo=npm)](https://www.npmjs.com/package/@stritti/vitepress-plugin-openspec)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/stritti/vitepress-plugin-openspec/blob/main/LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/stritti/vitepress-plugin-openspec/publish.yml?branch=main&style=flat-square&logo=github&label=release)](https://github.com/stritti/vitepress-plugin-openspec/actions/workflows/publish.yml)

---

## Installation

```bash
npm install @stritti/vitepress-plugin-openspec
# or with bun:
bun add @stritti/vitepress-plugin-openspec
```

---

## Usage

Add the following to your `.vitepress/config.ts`:

```typescript
import { defineConfig } from 'vitepress'
import { withOpenSpec } from '@stritti/vitepress-plugin-openspec'

export default defineConfig(
  withOpenSpec({
    // your regular VitePress config
    themeConfig: {
      nav: [{ text: 'Home', link: '/' }],
      sidebar: {},
    },
  })
)
```

`withOpenSpec` handles everything in one call: page generation, the Vite plugin for live reload, the nav entry, and the sidebar section. All fields are optional — it works with an empty config object and sensible defaults.

**Other Vite plugins** go into `vite.plugins` as usual — `withOpenSpec` appends to the array without replacing it:

```typescript
import { defineConfig } from 'vitepress'
import { withOpenSpec } from '@stritti/vitepress-plugin-openspec'
import myOtherPlugin from 'vitepress-plugin-something'

export default defineConfig(
  withOpenSpec(
    {
      vite: { plugins: [myOtherPlugin()] }, // preserved alongside openspec
      themeConfig: { nav: [], sidebar: {} },
    },
    { specDir: '../openspec', outDir: 'openspec' } // options (all optional)
  )
)
```

---

## Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `specDir` | `string` | `'./openspec'` | Path to your project's `openspec/` directory. Can be an absolute path or relative to the working directory — use `path.resolve(__dirname, '../../openspec')` when `config.ts` lives in `docs/.vitepress/`. |
| `outDir` | `string` | `'openspec'` | Output directory relative to VitePress `srcDir` |
| `srcDir` | `string` | `process.cwd()` | VitePress source directory (the `docs/` folder) |
| `nav` | `boolean` | `true` | Whether to prepend an openspec entry to `themeConfig.nav` |
| `sidebar` | `boolean` | `true` | Whether to inject the openspec sidebar section into `themeConfig.sidebar` |

> **Missing directory** — if `specDir` does not exist the plugin emits a `console.warn` and skips page generation, nav, and sidebar. No error is thrown and your VitePress build continues normally. This is intentional for projects that haven't set up an `openspec/` folder yet.

---

## Advanced / manual setup

If you need full control over each integration point, you can wire up the lower-level APIs individually:

```typescript
import { defineConfig } from 'vitepress'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import openspec, {
  generateOpenSpecPages,
  generateOpenSpecSidebar,
  openspecNav,
} from '@stritti/vitepress-plugin-openspec'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const specDir = path.resolve(__dirname, '../openspec')

// Must be called before defineConfig so pages exist when VitePress scans for routes.
generateOpenSpecPages({
  specDir,
  outDir: 'openspec',
  srcDir: path.resolve(__dirname, '..'),
})

export default defineConfig({
  vite: {
    plugins: [openspec({ specDir, outDir: 'openspec' })],
  },
  themeConfig: {
    nav: [openspecNav(specDir, { outDir: 'openspec', text: 'Docs' })],
    sidebar: {
      '/openspec/': generateOpenSpecSidebar(specDir, { outDir: 'openspec' }),
    },
  },
})
```

---

## Output Structure

The plugin reads your `openspec/` folder and generates:

```
openspec/                     ← your source (not committed if used as output)
├── specs/
│   └── <capability>/
│       └── spec.md
└── changes/
    ├── <change-name>/
    │   ├── .openspec.yaml
    │   ├── proposal.md
    │   ├── design.md
    │   └── tasks.md
    └── archive/
        └── YYYY-MM-DD-<change-name>/

docs/openspec/                ← generated by the plugin (automatically gitignored)
├── index.md
├── specs/
│   ├── index.md
│   └── <capability>/
│       └── index.md
└── changes/
    ├── index.md
    ├── <change-name>/
    │   ├── index.md
    │   ├── proposal.md
    │   └── tasks.md
    └── archive/
        └── YYYY-MM-DD-<change-name>/
            └── ...
```

The plugin automatically writes a `.gitignore` into the output directory that excludes all generated files from version control. No manual `.gitignore` entry is needed.

---

## How It Works

VitePress scans `srcDir` for `.md` files **before** any Vite plugin hooks run. The plugin solves this with two complementary mechanisms:

1. **`generateOpenSpecPages()`** — called synchronously at the top of `config.ts`, before `defineConfig()`. Ensures all pages exist when VitePress scans for routes (critical for first builds and CI).
2. **`openspec()` Vite plugin** — calls `generateOpenSpecPages()` again on every config reload during `vitepress dev`, keeping pages in sync with your source files.

---

## Development

```bash
bun install       # Install dependencies
bun run build     # Build the plugin
bun run dev       # Watch mode
bun test          # Run tests
bun run lint      # Type check
```

---

## License

MIT