Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions openspec/changes/add-web-dashboard-command/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-28
91 changes: 91 additions & 0 deletions openspec/changes/add-web-dashboard-command/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
## Context

OpenSpec currently has a terminal-based `openspec view` command (`src/core/view.ts`) that outputs a static text dashboard using chalk for formatting. It uses `ViewCommand.getChangesData()` and `ViewCommand.getSpecsData()` to gather project data. There are also utility modules for discovering changes (`src/utils/item-discovery.ts`), parsing task progress (`src/utils/task-progress.ts`), and parsing markdown specs (`src/core/parsers/markdown-parser.ts`).

The project has zero frontend dependencies and uses Node.js built-in modules wherever possible. The CLI uses Commander.js for command registration.

## Goals / Non-Goals

**Goals:**
- Add `openspec dashboard` command that starts a local HTTP server with a web-based project dashboard
- Reuse existing data-gathering logic from `ViewCommand`, `ListCommand`, and `item-discovery` utilities
- Zero new npm dependencies (use Node.js built-in `http` and `child_process` modules)
- Serve a self-contained HTML page with inline CSS and JS
- Support viewing rendered markdown for any artifact
- Clean cross-platform behavior (Windows `start`, macOS `open`, Linux `xdg-open`)

**Non-Goals:**
- Real-time file watching or live-reload (page can be refreshed manually)
- Editing or creating artifacts from the dashboard (read-only)
- Authentication or multi-user access (local development tool)
- External CDN dependencies or build toolchains for the frontend

## Decisions

### Decision 1: Node.js Built-in HTTP Server

Use `node:http` to create the server rather than adding Express or another framework.

**Rationale:**
- Zero new dependencies aligns with the project's minimal dependency philosophy
- The API surface is small (one HTML page, ~5 JSON endpoints)
- Commander.js already handles CLI concerns; the server is a simple request router
- `node:http` is stable, well-documented, and available on all target platforms

### Decision 2: Self-Contained HTML with Inline Markdown Rendering

The dashboard is a single HTML file with inline `<style>` and `<script>` blocks, including a minimal markdown-to-HTML converter (~60 lines of JS).

**Rationale:**
- No build step required; the HTML string is embedded in the TypeScript source
- No external CDN requests (works offline and in air-gapped environments)
- The markdown subset needed is limited (headings, lists, bold, code blocks, checkboxes, links) so a minimal parser suffices
- The HTML template is generated via a function that returns a template literal, making it easy to maintain

### Decision 3: Domain Grouping by Prefix

Specs are grouped by the prefix before the first hyphen in their directory name (e.g., `cli-init` and `cli-view` both go under domain "cli"). Specs with no hyphen use their full name as the domain.

**Rationale:**
- Matches the existing naming convention in the project (`cli-*`, `opsx-*`, etc.)
- Provides useful organization without requiring explicit domain configuration
- Simple and deterministic—no configuration needed

### Decision 4: Reuse Existing Data Utilities

The `DashboardCommand` class reuses `getTaskProgressForChange()`, `MarkdownParser`, and file-reading patterns from `ViewCommand` / `ListCommand` rather than implementing new data access.

**Rationale:**
- Keeps behavior consistent with the terminal dashboard
- Avoids duplicating file system traversal logic
- If the data model changes, only one place needs updating

### Decision 5: Browser Opening via child_process

Use `child_process.exec()` with platform-specific commands (`open` on macOS, `xdg-open` on Linux, `start` on Windows) to open the browser.

**Rationale:**
- Standard cross-platform approach used by many CLI tools
- No dependency on the `open` npm package
- Failures are non-fatal (user can always navigate manually)

### Decision 6: Port Selection with Fallback

Default to port 3000. If the user doesn't specify `--port` and port 3000 is unavailable, increment and try successive ports up to 3010.

**Rationale:**
- Port 3000 is a conventional default for development servers
- Auto-fallback avoids frustration when another process is using the port
- `--port` flag gives explicit control when needed
- Limiting fallback range (10 ports) avoids silently binding to unexpected ports

## Risks / Trade-offs

**Risk: Inline HTML becomes large and hard to maintain**
The HTML template embedded in TypeScript could grow. Mitigation: Keep the dashboard focused on reading project data (no editing features). The HTML function is isolated and can be extracted to a separate `.html` file loaded at runtime if it grows beyond ~300 lines.

