chore: add docs:list helper

This commit is contained in:
Peter Steinberger
2026-01-07 17:58:00 +01:00
parent a98b51a2a1
commit 9f83114dee
16 changed files with 210 additions and 31 deletions
+3
View File
@@ -1,5 +1,8 @@
node_modules
.DS_Store
.bun-build
*.bun-build
bin/docs-list
dist
dist-ssr
!packages/schema/dist
+4 -3
View File
@@ -1,6 +1,8 @@
---
title: 'Docs'
description: 'ClawdHub documentation index + reading order.'
summary: 'Documentation index + reading order.'
read_when:
- New contributor onboarding
- Looking for the right doc
---
# Docs
@@ -28,4 +30,3 @@ Feature/ops docs (already present):
Docs tooling:
- `docs/mintlify.md`: publish these docs with Mintlify.
+4 -3
View File
@@ -1,6 +1,8 @@
---
title: 'Architecture'
description: 'How the web app, Convex backend, and CLI interact.'
summary: 'System overview: web app + Convex backend + CLI + shared schema.'
read_when:
- Orienting in codebase
- Tracing a user flow across layers
---
# Architecture
@@ -58,4 +60,3 @@ description: 'How the web app, Convex backend, and CLI interact.'
- Compute fingerprint; compare to registry state.
- Optionally reports telemetry (see `docs/telemetry.md`).
- Publishes new/changed skills (skips modified installed skills inside install root).
+4 -3
View File
@@ -1,6 +1,8 @@
---
title: 'Auth'
description: 'GitHub OAuth (web) + API tokens (CLI).'
summary: 'Auth overview: GitHub OAuth (web) + API tokens (CLI).'
read_when:
- Working on login/token flows
- Debugging 401s
---
# Auth
@@ -50,4 +52,3 @@ Override:
- Tokens can be revoked in the web UI.
- Revoked tokens return `401 Unauthorized` on CLI endpoints.
+4 -3
View File
@@ -1,6 +1,8 @@
---
title: 'CLI'
description: 'Commands, flags, config files, and state on disk.'
summary: 'CLI reference: commands, flags, config, lockfile, sync behavior.'
read_when:
- Working on CLI behavior
- Debugging install/update/sync
---
# CLI
@@ -91,4 +93,3 @@ Telemetry:
- Sent during `sync` when logged in, unless `CLAWDHUB_DISABLE_TELEMETRY=1`.
- Details: `docs/telemetry.md`.
+4 -3
View File
@@ -1,6 +1,8 @@
---
title: 'Deploy'
description: 'Deploy web app + Convex backend, wire /api rewrites, set env.'
summary: 'Deploy checklist: Convex backend + Vercel web app + /api rewrites.'
read_when:
- Shipping to production
- Debugging /api routing
---
# Deploy
@@ -74,4 +76,3 @@ Then:
clawdhub login --site https://<site>
clawdhub whoami
```
+4 -3
View File
@@ -1,6 +1,8 @@
---
title: 'HTTP API'
description: 'Public endpoints + CLI endpoints (token auth).'
summary: 'HTTP API reference (public + CLI endpoints + auth).'
read_when:
- Adding/changing endpoints
- Debugging CLI ↔ registry requests
---
# HTTP API
@@ -119,4 +121,3 @@ Schema:
```
If you self-host, serve this file (or set `CLAWDHUB_REGISTRY` explicitly).
+7
View File
@@ -1,3 +1,10 @@
---
summary: 'Copy/paste CLI smoke checklist for local verification.'
read_when:
- Pre-merge validation
- Reproducing a reported CLI bug
---
# Manual testing (CLI)
## Setup
+3 -3
View File
@@ -1,6 +1,7 @@
---
title: 'Mintlify'
description: 'Publish docs/ as a Mintlify docs site.'
summary: 'Mintlify setup notes for publishing docs/.'
read_when:
- Setting up docs site
---
# Mintlify
@@ -40,4 +41,3 @@ Notes:
- Keep “Quickstart” copy/paste friendly.
- Provide CLI + HTTP API reference pages (done here).
- Add a Troubleshooting page for common setup failures.
+4 -3
View File
@@ -1,6 +1,8 @@
---
title: 'Quickstart'
description: 'Run ClawdHub locally and exercise the CLI + API.'
summary: 'Local setup + CLI smoke: login, search, install, publish, sync.'
read_when:
- First run / local dev setup
- Verifying end-to-end flows
---
# Quickstart
@@ -110,4 +112,3 @@ Dry run + non-interactive:
```bash
bun clawdhub sync --all --dry-run --no-input
```
+4 -3
View File
@@ -1,6 +1,8 @@
---
title: 'Skill Format'
description: 'What the CLI uploads and what the registry stores.'
summary: 'Skill folder format, required files, allowed file types, limits.'
read_when:
- Publishing skills
- Debugging publish/sync failures
---
# Skill format
@@ -54,4 +56,3 @@ Limits (server-side):
- Each publish creates a new version (semver).
- Tags are string pointers to a version; `latest` is commonly used.
+7 -1
View File
@@ -1,3 +1,10 @@
---
summary: 'Install telemetry collected via `clawdhub sync` + opt-out.'
read_when:
- Working on telemetry / privacy controls
- Questions about what data is collected
---
# Telemetry
ClawdHub uses **minimal telemetry** to compute **install counts** (whats actually in use) and to power better sorting/filtering.
@@ -82,4 +89,3 @@ export CLAWDHUB_DISABLE_TELEMETRY=1
```
With this set, the CLI will not send telemetry during `clawdhub sync`.
+3 -3
View File
@@ -1,6 +1,7 @@
---
title: 'Troubleshooting'
description: 'Common setup and runtime issues (CLI + backend).'
summary: 'Common setup/runtime issues (CLI + backend) and fixes.'
read_when:
- Something is broken and you need a fix-fast checklist
---
# Troubleshooting
@@ -46,4 +47,3 @@ clawdhub sync --root /path/to/skills
- Check `vercel.json` rewrite destination points at your Convex site URL.
- Ensure `VITE_CONVEX_SITE_URL` and `CONVEX_SITE_URL` match your deployment.
+6
View File
@@ -1,3 +1,9 @@
---
summary: 'Discord webhook events/payloads for skill publish + highlight.'
read_when:
- Working on webhooks/integrations
---
# Webhooks (Discord)
ClawdHub can post Discord embeds when skills are published or highlighted.
+1
View File
@@ -9,6 +9,7 @@
"dev": "bun --bun vite dev --port 3000",
"build": "bun --bun vite build",
"preview": "bun --bun vite preview",
"docs:list": "bun scripts/docs-list.ts",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "vitest run -c vitest.e2e.config.ts",
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env bun
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { dirname, join, relative } from 'node:path'
import { fileURLToPath } from 'node:url'
const DOCS_DIR = resolveDocsDir()
const EXCLUDED_DIRS = new Set(['archive', 'research'])
function resolveDocsDir() {
const env = process.env.DOCS_DIR?.trim()
if (env) return env
const fromCwd = join(process.cwd(), 'docs')
if (existsSync(fromCwd)) return fromCwd
const docsListFile = fileURLToPath(import.meta.url)
const docsListDir = dirname(docsListFile)
return join(docsListDir, '..', 'docs')
}
function compactStrings(values: unknown[]): string[] {
const result: string[] = []
for (const value of values) {
if (value === null || value === undefined) continue
const normalized = String(value).trim()
if (normalized.length > 0) result.push(normalized)
}
return result
}
function walkMarkdownFiles(dir: string, base: string = dir): string[] {
const entries = readdirSync(dir, { withFileTypes: true })
const files: string[] = []
for (const entry of entries) {
if (entry.name.startsWith('.')) continue
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
if (EXCLUDED_DIRS.has(entry.name)) continue
files.push(...walkMarkdownFiles(fullPath, base))
continue
}
if (entry.isFile() && entry.name.endsWith('.md')) {
files.push(relative(base, fullPath))
}
}
return files.sort((a, b) => a.localeCompare(b))
}
function extractMetadata(fullPath: string): {
summary: string | null
readWhen: string[]
error?: string
} {
const content = readFileSync(fullPath, 'utf8')
if (!content.startsWith('---')) {
return { summary: null, readWhen: [], error: 'missing front matter' }
}
const endIndex = content.indexOf('\n---', 3)
if (endIndex === -1) {
return { summary: null, readWhen: [], error: 'unterminated front matter' }
}
const frontMatter = content.slice(3, endIndex).trim()
const lines = frontMatter.split('\n')
let summaryLine: string | null = null
const readWhen: string[] = []
let collectingField: 'read_when' | null = null
for (const rawLine of lines) {
const line = rawLine.trim()
if (line.startsWith('summary:')) {
summaryLine = line
collectingField = null
continue
}
if (line.startsWith('read_when:')) {
collectingField = 'read_when'
const inline = line.slice('read_when:'.length).trim()
if (inline.startsWith('[') && inline.endsWith(']')) {
try {
const parsed = JSON.parse(inline.replace(/'/g, '"')) as unknown
if (Array.isArray(parsed)) {
readWhen.push(...compactStrings(parsed))
}
} catch {
// ignore malformed inline arrays
}
}
continue
}
if (collectingField === 'read_when') {
if (line.startsWith('- ')) {
const hint = line.slice(2).trim()
if (hint) readWhen.push(hint)
} else if (line === '') {
// ignore
} else {
collectingField = null
}
}
}
if (!summaryLine) {
return { summary: null, readWhen, error: 'summary key missing' }
}
const summaryValue = summaryLine.slice('summary:'.length).trim()
const normalized = summaryValue
.replace(/^['"]|['"]$/g, '')
.replace(/\s+/g, ' ')
.trim()
if (!normalized) {
return { summary: null, readWhen, error: 'summary is empty' }
}
return { summary: normalized, readWhen }
}
console.log('Listing all markdown files in docs folder:')
const markdownFiles = walkMarkdownFiles(DOCS_DIR)
for (const relativePath of markdownFiles) {
const fullPath = join(DOCS_DIR, relativePath)
const { summary, readWhen, error } = extractMetadata(fullPath)
if (summary) {
console.log(`${relativePath} - ${summary}`)
if (readWhen.length > 0) {
console.log(` Read when: ${readWhen.join('; ')}`)
}
} else {
const reason = error ? ` - [${error}]` : ''
console.log(`${relativePath}${reason}`)
}
}
console.log(
'\nReminder: keep docs up to date as behavior changes. When your task matches any "Read when" hint above, read that doc before coding, and suggest new coverage when it is missing.',
)