@davevdveen/spec-reader 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/package.json +28 -0
- package/read-specs +210 -0
- package/skills/read-specs/SKILL.md +35 -0
- package/spec-reader +104 -0
- package/spec-viewer.html +2366 -0
- package/view-proposal +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dave van der Veen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# SpecReader
|
|
2
|
+
|
|
3
|
+
Beautiful reading experience for [OpenSpec](https://openspec.dev) specification files and project documentation.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
AI-assisted development generates specs, proposals, and designs faster than ever. But reviewing structured markdown in a terminal or a GitHub diff is painful. When specs are hard to read, they don't get read.
|
|
8
|
+
|
|
9
|
+
SpecReader gives your specifications a proper reading experience. Collapsible requirements, themed reading modes, and structured navigation make it natural to review proposals and specs, whether they're yours or someone else's.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @davevdveen/spec-reader
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Works on any project directory. Auto-detects OpenSpec folders and renders everything readable: specs, proposals, READMEs, configs, and more.
|
|
18
|
+
|
|
19
|
+
Try it on the [OpenSpec framework](https://github.com/Fission-AI/OpenSpec) itself:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/Fission-AI/OpenSpec && cd OpenSpec && npx @davevdveen/spec-reader
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## AI coding agent skill
|
|
26
|
+
|
|
27
|
+
SpecReader includes a skill for AI coding agents (Claude Code, OpenCode) that opens the viewer for any PR or branch:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
/read-specs # open specs for current branch
|
|
31
|
+
/read-specs 42 # checkout PR #42 and open its specs
|
|
32
|
+
/read-specs feature/xyz # checkout branch and open specs
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Install the skill in your project:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx @davevdveen/spec-reader init
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This auto-detects your agent (Claude Code, OpenCode, or both) and installs the skill in the right place.
|
|
42
|
+
|
|
43
|
+
## Scopes
|
|
44
|
+
|
|
45
|
+
| Scope | Shows |
|
|
46
|
+
|-------|-------|
|
|
47
|
+
| Specs | OpenSpec files |
|
|
48
|
+
| Docs | All markdown files |
|
|
49
|
+
| All | All readable files (.md, .yaml, .yml, .txt, extensionless) |
|
|
50
|
+
| Search | Filter across all files |
|
|
51
|
+
|
|
52
|
+
## Themes
|
|
53
|
+
|
|
54
|
+
Four reading themes, each with light and dark variants:
|
|
55
|
+
|
|
56
|
+
| Theme | Font |
|
|
57
|
+
|-------|------|
|
|
58
|
+
| Original | Sans-serif |
|
|
59
|
+
| Paper | Georgia serif |
|
|
60
|
+
| Calm | Serif, warm tones |
|
|
61
|
+
| Mono | Monospace |
|
|
62
|
+
|
|
63
|
+
Follows system light/dark preference with manual override.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
spec-reader # serve current directory
|
|
69
|
+
spec-reader ~/projects/myapp # serve any project
|
|
70
|
+
spec-reader export openspec/ dist/ # export static site
|
|
71
|
+
spec-reader init # install AI agent skill
|
|
72
|
+
view-proposal add-dark-mode # open viewer on a proposal
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Keyboard shortcuts
|
|
76
|
+
|
|
77
|
+
| Key | Action |
|
|
78
|
+
|-----|--------|
|
|
79
|
+
| `←` `→` | Previous / next page |
|
|
80
|
+
| `↑` `↓` | Scroll content |
|
|
81
|
+
| `-` | Expand / collapse all sections in the document |
|
|
82
|
+
| `Cmd+Shift+R` | Toggle sidebar |
|
|
83
|
+
|
|
84
|
+
## How it works
|
|
85
|
+
|
|
86
|
+
A single HTML file served by a shell script that walks your directory, generates a manifest, and starts Python's built-in HTTP server. Zero runtime dependencies beyond Python 3.
|
|
87
|
+
|
|
88
|
+
File changes are detected every 3 seconds. Edit a spec, see it update in the viewer.
|
|
89
|
+
|
|
90
|
+
## Prerequisites
|
|
91
|
+
|
|
92
|
+
- **Python 3** (used to serve files locally)
|
|
93
|
+
- **Node.js** (for `npx` installation only)
|
|
94
|
+
- **gh CLI** (optional, for the `/read-specs` skill to checkout PRs)
|
|
95
|
+
|
|
96
|
+
Works on macOS, Linux, and Windows (WSL). Tested in Safari, should work in any modern browser.
|
|
97
|
+
|
|
98
|
+
## Install
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm install -g @davevdveen/spec-reader
|
|
102
|
+
npx @davevdveen/spec-reader
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@davevdveen/spec-reader",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Beautiful reading experience for OpenSpec specification files",
|
|
5
|
+
"bin": {
|
|
6
|
+
"spec-reader": "./spec-reader",
|
|
7
|
+
"view-proposal": "./view-proposal"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"spec-reader",
|
|
11
|
+
"read-specs",
|
|
12
|
+
"view-proposal",
|
|
13
|
+
"spec-viewer.html",
|
|
14
|
+
"skills/"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"openspec",
|
|
18
|
+
"markdown",
|
|
19
|
+
"viewer",
|
|
20
|
+
"specification",
|
|
21
|
+
"documentation"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/davevdveen/spec-reader"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/read-specs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# read-specs — Discover files, generate manifest, and serve the viewer.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./read-specs [directory] Serve the viewer for the given directory (default: .)
|
|
6
|
+
# ./read-specs --manifest-only [dir] Generate manifest.json without starting a server
|
|
7
|
+
#
|
|
8
|
+
# The viewer HTML stays in this script's directory. Nothing is copied to the
|
|
9
|
+
# target directory — only a temporary manifest.json is created there and
|
|
10
|
+
# cleaned up on exit.
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
TARGET_DIR="${2:-${1:-.}}"
|
|
16
|
+
MANIFEST_ONLY=false
|
|
17
|
+
FOCUS=""
|
|
18
|
+
PORT="${PORT:-3333}"
|
|
19
|
+
|
|
20
|
+
# Parse flags
|
|
21
|
+
if [[ "${1:-}" == "--manifest-only" ]]; then
|
|
22
|
+
MANIFEST_ONLY=true
|
|
23
|
+
TARGET_DIR="${2:-.}"
|
|
24
|
+
elif [[ "${1:-}" == "--focus" ]]; then
|
|
25
|
+
FOCUS="${2:-}"
|
|
26
|
+
TARGET_DIR="${3:-.}"
|
|
27
|
+
elif [[ "${1:-}" == --* ]]; then
|
|
28
|
+
echo "Unknown flag: $1" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Resolve target directory
|
|
33
|
+
TARGET_DIR="$(cd "$TARGET_DIR" && pwd)"
|
|
34
|
+
|
|
35
|
+
# ── Generate manifest.json ──
|
|
36
|
+
generate_manifest() {
|
|
37
|
+
local dir="$1"
|
|
38
|
+
local output="$2"
|
|
39
|
+
local first=true
|
|
40
|
+
|
|
41
|
+
local absdir
|
|
42
|
+
absdir="$(cd "$dir" && pwd)"
|
|
43
|
+
echo '{"generated":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","root":"'"$absdir"'","files":[' > "$output"
|
|
44
|
+
|
|
45
|
+
while IFS= read -r filepath; do
|
|
46
|
+
# Strip leading ./
|
|
47
|
+
filepath="${filepath#./}"
|
|
48
|
+
|
|
49
|
+
# Skip build artifacts, git, and dependency folders
|
|
50
|
+
[[ "$filepath" == .git/* ]] && continue
|
|
51
|
+
[[ "$filepath" == .build/* ]] && continue
|
|
52
|
+
[[ "$filepath" == */.build/* ]] && continue
|
|
53
|
+
[[ "$filepath" == node_modules/* ]] && continue
|
|
54
|
+
[[ "$filepath" == */node_modules/* ]] && continue
|
|
55
|
+
[[ "$filepath" == Pods/* ]] && continue
|
|
56
|
+
[[ "$filepath" == */Pods/* ]] && continue
|
|
57
|
+
[[ "$filepath" == Carthage/* ]] && continue
|
|
58
|
+
[[ "$filepath" == */Carthage/* ]] && continue
|
|
59
|
+
[[ "$filepath" == */checkouts/* ]] && continue
|
|
60
|
+
[[ "$filepath" == *.docc/* ]] && continue
|
|
61
|
+
[[ "$filepath" == manifest.json ]] && continue
|
|
62
|
+
[[ "$(basename "$filepath")" == .openspec.yaml ]] && continue
|
|
63
|
+
|
|
64
|
+
local name
|
|
65
|
+
name="$(basename "$filepath")"
|
|
66
|
+
|
|
67
|
+
# Calculate task counts for tasks.md files
|
|
68
|
+
local task_checked="" task_total=""
|
|
69
|
+
if [[ "$name" == "tasks.md" ]]; then
|
|
70
|
+
local checked unchecked total
|
|
71
|
+
checked=$(grep -c '\- \[x\]' "$dir/$filepath" 2>/dev/null || true)
|
|
72
|
+
unchecked=$(grep -c '\- \[ \]' "$dir/$filepath" 2>/dev/null || true)
|
|
73
|
+
checked=${checked:-0}
|
|
74
|
+
unchecked=${unchecked:-0}
|
|
75
|
+
checked=$(echo "$checked" | tr -d '[:space:]')
|
|
76
|
+
unchecked=$(echo "$unchecked" | tr -d '[:space:]')
|
|
77
|
+
total=$((checked + unchecked))
|
|
78
|
+
if [[ $total -gt 0 ]]; then
|
|
79
|
+
task_checked=$checked
|
|
80
|
+
task_total=$total
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Emit JSON — just path and name, no section classification
|
|
85
|
+
if $first; then
|
|
86
|
+
first=false
|
|
87
|
+
else
|
|
88
|
+
printf ',' >> "$output"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if [[ -n "$task_checked" ]]; then
|
|
92
|
+
printf '{"path":"%s","name":"%s","taskChecked":%s,"taskTotal":%s}' \
|
|
93
|
+
"$filepath" "$name" "$task_checked" "$task_total" >> "$output"
|
|
94
|
+
else
|
|
95
|
+
printf '{"path":"%s","name":"%s"}' \
|
|
96
|
+
"$filepath" "$name" >> "$output"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
done < <(cd "$dir" && find . -type f \( \
|
|
100
|
+
-name '*.md' -o -name '*.yaml' -o -name '*.yml' -o -name '*.txt' \
|
|
101
|
+
-o -name 'Brewfile' -o -name 'Gemfile' -o -name 'Podfile' -o -name 'Fastfile' \
|
|
102
|
+
-o -name 'Appfile' -o -name 'Matchfile' -o -name 'Pluginfile' -o -name 'Dangerfile' \
|
|
103
|
+
-o -name 'Dockerfile' -o -name 'Makefile' -o -name 'CODEOWNERS' -o -name 'LICENSE' \
|
|
104
|
+
\) | sort)
|
|
105
|
+
|
|
106
|
+
echo ']}' >> "$output"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# ── Main ──
|
|
110
|
+
|
|
111
|
+
# Generate manifest in a temp location
|
|
112
|
+
MANIFEST="$(mktemp)"
|
|
113
|
+
generate_manifest "$TARGET_DIR" "$MANIFEST"
|
|
114
|
+
echo "Generated manifest with $(grep -o '"path"' "$MANIFEST" | wc -l | tr -d ' ') files"
|
|
115
|
+
|
|
116
|
+
if $MANIFEST_ONLY; then
|
|
117
|
+
cp "$MANIFEST" "$TARGET_DIR/manifest.json"
|
|
118
|
+
rm -f "$MANIFEST"
|
|
119
|
+
echo "Wrote manifest.json to $TARGET_DIR"
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Start a Python server that serves:
|
|
124
|
+
# /spec-viewer.html → from SCRIPT_DIR
|
|
125
|
+
# /manifest.json → from temp file
|
|
126
|
+
# everything else → from TARGET_DIR
|
|
127
|
+
python3 -u - "$SCRIPT_DIR" "$TARGET_DIR" "$MANIFEST" "$PORT" <<'PYSERVER' &
|
|
128
|
+
import http.server
|
|
129
|
+
import os
|
|
130
|
+
import sys
|
|
131
|
+
import urllib.parse
|
|
132
|
+
|
|
133
|
+
script_dir = sys.argv[1]
|
|
134
|
+
target_dir = sys.argv[2]
|
|
135
|
+
manifest_path = sys.argv[3]
|
|
136
|
+
port = int(sys.argv[4])
|
|
137
|
+
|
|
138
|
+
class SpecHandler(http.server.SimpleHTTPRequestHandler):
|
|
139
|
+
def do_GET(self):
|
|
140
|
+
clean_path = self.path.split('?', 1)[0].split('#', 1)[0].lstrip('/')
|
|
141
|
+
if clean_path == 'api/refresh':
|
|
142
|
+
import subprocess
|
|
143
|
+
subprocess.run(
|
|
144
|
+
[os.path.join(script_dir, 'read-specs'), '--manifest-only', target_dir],
|
|
145
|
+
capture_output=True
|
|
146
|
+
)
|
|
147
|
+
import shutil
|
|
148
|
+
generated = os.path.join(target_dir, 'manifest.json')
|
|
149
|
+
if os.path.exists(generated):
|
|
150
|
+
shutil.copy(generated, manifest_path)
|
|
151
|
+
os.remove(generated)
|
|
152
|
+
self.send_response(200)
|
|
153
|
+
self.send_header('Content-Type', 'application/json')
|
|
154
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
155
|
+
self.end_headers()
|
|
156
|
+
self.wfile.write(b'{"ok":true}')
|
|
157
|
+
return
|
|
158
|
+
super().do_GET()
|
|
159
|
+
|
|
160
|
+
def translate_path(self, path):
|
|
161
|
+
# Strip query string and fragment
|
|
162
|
+
path = path.split('?', 1)[0].split('#', 1)[0]
|
|
163
|
+
# URL-decode (handles %20 for spaces, etc.)
|
|
164
|
+
path = urllib.parse.unquote(path)
|
|
165
|
+
# Remove leading /
|
|
166
|
+
path = path.lstrip('/')
|
|
167
|
+
|
|
168
|
+
# Serve viewer HTML from the script directory
|
|
169
|
+
if path == 'spec-viewer.html':
|
|
170
|
+
return os.path.join(script_dir, 'spec-viewer.html')
|
|
171
|
+
|
|
172
|
+
# Serve manifest from temp file
|
|
173
|
+
if path == 'manifest.json':
|
|
174
|
+
return manifest_path
|
|
175
|
+
|
|
176
|
+
# Everything else from target directory
|
|
177
|
+
return os.path.join(target_dir, path)
|
|
178
|
+
|
|
179
|
+
def log_message(self, format, *args):
|
|
180
|
+
pass # Silence request logs
|
|
181
|
+
|
|
182
|
+
server = http.server.HTTPServer(('', port), SpecHandler)
|
|
183
|
+
print(f"Serving specs from {target_dir}", flush=True)
|
|
184
|
+
server.serve_forever()
|
|
185
|
+
PYSERVER
|
|
186
|
+
SERVER_PID=$!
|
|
187
|
+
|
|
188
|
+
cleanup() {
|
|
189
|
+
kill "$SERVER_PID" 2>/dev/null
|
|
190
|
+
rm -f "$MANIFEST"
|
|
191
|
+
}
|
|
192
|
+
trap cleanup EXIT
|
|
193
|
+
|
|
194
|
+
sleep 1
|
|
195
|
+
|
|
196
|
+
URL="http://localhost:$PORT/spec-viewer.html"
|
|
197
|
+
if [[ -n "$FOCUS" ]]; then
|
|
198
|
+
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$FOCUS")
|
|
199
|
+
URL="${URL}#${ENCODED}"
|
|
200
|
+
fi
|
|
201
|
+
echo "Viewer: $URL"
|
|
202
|
+
|
|
203
|
+
if command -v open &>/dev/null; then
|
|
204
|
+
open "$URL"
|
|
205
|
+
elif command -v xdg-open &>/dev/null; then
|
|
206
|
+
xdg-open "$URL"
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
echo "Press Ctrl+C to stop"
|
|
210
|
+
wait
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: read-specs
|
|
3
|
+
description: Open the SpecReader viewer to read OpenSpec specifications, proposals, designs, tasks, and project documentation in a themed reading experience. Use this skill whenever the user wants to read specs, review a proposal, browse documentation, view OpenSpec files, read READMEs, or says "open specs", "read specs", "show me the specs", "review the proposal", "open the viewer", or "/read-specs". Also trigger when the user wants to review specs from a specific PR or branch.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Open the SpecReader viewer for the current repo's OpenSpec files and documentation, or for a specific PR or branch.
|
|
7
|
+
|
|
8
|
+
Works with Claude Code and OpenCode.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
- `/read-specs` — open specs for the current branch
|
|
13
|
+
- `/read-specs 42` — checkout PR #42 and open its specs
|
|
14
|
+
- `/read-specs feature/my-branch` — checkout branch and open its specs
|
|
15
|
+
|
|
16
|
+
## Steps
|
|
17
|
+
|
|
18
|
+
1. If an argument is provided:
|
|
19
|
+
- If it's a number, run `gh pr checkout <number>` (requires gh CLI)
|
|
20
|
+
- If it's a branch name, run `git checkout <branch>`
|
|
21
|
+
|
|
22
|
+
2. Find the project root (look for `.git` directory).
|
|
23
|
+
|
|
24
|
+
3. Run the viewer in the background:
|
|
25
|
+
```bash
|
|
26
|
+
npx @davevdveen/spec-reader &
|
|
27
|
+
```
|
|
28
|
+
This starts a local server and opens the browser.
|
|
29
|
+
|
|
30
|
+
4. Tell the user:
|
|
31
|
+
- "Spec viewer is open at http://localhost:3333"
|
|
32
|
+
- "Changes are auto-detected every 3 seconds"
|
|
33
|
+
- "Use arrow keys to navigate between pages"
|
|
34
|
+
- "Use the scope bar to switch between Specs, Docs, All, or Search"
|
|
35
|
+
- "Use the Aa button to change themes and light/dark mode"
|
package/spec-reader
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# spec-reader — View, export, or set up SpecReader
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# spec-reader [directory] Serve the viewer (default: .)
|
|
8
|
+
# spec-reader export [dir] [output] Export static site to a directory
|
|
9
|
+
# spec-reader init Install the /read-specs skill for your AI coding agent
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
|
|
13
|
+
case "${1:-}" in
|
|
14
|
+
init)
|
|
15
|
+
SKILL_SRC="$SCRIPT_DIR/skills/read-specs/SKILL.md"
|
|
16
|
+
installed=false
|
|
17
|
+
|
|
18
|
+
if [[ -d ".claude" ]]; then
|
|
19
|
+
mkdir -p .claude/skills/read-specs
|
|
20
|
+
cp "$SKILL_SRC" .claude/skills/read-specs/SKILL.md
|
|
21
|
+
echo "Installed skill to .claude/skills/read-specs/"
|
|
22
|
+
installed=true
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if [[ -d ".opencode" ]]; then
|
|
26
|
+
mkdir -p .opencode/skills/read-specs
|
|
27
|
+
cp "$SKILL_SRC" .opencode/skills/read-specs/SKILL.md
|
|
28
|
+
echo "Installed skill to .opencode/skills/read-specs/"
|
|
29
|
+
installed=true
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [[ "$installed" == false ]]; then
|
|
33
|
+
echo "No .claude/ or .opencode/ directory found."
|
|
34
|
+
echo ""
|
|
35
|
+
echo "Which agent do you use?"
|
|
36
|
+
echo " 1) Claude Code (.claude/skills/)"
|
|
37
|
+
echo " 2) OpenCode (.opencode/skills/)"
|
|
38
|
+
echo " 3) Both"
|
|
39
|
+
read -rp "Choice [1/2/3]: " choice
|
|
40
|
+
case "$choice" in
|
|
41
|
+
1)
|
|
42
|
+
mkdir -p .claude/skills/read-specs
|
|
43
|
+
cp "$SKILL_SRC" .claude/skills/read-specs/SKILL.md
|
|
44
|
+
echo "Installed skill to .claude/skills/read-specs/"
|
|
45
|
+
;;
|
|
46
|
+
2)
|
|
47
|
+
mkdir -p .opencode/skills/read-specs
|
|
48
|
+
cp "$SKILL_SRC" .opencode/skills/read-specs/SKILL.md
|
|
49
|
+
echo "Installed skill to .opencode/skills/read-specs/"
|
|
50
|
+
;;
|
|
51
|
+
3)
|
|
52
|
+
mkdir -p .claude/skills/read-specs .opencode/skills/read-specs
|
|
53
|
+
cp "$SKILL_SRC" .claude/skills/read-specs/SKILL.md
|
|
54
|
+
cp "$SKILL_SRC" .opencode/skills/read-specs/SKILL.md
|
|
55
|
+
echo "Installed skill to .claude/skills/read-specs/ and .opencode/skills/read-specs/"
|
|
56
|
+
;;
|
|
57
|
+
*)
|
|
58
|
+
echo "Cancelled."
|
|
59
|
+
exit 1
|
|
60
|
+
;;
|
|
61
|
+
esac
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
echo ""
|
|
65
|
+
echo "You can now use /read-specs in your AI coding agent."
|
|
66
|
+
;;
|
|
67
|
+
|
|
68
|
+
export)
|
|
69
|
+
SOURCE_DIR="${2:-.}"
|
|
70
|
+
OUTPUT_DIR="${3:-spec-site}"
|
|
71
|
+
|
|
72
|
+
echo "Exporting spec site from $SOURCE_DIR to $OUTPUT_DIR..."
|
|
73
|
+
|
|
74
|
+
mkdir -p "$OUTPUT_DIR"
|
|
75
|
+
|
|
76
|
+
# Copy spec files
|
|
77
|
+
cd "$SOURCE_DIR"
|
|
78
|
+
find . -type f \( -name '*.md' -o -name '*.yaml' -o -name '*.yml' \) | while read -r f; do
|
|
79
|
+
f="${f#./}"
|
|
80
|
+
[[ "$f" == .git/* ]] && continue
|
|
81
|
+
[[ "$f" == */.build/* ]] && continue
|
|
82
|
+
[[ "$f" == */node_modules/* ]] && continue
|
|
83
|
+
mkdir -p "$OUTPUT_DIR/$(dirname "$f")"
|
|
84
|
+
cp "$f" "$OUTPUT_DIR/$f"
|
|
85
|
+
done
|
|
86
|
+
cd - > /dev/null
|
|
87
|
+
|
|
88
|
+
# Generate manifest
|
|
89
|
+
"$SCRIPT_DIR/read-specs" --manifest-only "$SOURCE_DIR"
|
|
90
|
+
mv "$SOURCE_DIR/manifest.json" "$OUTPUT_DIR/manifest.json"
|
|
91
|
+
|
|
92
|
+
# Copy viewer as index.html
|
|
93
|
+
cp "$SCRIPT_DIR/spec-viewer.html" "$OUTPUT_DIR/index.html"
|
|
94
|
+
|
|
95
|
+
echo "Spec site exported to $OUTPUT_DIR/"
|
|
96
|
+
echo " Files: $(find "$OUTPUT_DIR" -type f | wc -l | tr -d ' ')"
|
|
97
|
+
echo " Serve: cd $OUTPUT_DIR && python3 -m http.server 3333"
|
|
98
|
+
;;
|
|
99
|
+
|
|
100
|
+
*)
|
|
101
|
+
# Serve mode — serve from the given directory
|
|
102
|
+
exec "$SCRIPT_DIR/read-specs" "${1:-.}"
|
|
103
|
+
;;
|
|
104
|
+
esac
|