**Risk: Minimal markdown parser doesn't handle edge cases**
A 60-line parser won't support every markdown feature. Mitigation: OpenSpec artifacts use a consistent subset of markdown (headings, lists, bold, code fences, checkboxes). The parser handles this subset. Full GFM support is a non-goal for v1.

**Risk: Cross-platform browser opening**
`child_process.exec()` with platform commands may fail on unusual configurations. Mitigation: Failures are caught and logged as warnings; the URL is always printed to stdout so users can navigate manually.
25 changes: 25 additions & 0 deletions openspec/changes/add-web-dashboard-command/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Why

The existing `openspec view` command outputs a static, text-based dashboard in the terminal. While useful for quick glances, it cannot display rendered markdown content, makes it difficult to navigate large projects with many specs and archived changes, and offers no interactivity beyond reading the output. A web-based dashboard would let users browse their entire OpenSpec project visually: viewing active changes with artifact status, exploring specs organized by domain prefix, reviewing archive history, and reading any artifact's rendered markdown, all in the browser with zero external dependencies.

## What Changes

- Add new `openspec dashboard` CLI command that starts a local HTTP server and opens the default browser
- The server exposes a JSON API that surfaces project data: active changes (with artifact completion status), main specs (grouped by domain prefix), and archived changes
- The server also serves a single-page HTML dashboard with embedded CSS and JavaScript (no build step, no frontend dependencies)
- The dashboard renders markdown content inline so users can read any artifact (proposal, design, tasks, spec) without leaving the browser
- The server shuts down cleanly on SIGINT/SIGTERM or when the user presses Ctrl+C

## Capabilities

### New Capabilities
- `cli-dashboard`: A `dashboard` command that starts a local web server serving a single-page dashboard for browsing OpenSpec project data, including active changes with artifact status, main specs grouped by domain, archive history, and rendered markdown content for any artifact

### Modified Capabilities
<!-- No existing specs are being modified - this is purely additive -->

## Impact

- `src/core/dashboard.ts`: New module implementing `DashboardCommand` with HTTP server, JSON API routes, and embedded HTML/CSS/JS
- `src/cli/index.ts`: Register the `dashboard` command with Commander.js
- `package.json`: No new dependencies needed (uses Node.js built-in `http` module and a lightweight bundled markdown-to-HTML converter)
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
## ADDED Requirements

### Requirement: Dashboard Command Registration

The system SHALL provide an `openspec dashboard` command that starts a local web server and opens a browser-based dashboard.

#### Scenario: Command invocation with defaults

- **WHEN** user runs `openspec dashboard`
- **THEN** system starts an HTTP server on port 3000 (or next available port)
- **AND** opens the default browser to `http://localhost:<port>`
- **AND** prints the URL to stdout

#### Scenario: Custom port option

- **WHEN** user runs `openspec dashboard --port 8080`
- **THEN** system starts the HTTP server on port 8080
- **AND** if port 8080 is in use, exits with an error message

#### Scenario: No-open option

- **WHEN** user runs `openspec dashboard --no-open`
- **THEN** system starts the server but does not open the browser
- **AND** prints the URL to stdout so user can navigate manually

#### Scenario: No openspec directory

- **WHEN** user runs `openspec dashboard` in a directory without an `openspec/` folder
- **THEN** system exits with error message indicating OpenSpec is not initialized

#### Scenario: Graceful shutdown

- **WHEN** user presses Ctrl+C or sends SIGINT/SIGTERM
- **THEN** system closes the HTTP server and exits cleanly

### Requirement: JSON API

The server SHALL expose a JSON API for the dashboard frontend to consume project data.

#### Scenario: GET /api/changes

- **WHEN** frontend requests `GET /api/changes`
- **THEN** server responds with JSON containing arrays for `draft`, `active`, and `completed` changes
- **AND** each active change includes `name` and `progress` (with `completed` and `total` task counts)
- **AND** each change includes an `artifacts` list showing which artifact files exist

#### Scenario: GET /api/specs

- **WHEN** frontend requests `GET /api/specs`
- **THEN** server responds with JSON array of specs, each containing `name`, `domain` (prefix before first hyphen or full name), and `requirementCount`

#### Scenario: GET /api/archive

- **WHEN** frontend requests `GET /api/archive`
- **THEN** server responds with JSON array of archived change names sorted reverse-chronologically

#### Scenario: GET /api/artifact

- **WHEN** frontend requests `GET /api/artifact?type=change&name=<name>&file=<path>`
- **THEN** server responds with JSON containing the raw markdown `content` of that artifact file
- **AND** the `file` parameter supports paths like `proposal.md`, `design.md`, `tasks.md`, `specs/<name>/spec.md`

