-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[With Old OpenSpec Version] Add openspec dashboard command #613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
TabishB
wants to merge
1
commit into
main
Choose a base branch
from
TabishB/web-dashboard-WITH-openspec
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,058
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| schema: spec-driven | ||
| created: 2026-01-28 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
139 changes: 139 additions & 0 deletions
139
openspec/changes/add-web-dashboard-command/specs/cli-dashboard/spec.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate
--portbefore passing it through.parseIntcan yieldNaNor 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