-
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
base: main
Are you sure you want to change the base?
Conversation
Implement a new `openspec dashboard` command that starts a local HTTP server and opens a browser-based dashboard for browsing OpenSpec project data. The dashboard provides a single-page app with zero external dependencies, serving project metrics, active/draft/completed changes with progress tracking, specs organized by domain, archived changes, and rendered markdown content for any artifact.
📝 WalkthroughWalkthroughA new web-based dashboard feature is introduced to openspec. It includes design documentation, formal specifications, implementation tasks, and the actual code implementation of a DashboardCommand that serves a local HTTP server with API endpoints for browsing specs, changes, and archives through an embedded HTML dashboard. Changes
Sequence DiagramsequenceDiagram
actor User
participant CLI as CLI Layer
participant DashboardCmd as DashboardCommand
participant Server as HTTP Server
participant FileSystem as File System
participant Browser as Browser
User->>CLI: openspec dashboard [--port N] [--no-open]
CLI->>DashboardCmd: execute(targetPath, options)
DashboardCmd->>FileSystem: Locate openspec directory
DashboardCmd->>DashboardCmd: findAvailablePort(3000, ...)
DashboardCmd->>Server: Create & listen on available port
Server-->>DashboardCmd: Server ready on port P
alt --no-open flag not set
DashboardCmd->>Browser: openBrowser(http://localhost:P)
end
Browser->>Server: GET / (request dashboard HTML)
Server-->>Browser: getDashboardHtml() with embedded CSS/JS
Browser->>Server: GET /api/changes
Server->>FileSystem: Read changes directory
FileSystem-->>Server: Changes data with artifact status
Server-->>Browser: JSON response
Browser->>Browser: Render dashboard with sidebar
Browser->>Server: GET /api/artifact?type=change&name=X&file=proposal.md
Server->>FileSystem: Read artifact file with path validation
FileSystem-->>Server: Artifact content
Server-->>Browser: Artifact markdown
Browser->>Browser: Render markdown content
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile OverviewGreptile SummaryThis PR adds a new Key changes:
Issues found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant CLI
participant DashboardCommand
participant HTTPServer
participant Browser
participant FileSystem
User->>CLI: openspec dashboard --port 3000
CLI->>DashboardCommand: execute('.', {port: 3000, open: true})
DashboardCommand->>FileSystem: Check openspec/ exists
FileSystem-->>DashboardCommand: Directory exists
DashboardCommand->>DashboardCommand: findAvailablePort(3000)
DashboardCommand->>HTTPServer: Create server & listen on port 3000
HTTPServer-->>DashboardCommand: Server started
DashboardCommand->>Browser: Open http://localhost:3000
DashboardCommand->>DashboardCommand: Register SIGINT/SIGTERM handlers
Browser->>HTTPServer: GET /
HTTPServer->>DashboardCommand: handleRequest()
DashboardCommand-->>Browser: Return HTML page (getDashboardHtml())
Browser->>HTTPServer: GET /api/changes
HTTPServer->>DashboardCommand: handleChanges()
DashboardCommand->>FileSystem: Read openspec/changes/
DashboardCommand->>FileSystem: Check artifacts exist
DashboardCommand-->>Browser: JSON {draft, active, completed}
Browser->>HTTPServer: GET /api/specs
HTTPServer->>DashboardCommand: handleSpecs()
DashboardCommand->>FileSystem: Read openspec/specs/
DashboardCommand->>FileSystem: Parse spec.md files
DashboardCommand-->>Browser: JSON [specs with domains]
Browser->>HTTPServer: GET /api/artifact?type=change&name=foo&file=proposal.md
HTTPServer->>DashboardCommand: handleArtifact()
DashboardCommand->>DashboardCommand: Validate path (prevent traversal)
DashboardCommand->>FileSystem: Read artifact file
DashboardCommand-->>Browser: JSON {content: "..."}
Browser->>Browser: renderMarkdown(content)
User->>CLI: Ctrl+C (SIGINT)
DashboardCommand->>HTTPServer: Close server
DashboardCommand->>CLI: process.exit(0)
|
Review CompleteYour review story is ready! Comment !reviewfast on this PR to re-generate the story. |
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.
2 files reviewed, 6 comments
| } else if (c.status === 'completed') { | ||
| extra = '<span class="progress-mini">Done</span>'; | ||
| } | ||
| html += '<div class="sidebar-item' + cls + '" onclick="selectChange(\\''+esc(c.name)+'\\',\\''+c.status+'\\')"><span class="dot dot-'+c.status+'"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(c.name)+'</span>'+extra+'</div>'; |
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.
concatenated inline HTML strings with user-controlled data through string interpolation on line 495, creating XSS vulnerability
| html += '<div class="sidebar-item' + cls + '" onclick="selectChange(\\''+esc(c.name)+'\\',\\''+c.status+'\\')"><span class="dot dot-'+c.status+'"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(c.name)+'</span>'+extra+'</div>'; | |
| html += '<div class="sidebar-item' + cls + '" onclick="selectChange(\''+esc(c.name)+'\',\''+esc(c.status)+'\')"><span class="dot dot-'+esc(c.status)+'"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(c.name)+'</span>'+extra+'</div>'; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 495:495
Comment:
concatenated inline HTML strings with user-controlled data through string interpolation on line 495, creating XSS vulnerability
```suggestion
html += '<div class="sidebar-item' + cls + '" onclick="selectChange(\''+esc(c.name)+'\',\''+esc(c.status)+'\')"><span class="dot dot-'+esc(c.status)+'"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(c.name)+'</span>'+extra+'</div>';
```
How can I resolve this? If you propose a fix, please make it concise.| // Code (backtick) - must come before bold/italic to avoid conflicts | ||
| s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>'); | ||
| // Bold | ||
| s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>'); |
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.
regex /\*\*([^*]+)\*\*/g for bold text doesn't handle nested bold or multiple asterisks correctly (e.g. **bold*** would match incorrectly)
| s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>'); | |
| s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 729:729
Comment:
regex `/\*\*([^*]+)\*\*/g` for bold text doesn't handle nested bold or multiple asterisks correctly (e.g. `**bold***` would match incorrectly)
```suggestion
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
```
How can I resolve this? If you propose a fix, please make it concise.| // Bold | ||
| s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>'); | ||
| // Italic | ||
| s = s.replace(/\\*([^*]+)\\*/g, '<em>$1</em>'); |
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.
regex /\*([^*]+)\*/g for italic incorrectly captures bold markers (e.g. **bold** would partially match)
| s = s.replace(/\\*([^*]+)\\*/g, '<em>$1</em>'); | |
| s = s.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>'); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 731:731
Comment:
regex `/\*([^*]+)\*/g` for italic incorrectly captures bold markers (e.g. `**bold**` would partially match)
```suggestion
s = s.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
```
How can I resolve this? If you propose a fix, please make it concise.| process.on('SIGINT', shutdown); | ||
| process.on('SIGTERM', shutdown); |
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.
signal handlers registered but never cleaned up, can cause memory leaks or duplicate handlers if execute() called multiple times
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 55:56
Comment:
signal handlers registered but never cleaned up, can cause memory leaks or duplicate handlers if `execute()` called multiple times
How can I resolve this? If you propose a fix, please make it concise.| const key = 'spec:' + s.name; | ||
| const cls = activeItem === key ? ' active' : ''; | ||
| const reqLabel = s.requirementCount === 1 ? 'req' : 'reqs'; | ||
| html += '<div class="sidebar-item' + cls + '" onclick="selectSpec(\\''+esc(s.name)+'\\')"><span class="dot dot-spec"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.name)+'</span><span class="progress-mini">'+s.requirementCount+' '+reqLabel+'</span></div>'; |
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.
inline attribute onclick with string interpolation on line 516 creates XSS risk if spec name contains quotes
| html += '<div class="sidebar-item' + cls + '" onclick="selectSpec(\\''+esc(s.name)+'\\')"><span class="dot dot-spec"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.name)+'</span><span class="progress-mini">'+s.requirementCount+' '+reqLabel+'</span></div>'; | |
| html += '<div class="sidebar-item' + cls + '" onclick="selectSpec(\''+esc(s.name)+'\')"><span class="dot dot-spec"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.name)+'</span><span class="progress-mini">'+s.requirementCount+' '+reqLabel+'</span></div>'; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 516:516
Comment:
inline attribute `onclick` with string interpolation on line 516 creates XSS risk if spec name contains quotes
```suggestion
html += '<div class="sidebar-item' + cls + '" onclick="selectSpec(\''+esc(s.name)+'\')"><span class="dot dot-spec"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.name)+'</span><span class="progress-mini">'+s.requirementCount+' '+reqLabel+'</span></div>';
```
How can I resolve this? If you propose a fix, please make it concise.| for (const name of archiveData) { | ||
| const key = 'archive:' + name; | ||
| const cls = activeItem === key ? ' active' : ''; | ||
| html += '<div class="sidebar-item' + cls + '" onclick="selectArchive(\\''+esc(name)+'\\')"><span class="dot dot-archive"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(name)+'</span></div>'; |
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.
inline attribute onclick with string interpolation creates XSS risk
| html += '<div class="sidebar-item' + cls + '" onclick="selectArchive(\\''+esc(name)+'\\')"><span class="dot dot-archive"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(name)+'</span></div>'; | |
| html += '<div class="sidebar-item' + cls + '" onclick="selectArchive(\''+esc(name)+'\')"><span class="dot dot-archive"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(name)+'</span></div>'; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 529:529
Comment:
inline attribute `onclick` with string interpolation creates XSS risk
```suggestion
html += '<div class="sidebar-item' + cls + '" onclick="selectArchive(\''+esc(name)+'\')"><span class="dot dot-archive"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(name)+'</span></div>';
```
How can I resolve this? If you propose a fix, please make it concise.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.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/cli/index.ts`:
- Around line 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.
In `@src/core/dashboard.ts`:
- Around line 329-334: The sendJson method currently sets a wildcard CORS header
which exposes local project data; remove the 'Access-Control-Allow-Origin': '*'
entry from the res.writeHead call in sendJson (or replace it with a strict
allowlist check using the incoming request Origin and a configured allowed
origin) so the API remains same‑origin only; update any callers to provide an
Origin or config value if you implement allowlist behavior and ensure sendJson
only emits the header when the Origin matches an approved value.
- Around line 724-739: The link rendering in inline() allows unsafe schemes and
omits rel attributes; update the link replacement to use a callback that (1)
URL-decodes/validates the captured href against a whitelist of safe
schemes/paths (allow only http:, https:, mailto:, fragment '#' and relative
paths like starting with '/', './', '../' or no scheme), (2) sanitize/escape the
href via escHtml before inserting it, and (3) output the anchor with
target="_blank" plus rel="noopener noreferrer" when allowed; if the href is not
allowed, render only the link text without a clickable href (or use a safe
fallback like href="#" without target/rel). Implement this logic inside inline()
where the /\[([^\]]+)\]\(([^)]+)\)/g replacement is done and reuse escHtml for
escaping.
| 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, | ||
| }); |
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 --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.
| private sendJson(res: http.ServerResponse, status: number, data: any): void { | ||
| res.writeHead(status, { | ||
| 'Content-Type': 'application/json', | ||
| 'Access-Control-Allow-Origin': '*', | ||
| }); | ||
| res.end(JSON.stringify(data)); |
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.
Remove wildcard CORS to prevent cross‑origin reads of local data.
The API is same‑origin with the embedded dashboard, so Access-Control-Allow-Origin: * allows any website to read local project data while the server is running. Remove it (or strictly restrict it).
🔧 Proposed fix
res.writeHead(status, {
'Content-Type': 'application/json',
- 'Access-Control-Allow-Origin': '*',
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private sendJson(res: http.ServerResponse, status: number, data: any): void { | |
| res.writeHead(status, { | |
| 'Content-Type': 'application/json', | |
| 'Access-Control-Allow-Origin': '*', | |
| }); | |
| res.end(JSON.stringify(data)); | |
| private sendJson(res: http.ServerResponse, status: number, data: any): void { | |
| res.writeHead(status, { | |
| 'Content-Type': 'application/json', | |
| }); | |
| res.end(JSON.stringify(data)); |
🤖 Prompt for AI Agents
In `@src/core/dashboard.ts` around lines 329 - 334, The sendJson method currently
sets a wildcard CORS header which exposes local project data; remove the
'Access-Control-Allow-Origin': '*' entry from the res.writeHead call in sendJson
(or replace it with a strict allowlist check using the incoming request Origin
and a configured allowed origin) so the API remains same‑origin only; update any
callers to provide an Origin or config value if you implement allowlist behavior
and ensure sendJson only emits the header when the Origin matches an approved
value.
| function inline(text) { | ||
| let s = escHtml(text); | ||
| // Code (backtick) - must come before bold/italic to avoid conflicts | ||
| s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>'); | ||
| // Bold | ||
| s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>'); | ||
| // Italic | ||
| s = s.replace(/\\*([^*]+)\\*/g, '<em>$1</em>'); | ||
| // Links | ||
| s = s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>'); | ||
| return s; | ||
| } | ||
|
|
||
| function escHtml(s) { | ||
| return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | ||
| } |
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.
Harden rendered links (tabnabbing + unsafe schemes).
Links open in a new tab without rel="noopener noreferrer" and accept any scheme. That allows javascript: links from markdown and tabnabbing. Add rel and filter schemes to http(s)/mailto/relative/fragment only.
🔧 Proposed fix
function inline(text) {
let s = escHtml(text);
// Code (backtick) - must come before bold/italic to avoid conflicts
s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
// Bold
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// Italic
s = s.replace(/\*([^*]+)\*/g, '<em>$1</em>');
// Links
- s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
+ s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (m, text, url) => {
+ const safeUrl = sanitizeUrl(url);
+ return '<a href="' + safeUrl + '" target="_blank" rel="noopener noreferrer">' + text + '</a>';
+ });
return s;
}
+ function sanitizeUrl(url) {
+ const u = url.trim();
+ if (/^(https?:|mailto:|\/|#|\.\.?\/)/i.test(u)) return u;
+ return '#';
+ }
+
function escHtml(s) {
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}🤖 Prompt for AI Agents
In `@src/core/dashboard.ts` around lines 724 - 739, The link rendering in inline()
allows unsafe schemes and omits rel attributes; update the link replacement to
use a callback that (1) URL-decodes/validates the captured href against a
whitelist of safe schemes/paths (allow only http:, https:, mailto:, fragment '#'
and relative paths like starting with '/', './', '../' or no scheme), (2)
sanitize/escape the href via escHtml before inserting it, and (3) output the
anchor with target="_blank" plus rel="noopener noreferrer" when allowed; if the
href is not allowed, render only the link text without a clickable href (or use
a safe fallback like href="#" without target/rel). Implement this logic inside
inline() where the /\[([^\]]+)\]\(([^)]+)\)/g replacement is done and reuse
escHtml for escaping.
Summary
Implement a new
openspec dashboardcommand that launches a web-based dashboard for browsing OpenSpec project data. The dashboard runs on localhost with zero external dependencies and provides a single-page interface for viewing active changes with progress tracking, specs organized by domain, archived changes, and rendered markdown content for any artifact.Key Features
Testing
All 1190 existing tests pass. New functionality verified to compile and integrate correctly with the CLI.
Summary by CodeRabbit
Release Notes
openspec dashboardcommand that launches a local web-based interface for browsing specs, changes, and archived items.--portand--no-openoptions.✏️ Tip: You can customize this high-level summary in your review settings.