#### Scenario: GET /api/artifact for specs

- **WHEN** frontend requests `GET /api/artifact?type=spec&name=<name>`
- **THEN** server responds with the raw markdown content of `openspec/specs/<name>/spec.md`

#### Scenario: GET /api/artifact for archived changes

- **WHEN** frontend requests `GET /api/artifact?type=archive&name=<name>&file=<path>`
- **THEN** server responds with the raw markdown content of the file inside `openspec/changes/archive/<name>/<path>`

#### Scenario: Artifact not found

- **WHEN** frontend requests an artifact that does not exist
- **THEN** server responds with 404 status and a JSON error message

#### Scenario: Path traversal prevention

- **WHEN** frontend requests an artifact path containing `..` segments
- **THEN** server responds with 400 status and rejects the request
- **AND** the resolved path must remain within the `openspec/` directory

### Requirement: Dashboard HTML Page

The server SHALL serve a single-page HTML dashboard at the root URL with embedded CSS and JavaScript.

#### Scenario: Page load

- **WHEN** browser navigates to `http://localhost:<port>/`
- **THEN** server responds with a self-contained HTML page (inline styles and scripts, no external dependencies)

#### Scenario: Active changes section

- **WHEN** dashboard loads change data
- **THEN** it displays a section showing draft, active, and completed changes
- **AND** active changes show progress bars and artifact status indicators
- **AND** clicking a change name reveals its artifacts for viewing

#### Scenario: Specs section

- **WHEN** dashboard loads spec data
- **THEN** it displays specs grouped by domain prefix (e.g., all `cli-*` specs under a "cli" domain heading)
- **AND** each spec shows its requirement count
- **AND** clicking a spec name opens its rendered markdown content

#### Scenario: Archive section

- **WHEN** dashboard loads archive data
- **THEN** it displays archived changes sorted reverse-chronologically
- **AND** clicking an archived change reveals its artifacts for viewing

#### Scenario: Markdown rendering

- **WHEN** user clicks an artifact to view
- **THEN** dashboard fetches the raw markdown via the API
- **AND** renders it as formatted HTML using a lightweight client-side markdown parser
- **AND** displays it in a content panel alongside the navigation

#### Scenario: Cross-platform path handling

- **WHEN** dashboard constructs file paths for API requests
- **THEN** it uses forward slashes in URL parameters regardless of host OS
- **AND** the server normalizes paths using `path.join()` internally

### Requirement: Error Handling

The dashboard SHALL handle errors gracefully without crashing.

#### Scenario: Malformed API requests

- **WHEN** API receives a request with missing or invalid parameters
- **THEN** server responds with appropriate 4xx status and descriptive JSON error

#### Scenario: File system errors

- **WHEN** reading a file fails due to permissions or corruption
- **THEN** server responds with 500 status and a JSON error message
- **AND** the server continues running for subsequent requests
33 changes: 33 additions & 0 deletions openspec/changes/add-web-dashboard-command/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## 1. Create Dashboard Module

- [ ] 1.1 Create `src/core/dashboard.ts` with `DashboardCommand` class containing an `execute(options)` method
- [ ] 1.2 Implement HTTP server creation using `node:http` with request routing for `/`, `/api/changes`, `/api/specs`, `/api/archive`, `/api/artifact`
- [ ] 1.3 Implement `GET /api/changes` endpoint that returns `{ draft, active, completed }` arrays with artifact existence info and task progress, reusing `getTaskProgressForChange()` and file system checks
- [ ] 1.4 Implement `GET /api/specs` endpoint that returns specs with `name`, `domain` (prefix before first hyphen), and `requirementCount` using `MarkdownParser`
- [ ] 1.5 Implement `GET /api/archive` endpoint that returns archived change names sorted reverse-chronologically
- [ ] 1.6 Implement `GET /api/artifact` endpoint with `type`, `name`, and `file` query parameters, reading raw markdown content from the appropriate directory
- [ ] 1.7 Add path traversal prevention: reject any `file` parameter containing `..` and verify resolved paths remain within `openspec/`
- [ ] 1.8 Implement port selection logic: try default port 3000 (or `--port` value), auto-increment up to +10 if default is in use
- [ ] 1.9 Implement cross-platform browser opening using `child_process.exec()` with `open` (macOS), `xdg-open` (Linux), `start` (Windows)
- [ ] 1.10 Add graceful shutdown on SIGINT/SIGTERM

## 2. Create Dashboard HTML

- [ ] 2.1 Create a `getDashboardHtml()` function in `src/core/dashboard.ts` that returns a self-contained HTML string with inline CSS and JS
- [ ] 2.2 Implement the dashboard layout with sidebar navigation (Changes, Specs, Archive sections) and a main content panel for rendered markdown
- [ ] 2.3 Implement JavaScript to fetch `/api/changes` and render draft/active/completed changes with progress bars and artifact status indicators
- [ ] 2.4 Implement JavaScript to fetch `/api/specs` and render specs grouped by domain prefix with requirement counts
- [ ] 2.5 Implement JavaScript to fetch `/api/archive` and render archived changes sorted reverse-chronologically
- [ ] 2.6 Implement a minimal client-side markdown-to-HTML renderer supporting headings, paragraphs, bold, italic, inline code, code fences, unordered/ordered lists, checkboxes, links, and horizontal rules
- [ ] 2.7 Implement artifact viewing: clicking an artifact fetches its markdown via `/api/artifact` and renders it in the content panel

## 3. Register CLI Command

- [ ] 3.1 Import `DashboardCommand` in `src/cli/index.ts` and register `openspec dashboard` command with `--port <number>` and `--no-open` options
- [ ] 3.2 Follow existing error handling pattern (try/catch with `ora().fail()` and `process.exit(1)`)

## 4. Verify

- [ ] 4.1 Run `pnpm run build` to ensure TypeScript compiles without errors
- [ ] 4.2 Run `pnpm test` to ensure no existing tests are broken
- [ ] 4.3 Manually test `openspec dashboard` in the project directory: verify browser opens, changes/specs/archive load, and markdown rendering works
20 changes: 20 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { UpdateCommand } from '../core/update.js';
import { ListCommand } from '../core/list.js';
import { ArchiveCommand } from '../core/archive.js';
import { ViewCommand } from '../core/view.js';
import { DashboardCommand } from '../core/dashboard.js';
import { registerSpecCommand } from '../commands/spec.js';
import { ChangeCommand } from '../commands/change.js';
import { ValidateCommand } from '../commands/validate.js';
Expand Down Expand Up @@ -201,6 +202,25 @@ program
}
});

program
.command('dashboard')
.description('Open a web-based dashboard for browsing specs, changes, and archive')
.option('--port <number>', 'Port to run the server on (default: 3000)')
.option('--no-open', 'Do not open the browser automatically')
.action(async (options?: { port?: string; open?: boolean }) => {
try {
const dashboardCommand = new DashboardCommand();
await dashboardCommand.execute('.', {
port: options?.port ? parseInt(options.port, 10) : undefined,
open: options?.open,
});
Comment on lines +205 to +216
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate --port before passing it through.
parseInt can yield NaN or out‑of‑range values, which then reach the server and cause runtime errors. Fail fast with a clear message.

🔧 Proposed fix
   .action(async (options?: { port?: string; open?: boolean }) => {
     try {
       const dashboardCommand = new DashboardCommand();
+      const parsedPort =
+        options?.port !== undefined ? Number(options.port) : undefined;
+      if (
+        options?.port !== undefined &&
+        (!Number.isInteger(parsedPort) || parsedPort! < 1 || parsedPort! > 65535)
+      ) {
+        throw new Error(`Invalid port "${options.port}". Use an integer between 1 and 65535.`);
+      }
       await dashboardCommand.execute('.', {
-        port: options?.port ? parseInt(options.port, 10) : undefined,
+        port: parsedPort,
         open: options?.open,
       });
     } catch (error) {
🤖 Prompt for AI Agents
In `@src/cli/index.ts` around lines 205 - 216, Validate the --port option before
calling DashboardCommand.execute: in the dashboard command action, if
options?.port is provided, parse it with parseInt and check Number.isFinite and
that it's an integer within the valid TCP range (1–65535); if parsing fails or
the value is out of range, throw or exit with a clear error message (e.g.,
"Invalid port: must be an integer between 1 and 65535") instead of passing NaN
or an out‑of‑range value to DashboardCommand.execute; otherwise pass undefined
when no port was provided. Ensure you update the argument passed to
DashboardCommand.execute (the object with port and open) to use the validated
numeric port variable.

} catch (error) {
console.log(); // Empty line for spacing
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});

// Change command with subcommands
const changeCmd = program
.command('change')
Expand Down
Loading
Loading