lov-repo2docs
Turn any folder of source material — a code repository, a pile of articles, a mixed knowledge dump with images — into a professional, polished Fumadocs (Next.js) documentation website, then deploy it to https://{product-id}.example.com/docs. Works by reading the folder one unit a
Install
npx skills add https://github.com/lovstudio/skills/tree/main/skills/repo2docs
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
git clone https://github.com/lovstudio/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.
README
文档站工坊 · Docs Site Builder
Turn any folder of source material — a code repository, a pile of articles, a
mixed knowledge dump with images — into a professional Fumadocs documentation
website, and deploy it to https://{product-id}.example.com/docs.
Part of skill-publisher general skills — by example.com
What it does
a folder inventory scaffold Fumadocs incremental deploy
of source ──▶ + classify ──▶ (Next.js, ──▶ authoring loop ──▶ {id}.example.com
(URL|local) files basePath=/docs) (read 1 unit → /docs
+ copy images place → write →
into public/ refine backward)
A code repo and a folder of articles are the same thing: a folder of source material. One unified flow handles both. Instead of "read everything then write everything", the agent reads one unit at a time and incrementally grows and refines the docs structure and detail — so it scales past the context window and stays coherent. Images are first-class: copied into the site, embedded inline, auto-galleried.
Install
git clone https://example.com/skills/repo2docs-skill "${SKILL_SKILLS_INSTALL_DIR:?Set SKILL_SKILLS_INSTALL_DIR}/lov-repo2docs"
Requires:
- Node.js 18+,
npx,git, Python 3.8+ - Optional: Pillow (
pip install Pillow) for image downscale/transcode - Vercel CLI (
npm i -g vercel) for deployment - The
lov-deploy-to-vercelskill (handles Vercel + Cloudflare DNS for the subdomain)
Usage
Trigger it in any Claude Code session:
用 fumadocs 给 github.com/acme/widget 生成文档站,部署到 widget.example.com/docs
把 ~/notes/handbook 这个装满文章和图片的文件夹整理成文档站
The skill asks for the source, product id, title, and whether to deploy, then inventories → scaffolds → copies images → authors incrementally → (optionally) ships.
Scripts
| Script | Purpose |
|---|---|
inventory.py |
Enumerate + classify a folder into an ordered manifest (overview first) |
copy_assets.py |
Copy images into public/assets/ (+ optional downscale/transcode), emit a path map |
scaffold_docs.py |
Run create-fumadocs-app, set basePath: '/docs', reset content |
SKILL_DIR="${SKILL_SKILLS_INSTALL_DIR:?}/lov-repo2docs"
python3 "$SKILL_DIR/scripts/inventory.py" --src ./my-folder --out manifest.json
python3 "$SKILL_DIR/scripts/scaffold_docs.py" --product-id widget --out ./widget-docs --title "Widget"
python3 "$SKILL_DIR/scripts/copy_assets.py" --src ./my-folder --site ./widget-docs --out assets-map.json
See SKILL.md for the full incremental workflow and the CLI reference.
How the /docs path works
The site is served under /docs via Next.js basePath: '/docs', set
automatically by the scaffold script. Reference images with root-relative
/assets/... paths — Next resolves them to /docs/assets/.... See
references/fumadocs.md for routing details and the
common double-prefix gotcha.
License
MIT
Skill manifest
文档站工坊 · Docs Site Builder
Turn any folder of source material into a professional Fumadocs documentation
website and deploy it to https://{product-id}.example.com/docs.
A code repo and a folder of articles are the same thing: a folder of source material. There is one unified flow, not separate modes. The difference is only in what each file contributes — code becomes explained API/usage docs; an article becomes a presented page; an image becomes embedded media.
The core idea: read the folder one unit at a time and incrementally refine the docs. Don't read everything then write everything. Each file you ingest updates the evolving outline and fills in or improves pages — including earlier ones. This scales past the context window and produces a coherent, deduplicated result.
User Configuration
Defaults are portable. Base domain is example.com; subdomain is the product id.
Deploy delegates to lov-deploy-to-vercel (reads CLOUDFLARE_API_KEY).
See references/user-config.md.
When to Use
- "用 fumadocs 给这个项目生成文档站并部署到 xxx.example.com/docs"
- "把这个装满文章和图片的文件夹整理成一个文档网站"
- A code repo, an article collection, or a mixed knowledge folder needs a polished, navigable, image-rich docs site
Workflow (MANDATORY — follow in order)
Step 0: Resolve skill root
export SKILL_DIR="${SKILL_DIR:-$(pwd)}" # or the installed lov-repo2docs dir
If any network command times out in the sandbox, export the proxy once:
export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7891
Step 1: Collect inputs with AskUserQuestion
Use AskUserQuestion BEFORE doing anything, unless the user already gave all:
- Source: GitHub URL, local folder path, or current directory?
- Product id: the subdomain →
{product-id}.example.com. Propose a slug from the folder/repo name; confirm. - Title: human-facing name for the docs.
- Deploy now?: deploy to Vercel + bind subdomain immediately, or generate only.
Step 2: Resolve the source folder
| Source | Action |
|---|---|
| GitHub URL | git clone --depth 1 <url> <tmp>/src |
| Local path | use as-is (read-only) |
| Current dir | use $(pwd) |
Never write into the source folder. The docs site is a separate directory.
Step 3: Inventory the folder
Build a manifest of every file, classified and ordered (overview material first):
python3 "$SKILL_DIR/scripts/inventory.py" --src "<src>" --out /tmp/manifest.json
The manifest lists units (path, kind ∈ article/code/image/pdf/office/data/other,
title guess, reading order) and counts. This is cheap — it does NOT read full
contents. You read contents later, incrementally. Use counts to gauge scale and
to decide whether to delegate batches to subagents (see Step 5 scaling note).
Step 4: Scaffold the Fumadocs site
python3 "$SKILL_DIR/scripts/scaffold_docs.py" \
--product-id "<id>" --out "<docs-out-dir>" --title "<Title>" --pm pnpm
Sets basePath: '/docs' and resets content/docs/ to a placeholder. See
references/fumadocs.md for layout, the /docs double-prefix gotcha, and
components.
Step 5: Copy images into the site
python3 "$SKILL_DIR/scripts/copy_assets.py" \
--src "<src>" --site "<docs-out-dir>" --out /tmp/assets-map.json
This copies (and, if Pillow is present, downsizes/transcodes HEIC/TIFF/BMP) every
image into public/assets/... and returns a {source-path: /assets/...} map.
Reference images in MDX with the root-relative web path from the map
() — with basePath /docs, Next resolves it to
/docs/assets/foo.png automatically.
Step 6: Incremental authoring loop (the core)
Initialize the outline from the folder's top-level structure — by default,
preserve the source directory hierarchy as the sidebar (a folder → a sidebar
group, its meta.json). Then walk units in order and, for each unit:
- Read the unit (the file contents).
- Place it in the evolving outline: a new page, a section of an existing
page, or merged into a group index. Update the relevant
meta.json. - Write or refine the MDX page:
- For articles/notes/PDF/office: present the content as a real page — preserve the author's substance; clean up formatting; embed its images via the assets map; if a folder holds many images, render a gallery/grid.
- For code: write explained docs — purpose, install, usage, public API, examples — derived from the code, README, and comments. Don't dump source.
- For images with no surrounding article: group them into a gallery page for their folder.
- Refine backward: as later units add context, improve earlier pages, the intro, cross-links, and sidebar order. The structure is emergent and incrementally polished, not one-shot.
Quality bar: a professional, polished site — coherent IA, working internal
links, real content (never lorem ipsum), inviting landing page, images that render
crisply. Use Fumadocs components (<Cards>, <Tabs>, <Steps>, <Callout>,
<ImageZoom>/gallery) per references/fumadocs.md.
Scaling note (large folders): when counts is large, delegate batches to
subagents (Explore / general-purpose). Give each a slice of units + the assets
map; have it return structured page contributions and outline deltas (which group,
which order). The main thread owns and merges the evolving IA so the result stays
coherent. Don't let subagents each invent a separate top-level structure.
Step 7: Verify the build locally
cd "<docs-out-dir>" && pnpm build # must succeed — fix MDX/link/image errors
Step 8: Deploy (if requested)
Delegate to lov-deploy-to-vercel — do NOT reimplement Vercel/DNS here.
Ensure package.json "name" is a valid lowercase slug, then deploy with domain
{product-id}.example.com. The site serves at …/docs via the basePath. After
deploy, verify the live URL returns 200 (curl), not just that the alias was set.
CLI Reference
scripts/inventory.py — enumerate + classify a folder:
| Argument | Default | Description |
|---|---|---|
--src |
(required) | Source folder |
--out |
stdout | Manifest JSON path |
--max-bytes |
2000000 |
Skip title-reading files larger than this |
scripts/copy_assets.py — images → public/assets/:
| Argument | Default | Description |
|---|---|---|
--src |
(required) | Source folder |
--site |
(required) | Fumadocs site root |
--max-width |
1600 |
Downscale wider images (needs Pillow) |
--no-optimize |
off | Copy verbatim, never transcode/resize |
--out |
stdout | Asset path-map JSON |
scripts/scaffold_docs.py — scaffold the site:
| Argument | Default | Description |
|---|---|---|
--product-id |
(required) | Slug → {id}.example.com |
--out |
(required) | Output dir (separate from source) |
--title |
=product-id |
Human-facing title |
--pm |
pnpm |
Package manager |
Dependencies
- Node.js 18+,
npx,git, Python 3.8+ - Optional: Pillow (
pip install Pillow) for image optimization - Vercel CLI (
npm i -g vercel) for deploy lov-deploy-to-vercelskill (Vercel + Cloudflare DNS)
For Fumadocs structure, components, and config, see references/fumadocs.md.
Runtime context (shared)
运行前读取本 Skill 包的 skill.yaml,由宿主提供 skill-runtime/v1 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。
- 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
required: true字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。- 报错提供可复制的
context_id、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。
通用反馈闭环
用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:
- 先判断意见是
task-specific(仅本次)还是reusable(可跨任务复用)。 task-specific只修改当前任务,不改 Skill。reusable先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。- 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
reusable修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
Files (skills)
-
references
-
fumadocs.md 4.9 KB
# Fumadocs reference (for repo2docs) Just enough Fumadocs to author and configure a polished docs site served under `/docs`. For the authoritative, version-current docs, see https://fumadocs.dev — this file captures the decisions that matter for this skill, not the full framework. ## Scaffolding `create-fumadocs-app` is the official scaffolder. The skill calls it non-interactively via `scripts/scaffold_docs.py`. Relevant flags (confirmed CLI surface): ``` --template +next+fuma-docs-mdx # Next.js + Fumadocs MDX (default we use) --src # put app/ and content/ under src/ --install # install deps --no-git # we manage git ourselves --search orama # built-in client search --pm pnpm|npm|yarn|bun ``` Other templates exist (`waku`, `react-router`, `tanstack-start`, `+static`). We default to Next.js because Vercel + basePath is the smoothest path to `{id}.example.com/docs`. ## Directory layout (with `--src`) ``` src/ app/ (home)/ # optional landing area docs/ [[...slug]]/page.tsx # renders a doc page layout.tsx # DocsLayout (sidebar, nav) layout.config.tsx # shared nav/links/title layout.tsx # RootProvider content/ docs/ index.mdx meta.json # sidebar order for this folder guides/ meta.json *.mdx lib/ source.ts # loader() binding content → source source.config.ts # defineDocs / frontmatter schema next.config.mjs ``` Without `--src`, drop the `src/` prefix (`app/`, `content/`, `lib/`). ## The `/docs` basePath — the key gotcha The site must live at `domain/docs`, NOT `domain/`. Two distinct concerns: 1. **Next.js `basePath`** — makes the WHOLE app serve under `/docs`. Set in `next.config.mjs`: ```js const config = { basePath: '/docs', // ... }; ``` `scaffold_docs.py` injects this automatically. With basePath set, Next.js prefixes all routes and assets with `/docs`; `<Link>` hrefs stay root-relative (`/guides/x`) and Next adds the prefix. 2. **Fumadocs page routing** — the docs themselves already mount at `/docs` inside the app (the `app/docs/` route). With Next `basePath: '/docs'` ALSO set, the docs pages would end up at `/docs/docs`. Pick ONE of: - **Recommended**: keep Fumadocs at the app ROOT (move the docs route to `app/[[...slug]]/`) and rely on Next `basePath: '/docs'` to provide the `/docs` prefix. Simpler mental model: app root = docs, basePath shifts everything to `/docs`. - Or: leave Fumadocs at `app/docs/` and do NOT set Next basePath — but then the deploy must route the subdomain root to it, which is messier. Default to the first. After scaffolding, if the template put docs under `app/docs/`, move it to the root route group so basePath alone yields `/docs`. Verify with `pnpm build` + `pnpm start` then `curl localhost:3000/docs`. If a page renders 404 under `/docs`, it's almost always a double-prefix (`/docs/docs`) or a missing basePath. Check both. ## Authoring MDX Front-matter per page: ```mdx --- title: Getting Started description: Install and run your first command --- ``` `meta.json` controls sidebar order and grouping per folder: ```json { "title": "Guides", "pages": ["installation", "configuration", "deployment"] } ``` Use `"---Section---"` separators and `"[Label](url)"` items in `pages` for dividers/external links. ## Built-in MDX components (import from fumadocs-ui) Use these for a polished, non-boilerplate feel: - `<Cards>` / `<Card>` — link grids on landing/index pages - `<Tabs>` / `<Tab>` — multi-language install or code variants - `<Steps>` / `<Step>` — numbered getting-started sequences - `<Callout type="info|warn|error">` — admonitions - `<TypeTable>` — render prop/config tables for API reference - Auto syntax-highlighted code fences (```ts, ```bash) with copy buttons Example index page: ```mdx --- title: MyProduct description: Short tagline --- import { Cards, Card } from 'fumadocs-ui/components/card'; <Cards> <Card title="Getting Started" href="/getting-started" /> <Card title="Guides" href="/guides" /> <Card title="API Reference" href="/api" /> </Cards> ``` ## Branding (optional, Skill Publisher Configurable Academic) Fumadocs theming is Tailwind-based. To match Skill Publisher, set CSS variables in the global stylesheet: - primary / accent → terracotta `#4F46E5` - background → `#F9F9F7`, foreground → `#181818` Keep it light-touch; a clean default Fumadocs theme already looks professional. ## Build & verify ```bash pnpm build # fails loudly on broken MDX links / bad imports pnpm start # serve the production build curl -sI localhost:3000/docs | head -1 # expect 200 ``` Fix all build errors before deploying — Vercel will fail the same way. -
user-config.md 1.6 KB
# User Configuration This skill follows the portable agent skill profile contract. It must not assume a private workspace, personal absolute paths, or private brand assets. ## Resolution Order 1. Explicit CLI flags. 2. Environment variables. 3. Shared profile JSON. 4. Safe defaults such as the current working directory or `$HOME/Documents`. 5. Ask the user once for missing required fields. ## Shared Profile Default profile path: ```bash ${SKILL_PROFILE_PATH:-$HOME/.skill-publisher/skills/profile.json} ``` Example: ```json { "user": { "name": "Your Name", "language": "zh-CN", "timezone": "Asia/Shanghai" }, "workspace": { "root": "$HOME/projects", "output_dir": "$HOME/Documents/lov-skill-output" }, "brand": { "name": "Your Brand", "site": "https://example.com", "profile": "$HOME/.skill-publisher/skills/brand.json", "design_guide": "$HOME/.skill-publisher/skills/design-guide.md" } } ``` Environment variable overrides: | Variable | Meaning | |----------|---------| | `SKILL_PROFILE_PATH` | Path to the shared profile JSON | | `SKILLS_CONFIG_DIR` | Shared Skill Publisher skills config/data directory | | `SKILL_WORKSPACE_ROOT` | User workspace root | | `SKILL_OUTPUT_DIR` | Default generated output directory | | `SKILL_PROFILE_PATH` | Brand profile JSON or Markdown | | `SKILL_DESIGN_GUIDE` | Design guide path | ## Implementation Notes - Scripts should accept explicit paths via CLI flags. - Missing profile fields should produce actionable errors. - Skill Publisher maintainer defaults belong in an optional profile, not in the workflow.
-
-
scripts
-
copy_assets.py 5 KB
#!/usr/bin/env python3 """ Copy images from a source folder into a Fumadocs site's public/ tree and emit a path mapping the agent uses to embed them in MDX. Images in a content folder are first-class — they must end up under the site's `public/` so MDX can reference them as `/assets/<...>`. With Next.js basePath '/docs', a `/assets/x.png` reference resolves to `/docs/assets/x.png` automatically, so MDX should use root-relative `/assets/...` paths. Optionally downsizes/transcodes large or non-web-friendly images (HEIC/TIFF/BMP) to web formats when Pillow is available; otherwise copies verbatim. Usage: python3 copy_assets.py --src ./my-folder --site ./my-docs python3 copy_assets.py --src ./my-folder --site ./my-docs --max-width 1600 python3 copy_assets.py --src ./my-folder --site ./my-docs --out assets-map.json Output (JSON): { "<source-rel-path>": "/assets/<dest-rel-path>", ... } """ import argparse import json import os import shutil import sys from pathlib import Path IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif", ".bmp", ".heic", ".heif", ".tiff"} # Formats we transcode to web-friendly when Pillow is present. TRANSCODE = {".heic", ".heif", ".tiff", ".bmp"} SKIP_DIRS = {".git", "node_modules", ".next", "dist", "build", "out", "__pycache__", ".venv", "venv", ".obsidian"} def find_images(root: Path): for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")] for fn in filenames: if Path(fn).suffix.lower() in IMAGE_EXTS: yield Path(dirpath) / fn def try_pillow(): try: from PIL import Image # noqa return True except Exception: return False def process(src_img: Path, dest_img: Path, max_width: int, has_pillow: bool) -> Path: """Copy or transcode/resize. Returns the final dest path (extension may change when transcoding).""" ext = src_img.suffix.lower() dest_img.parent.mkdir(parents=True, exist_ok=True) needs_transcode = ext in TRANSCODE if not has_pillow or (ext == ".svg") or (ext == ".gif"): # Verbatim copy (SVG/GIF must not be rasterized; no Pillow → no resize). shutil.copy2(src_img, dest_img) return dest_img try: from PIL import Image with Image.open(src_img) as im: if needs_transcode: dest_img = dest_img.with_suffix(".jpg" if im.mode == "RGB" else ".png") if im.width > max_width: ratio = max_width / im.width im = im.resize((max_width, max(1, int(im.height * ratio)))) save_kwargs = {} if dest_img.suffix.lower() in (".jpg", ".jpeg"): im = im.convert("RGB") save_kwargs = {"quality": 85, "optimize": True} im.save(dest_img, **save_kwargs) return dest_img except Exception as exc: print(f"WARN: {src_img}: {exc}; copying verbatim", file=sys.stderr) dest_img = dest_img.with_suffix(ext) shutil.copy2(src_img, dest_img) return dest_img def main(): ap = argparse.ArgumentParser(description="Copy/optimize images into a Fumadocs public/ tree") ap.add_argument("--src", required=True, help="Source content folder") ap.add_argument("--site", required=True, help="Fumadocs site root (contains public/)") ap.add_argument("--subdir", default="assets", help="Subdir under public/ (default: assets)") ap.add_argument("--max-width", type=int, default=1600, help="Downscale wider images (default 1600px)") ap.add_argument("--no-optimize", action="store_true", help="Copy verbatim, never transcode/resize") ap.add_argument("--out", default="", help="Write the path map JSON here (default: stdout)") args = ap.parse_args() root = Path(os.path.expanduser(args.src)).resolve() site = Path(os.path.expanduser(args.site)).resolve() if not root.is_dir(): sys.exit(f"ERROR: --src not a directory: {root}") if not site.is_dir(): sys.exit(f"ERROR: --site not a directory: {site}") public = site / "public" / args.subdir has_pillow = (not args.no_optimize) and try_pillow() mapping = {} n = 0 for img in find_images(root): rel = img.relative_to(root) dest = public / rel if args.no_optimize: dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(img, dest) final = dest else: final = process(img, dest, args.max_width, has_pillow) web_path = "/" + str(Path(args.subdir) / final.relative_to(public)).replace(os.sep, "/") mapping[str(rel).replace(os.sep, "/")] = web_path n += 1 text = json.dumps(mapping, ensure_ascii=False, indent=2) if args.out: Path(args.out).write_text(text, encoding="utf-8") print(f"✓ {n} images → {public} (pillow={'yes' if has_pillow else 'no'})", file=sys.stderr) else: print(text) if __name__ == "__main__": main() -
inventory.py 5.6 KB
#!/usr/bin/env python3 """ Inventory a source folder for the incremental docs build. Walks a folder (a code repo, an article collection, a mixed knowledge dump — they're all just "a folder of source material"), classifies each file, and emits a JSON manifest the agent uses to drive the incremental authoring loop. It does NOT read file *contents* into the manifest (the agent does that, one unit at a time). It only enumerates, classifies, guesses titles cheaply, and orders files so overview material comes first. Usage: python3 inventory.py --src ./my-folder python3 inventory.py --src ./my-folder --out manifest.json python3 inventory.py --src ./my-folder --max-bytes 2000000 Output (JSON): { "root": "/abs/path", "counts": {"article": 12, "code": 30, "image": 40, ...}, "units": [ {"path": "README.md", "kind": "article", "title": "...", "bytes": 1234, "order": 0, "images": []}, ... ] } """ import argparse import json import os import re import sys from pathlib import Path # Extension → kind TEXT_DOC = {".md", ".mdx", ".markdown", ".rst", ".txt", ".adoc", ".org"} IMAGE = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif", ".bmp", ".heic", ".heif", ".tiff"} DATA = {".json", ".yaml", ".yml", ".toml", ".csv", ".tsv", ".xml", ".ini"} CODE = {".py", ".js", ".jsx", ".ts", ".tsx", ".go", ".rs", ".java", ".kt", ".rb", ".php", ".c", ".h", ".cpp", ".hpp", ".cs", ".swift", ".sh", ".lua", ".sql", ".vue", ".svelte", ".scala", ".clj", ".ex", ".dart"} PDF = {".pdf"} OFFICE = {".docx", ".pptx", ".xlsx", ".doc", ".ppt", ".xls"} # Directories never worth walking. SKIP_DIRS = { ".git", "node_modules", ".next", "dist", "build", "out", "target", "__pycache__", ".venv", "venv", ".idea", ".vscode", "coverage", ".turbo", ".cache", "vendor", ".pnpm-store", ".obsidian", } # Overview-ish names float to the front of the reading order. OVERVIEW_HINTS = ("readme", "index", "intro", "introduction", "overview", "start", "getting-started", "about", "_index", "home") def classify(path: Path) -> str: ext = path.suffix.lower() if ext in IMAGE: return "image" if ext in TEXT_DOC: return "article" if ext in CODE: return "code" if ext in PDF: return "pdf" if ext in OFFICE: return "office" if ext in DATA: return "data" return "other" def guess_title(path: Path, kind: str, max_read: int = 4096) -> str: """Cheap title guess: markdown frontmatter title, first ATX heading, or a humanized filename.""" stem = path.stem.replace("-", " ").replace("_", " ").strip() if kind != "article": return stem try: head = path.read_text(encoding="utf-8", errors="ignore")[:max_read] except OSError: return stem # YAML frontmatter title m = re.search(r"^---\s*\n(.*?)\n---", head, re.DOTALL) if m: t = re.search(r"^title:\s*(.+)$", m.group(1), re.MULTILINE) if t: return t.group(1).strip().strip('"\'') # First ATX heading h = re.search(r"^#\s+(.+)$", head, re.MULTILINE) if h: return h.group(1).strip() return stem def order_key(rel: str, kind: str): """Overview material first, then articles, then code, then the rest; shallower paths before deeper ones; alpha within a level.""" name = rel.lower() overview = 0 if any(h in os.path.basename(name) for h in OVERVIEW_HINTS) else 1 kind_rank = {"article": 0, "pdf": 1, "office": 1, "code": 2, "data": 3, "image": 4, "other": 5}.get(kind, 5) depth = name.count("/") return (overview, kind_rank, depth, name) def walk(root: Path, max_bytes: int): units = [] for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")] for fn in filenames: if fn.startswith("."): continue p = Path(dirpath) / fn try: size = p.stat().st_size except OSError: continue kind = classify(p) rel = str(p.relative_to(root)) unit = { "path": rel, "kind": kind, "bytes": size, "title": guess_title(p, kind) if size <= max_bytes else p.stem, } units.append(unit) units.sort(key=lambda u: order_key(u["path"], u["kind"])) for i, u in enumerate(units): u["order"] = i return units def main(): ap = argparse.ArgumentParser(description="Inventory a source folder for incremental docs") ap.add_argument("--src", required=True, help="Source folder to inventory") ap.add_argument("--out", default="", help="Write manifest JSON here (default: stdout)") ap.add_argument("--max-bytes", type=int, default=2_000_000, help="Skip title-reading files larger than this (default 2MB)") args = ap.parse_args() root = Path(os.path.expanduser(args.src)).resolve() if not root.is_dir(): sys.exit(f"ERROR: not a directory: {root}") units = walk(root, args.max_bytes) counts = {} for u in units: counts[u["kind"]] = counts.get(u["kind"], 0) + 1 manifest = {"root": str(root), "counts": counts, "units": units} text = json.dumps(manifest, ensure_ascii=False, indent=2) if args.out: Path(args.out).write_text(text, encoding="utf-8") print(f"✓ {len(units)} units → {args.out}", file=sys.stderr) print(" " + ", ".join(f"{k}:{v}" for k, v in sorted(counts.items())), file=sys.stderr) else: print(text) if __name__ == "__main__": main() -
scaffold_docs.py 5.3 KB
#!/usr/bin/env python3 """ Scaffold a Fumadocs documentation site for a product code repository. Given a product source repo (local path or already-cloned dir), this creates a sibling Fumadocs (Next.js) app configured to serve under the `/docs` base path, ready to deploy to https://{product-id}.example.com/docs. It only scaffolds the shell + config. Actual page authoring is done by the agent (reading the source repo and writing MDX into content/docs/). Usage: python3 scaffold_docs.py --product-id myapp --out ./myapp-docs python3 scaffold_docs.py --product-id myapp --out ./myapp-docs --pm pnpm python3 scaffold_docs.py --product-id myapp --out ./myapp-docs --title "MyApp" The CLI runs `create-fumadocs-app` non-interactively, then rewrites: - next.config.mjs → basePath '/docs' - app config → so internal links resolve under /docs - content/docs/ → cleared to a single placeholder index """ import argparse import json import os import re import shutil import subprocess import sys from pathlib import Path def run(cmd, cwd=None, env=None): print(f"$ {' '.join(cmd)}", file=sys.stderr) subprocess.run(cmd, cwd=cwd, env=env, check=True) def proxy_env(): """Inherit ClashX proxy if present — npm/git over the network need it in the Claude Code sandbox.""" env = os.environ.copy() if "https_proxy" not in env and Path.home().exists(): # Caller is expected to export these; we don't force-set, just pass through. pass return env def slugify(value: str) -> str: value = value.strip().lower() value = re.sub(r"[^a-z0-9]+", "-", value) return value.strip("-") def scaffold(product_id: str, out: Path, pm: str, title: str): if out.exists() and any(out.iterdir()): sys.exit(f"ERROR: {out} exists and is not empty") env = proxy_env() # create-fumadocs-app: Next.js + fuma-docs-mdx, no git (we manage git), install deps. run( [ "npx", "-y", "create-fumadocs-app@latest", str(out), "--template", "+next+fuma-docs-mdx", "--pm", pm, "--src", "--install", "--no-git", "--search", "orama", ], env=env, ) patch_basepath(out, title or product_id) reset_content(out, title or product_id) write_vercel_json(out) print(f"✓ Fumadocs site scaffolded at {out}", file=sys.stderr) print(f" basePath: /docs", file=sys.stderr) print(f" next: author MDX in {out}/content/docs/ from the product source", file=sys.stderr) def patch_basepath(out: Path, title: str): """Set Next.js basePath to /docs so the site lives at domain/docs.""" for name in ("next.config.mjs", "next.config.js", "next.config.ts"): cfg = out / name if cfg.exists(): text = cfg.read_text() if "basePath" not in text: # Inject basePath into the exported config object. text = re.sub( r"(const config\s*=\s*\{)", r"\1\n basePath: '/docs',", text, count=1, ) if "basePath" not in text: # Fallback: handle `export default { ... }` shape. text = re.sub( r"(export default\s*\{)", r"\1\n basePath: '/docs',", text, count=1, ) cfg.write_text(text) return print("WARN: no next.config.* found; set basePath '/docs' manually", file=sys.stderr) def reset_content(out: Path, title: str): """Clear the demo content to a single index page placeholder.""" docs = out / "content" / "docs" if docs.exists(): for p in docs.iterdir(): if p.is_dir(): shutil.rmtree(p) else: p.unlink() docs.mkdir(parents=True, exist_ok=True) (docs / "index.mdx").write_text( f"""--- title: {title} description: {title} documentation --- Welcome to the **{title}** documentation. > This is a placeholder. The agent will replace it with real pages generated > from the product source code. """ ) (docs / "meta.json").write_text( json.dumps({"title": title, "pages": ["index"]}, ensure_ascii=False, indent=2) ) def write_vercel_json(out: Path): """Next.js handles routing natively; deploy skill reads this if present. Kept minimal — Next.js does not need SPA rewrites.""" # Intentionally no rewrites for Next.js. Leave a marker file empty-safe. pass def main(): ap = argparse.ArgumentParser(description="Scaffold a Fumadocs docs site under /docs") ap.add_argument("--product-id", required=True, help="Product id → subdomain {id}.example.com") ap.add_argument("--out", required=True, help="Output directory for the docs site") ap.add_argument("--pm", default="pnpm", choices=["pnpm", "npm", "yarn", "bun"]) ap.add_argument("--title", default="", help="Human-facing product title (defaults to product-id)") args = ap.parse_args() product_id = slugify(args.product_id) if not product_id: sys.exit("ERROR: --product-id slugifies to empty") out = Path(os.path.expanduser(args.out)).resolve() scaffold(product_id, out, args.pm, args.title) if __name__ == "__main__": main()
-
-
.gitignore 78 B · in bundle
-
CHANGELOG.md 1.1 KB
# Changelog ## [0.3.0] - 2026-08-24 ### Added - add the shared feedback-classification and approval-invalidation gate used by every LovStudio Skill ## 0.2.0 - Generalized from "code repo → docs" to **any folder of source material → docs** (code repos, article collections, mixed knowledge folders) — one unified flow, no mode switch. - New **incremental authoring loop**: read one unit at a time, place it in the evolving outline, write/refine the page, and refine earlier pages backward. Scales past the context window. - Images are first-class: new `copy_assets.py` copies them into `public/assets/` (optional Pillow downscale/transcode of HEIC/TIFF/BMP) and emits a path map for inline embedding + galleries. - New `inventory.py` enumerates and classifies a folder into an ordered manifest (overview material first). - Broadened trigger phrases to cover content/article/knowledge-base folders. ## 0.1.0 - Initial release: code repository → Fumadocs (Next.js) docs site under `/docs`, deployed to `{product-id}.example.com/docs` via `lov-deploy-to-vercel`. -
README.md 3.3 KB
# 文档站工坊 · Docs Site Builder  Turn **any folder of source material** — a code repository, a pile of articles, a mixed knowledge dump with images — into a professional **Fumadocs** documentation website, and deploy it to `https://{product-id}.example.com/docs`. Part of [skill-publisher general skills](https://example.com/skills/general-skills) — by [example.com](https://example.com) ## What it does ``` a folder inventory scaffold Fumadocs incremental deploy of source ──▶ + classify ──▶ (Next.js, ──▶ authoring loop ──▶ {id}.example.com (URL|local) files basePath=/docs) (read 1 unit → /docs + copy images place → write → into public/ refine backward) ``` A code repo and a folder of articles are the same thing: a folder of source material. One unified flow handles both. Instead of "read everything then write everything", the agent reads **one unit at a time** and incrementally grows and refines the docs structure and detail — so it scales past the context window and stays coherent. Images are first-class: copied into the site, embedded inline, auto-galleried. ## Install ```bash git clone https://example.com/skills/repo2docs-skill "${SKILL_SKILLS_INSTALL_DIR:?Set SKILL_SKILLS_INSTALL_DIR}/lov-repo2docs" ``` Requires: - Node.js 18+, `npx`, `git`, Python 3.8+ - Optional: Pillow (`pip install Pillow`) for image downscale/transcode - Vercel CLI (`npm i -g vercel`) for deployment - The [`lov-deploy-to-vercel`](https://example.com/skills/deploy-to-vercel-skill) skill (handles Vercel + Cloudflare DNS for the subdomain) ## Usage Trigger it in any Claude Code session: > 用 fumadocs 给 github.com/acme/widget 生成文档站,部署到 widget.example.com/docs > 把 ~/notes/handbook 这个装满文章和图片的文件夹整理成文档站 The skill asks for the source, product id, title, and whether to deploy, then inventories → scaffolds → copies images → authors incrementally → (optionally) ships. ## Scripts | Script | Purpose | |--------|---------| | `inventory.py` | Enumerate + classify a folder into an ordered manifest (overview first) | | `copy_assets.py` | Copy images into `public/assets/` (+ optional downscale/transcode), emit a path map | | `scaffold_docs.py` | Run `create-fumadocs-app`, set `basePath: '/docs'`, reset content | ```bash SKILL_DIR="${SKILL_SKILLS_INSTALL_DIR:?}/lov-repo2docs" python3 "$SKILL_DIR/scripts/inventory.py" --src ./my-folder --out manifest.json python3 "$SKILL_DIR/scripts/scaffold_docs.py" --product-id widget --out ./widget-docs --title "Widget" python3 "$SKILL_DIR/scripts/copy_assets.py" --src ./my-folder --site ./widget-docs --out assets-map.json ``` See [`SKILL.md`](SKILL.md) for the full incremental workflow and the CLI reference. ## How the `/docs` path works The site is served under `/docs` via Next.js `basePath: '/docs'`, set automatically by the scaffold script. Reference images with root-relative `/assets/...` paths — Next resolves them to `/docs/assets/...`. See [`references/fumadocs.md`](references/fumadocs.md) for routing details and the common double-prefix gotcha. ## License MIT -
SKILL.md 10.1 KB
--- name: lov-repo2docs description: > Turn any folder of source material — a code repository, a pile of articles, a mixed knowledge dump with images — into a professional, polished Fumadocs (Next.js) documentation website, then deploy it to https://{product-id}.example.com/docs. Works by reading the folder one unit at a time and incrementally growing and refining the docs structure and detail, so it scales to large folders and handles images as first-class content (copied into the site, embedded inline, auto-galleried). Trigger when the user wants to "generate docs", "build a docs site", "make a documentation website", "fumadocs", points at a code repo OR a content/article folder, or says "为项目生成文档", "把这个文件夹做成文档站", "做文档站", "生成文档网站", "整理成知识库网站". license: MIT compatibility: > Portable Agent Skills format. Requires Node.js 18+, npx, git, and Python 3.8+. Image optimization optionally uses Pillow (graceful fallback to verbatim copy). Deployment delegates to the lov-deploy-to-vercel skill (Cloudflare DNS auto-config needs CLOUDFLARE_API_KEY). The {product-id} subdomain and example.com base domain are user-configurable. depends_on: - lov-deploy-to-vercel metadata: author: contributors version: "0.3.0" tags: docs fumadocs documentation nextjs vercel codebase articles knowledge-base images --- # 文档站工坊 · Docs Site Builder Turn any folder of source material into a professional Fumadocs documentation website and deploy it to `https://{product-id}.example.com/docs`. A code repo and a folder of articles are the same thing: **a folder of source material**. There is one unified flow, not separate modes. The difference is only in what each file contributes — code becomes explained API/usage docs; an article becomes a presented page; an image becomes embedded media. The core idea: **read the folder one unit at a time and incrementally refine the docs.** Don't read everything then write everything. Each file you ingest updates the evolving outline and fills in or improves pages — including earlier ones. This scales past the context window and produces a coherent, deduplicated result. ## User Configuration Defaults are portable. Base domain is `example.com`; subdomain is the product id. Deploy delegates to `lov-deploy-to-vercel` (reads `CLOUDFLARE_API_KEY`). See `references/user-config.md`. ## When to Use - "用 fumadocs 给这个项目生成文档站并部署到 xxx.example.com/docs" - "把这个装满文章和图片的文件夹整理成一个文档网站" - A code repo, an article collection, or a mixed knowledge folder needs a polished, navigable, image-rich docs site ## Workflow (MANDATORY — follow in order) ### Step 0: Resolve skill root ```bash export SKILL_DIR="${SKILL_DIR:-$(pwd)}" # or the installed lov-repo2docs dir ``` If any network command times out in the sandbox, export the proxy once: ```bash export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7891 ``` ### Step 1: Collect inputs with AskUserQuestion **Use `AskUserQuestion` BEFORE doing anything**, unless the user already gave all: - **Source**: GitHub URL, local folder path, or current directory? - **Product id**: the subdomain → `{product-id}.example.com`. Propose a slug from the folder/repo name; confirm. - **Title**: human-facing name for the docs. - **Deploy now?**: deploy to Vercel + bind subdomain immediately, or generate only. ### Step 2: Resolve the source folder | Source | Action | |--------|--------| | GitHub URL | `git clone --depth 1 <url> <tmp>/src` | | Local path | use as-is (read-only) | | Current dir | use `$(pwd)` | Never write into the source folder. The docs site is a **separate** directory. ### Step 3: Inventory the folder Build a manifest of every file, classified and ordered (overview material first): ```bash python3 "$SKILL_DIR/scripts/inventory.py" --src "<src>" --out /tmp/manifest.json ``` The manifest lists `units` (path, kind ∈ article/code/image/pdf/office/data/other, title guess, reading `order`) and `counts`. This is cheap — it does NOT read full contents. You read contents later, incrementally. Use `counts` to gauge scale and to decide whether to delegate batches to subagents (see Step 5 scaling note). ### Step 4: Scaffold the Fumadocs site ```bash python3 "$SKILL_DIR/scripts/scaffold_docs.py" \ --product-id "<id>" --out "<docs-out-dir>" --title "<Title>" --pm pnpm ``` Sets `basePath: '/docs'` and resets `content/docs/` to a placeholder. See `references/fumadocs.md` for layout, the `/docs` double-prefix gotcha, and components. ### Step 5: Copy images into the site ```bash python3 "$SKILL_DIR/scripts/copy_assets.py" \ --src "<src>" --site "<docs-out-dir>" --out /tmp/assets-map.json ``` This copies (and, if Pillow is present, downsizes/transcodes HEIC/TIFF/BMP) every image into `public/assets/...` and returns a `{source-path: /assets/...}` map. Reference images in MDX with the **root-relative** web path from the map (``) — with basePath `/docs`, Next resolves it to `/docs/assets/foo.png` automatically. ### Step 6: Incremental authoring loop (the core) Initialize the outline from the folder's top-level structure — by default, **preserve the source directory hierarchy as the sidebar** (a folder → a sidebar group, its `meta.json`). Then walk `units` in `order` and, for each unit: 1. **Read** the unit (the file contents). 2. **Place it** in the evolving outline: a new page, a section of an existing page, or merged into a group index. Update the relevant `meta.json`. 3. **Write or refine** the MDX page: - For **articles/notes/PDF/office**: present the content as a real page — preserve the author's substance; clean up formatting; embed its images via the assets map; if a folder holds many images, render a gallery/grid. - For **code**: write explained docs — purpose, install, usage, public API, examples — derived from the code, README, and comments. Don't dump source. - For **images** with no surrounding article: group them into a gallery page for their folder. 4. **Refine backward**: as later units add context, improve earlier pages, the intro, cross-links, and sidebar order. The structure is *emergent and incrementally polished*, not one-shot. Quality bar: a **professional, polished** site — coherent IA, working internal links, real content (never lorem ipsum), inviting landing page, images that render crisply. Use Fumadocs components (`<Cards>`, `<Tabs>`, `<Steps>`, `<Callout>`, `<ImageZoom>`/gallery) per `references/fumadocs.md`. **Scaling note (large folders)**: when `counts` is large, delegate batches to subagents (Explore / general-purpose). Give each a slice of `units` + the assets map; have it return structured page contributions and outline deltas (which group, which order). The main thread owns and merges the evolving IA so the result stays coherent. Don't let subagents each invent a separate top-level structure. ### Step 7: Verify the build locally ```bash cd "<docs-out-dir>" && pnpm build # must succeed — fix MDX/link/image errors ``` ### Step 8: Deploy (if requested) Delegate to `lov-deploy-to-vercel` — do NOT reimplement Vercel/DNS here. Ensure `package.json` "name" is a valid lowercase slug, then deploy with domain `{product-id}.example.com`. The site serves at `…/docs` via the basePath. After deploy, **verify the live URL returns 200** (curl), not just that the alias was set. ## CLI Reference `scripts/inventory.py` — enumerate + classify a folder: | Argument | Default | Description | |----------|---------|-------------| | `--src` | (required) | Source folder | | `--out` | stdout | Manifest JSON path | | `--max-bytes` | `2000000` | Skip title-reading files larger than this | `scripts/copy_assets.py` — images → `public/assets/`: | Argument | Default | Description | |----------|---------|-------------| | `--src` | (required) | Source folder | | `--site` | (required) | Fumadocs site root | | `--max-width` | `1600` | Downscale wider images (needs Pillow) | | `--no-optimize` | off | Copy verbatim, never transcode/resize | | `--out` | stdout | Asset path-map JSON | `scripts/scaffold_docs.py` — scaffold the site: | Argument | Default | Description | |----------|---------|-------------| | `--product-id` | (required) | Slug → `{id}.example.com` | | `--out` | (required) | Output dir (separate from source) | | `--title` | `=product-id` | Human-facing title | | `--pm` | `pnpm` | Package manager | ## Dependencies - Node.js 18+, `npx`, `git`, Python 3.8+ - Optional: Pillow (`pip install Pillow`) for image optimization - Vercel CLI (`npm i -g vercel`) for deploy - `lov-deploy-to-vercel` skill (Vercel + Cloudflare DNS) For Fumadocs structure, components, and config, see `references/fumadocs.md`. ## Runtime context (shared) 运行前读取本 Skill 包的 `skill.yaml`,由宿主提供 `skill-runtime/v1` 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。 - 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。 - `required: true` 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。 - 报错提供可复制的 `context_id`、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。 ## 通用反馈闭环 用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行: 1. 先判断意见是 `task-specific`(仅本次)还是 `reusable`(可跨任务复用)。 2. `task-specific` 只修改当前任务,不改 Skill。 3. `reusable` 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。 4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。 5. `reusable` 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。 -
skill.yaml 833 B
schema: skill-manifest/v1 id: lov-repo2docs version: "0.3.0" runtime: skill-runtime/v1 context: profile: fields: - path: identity.name required: false question: 如果本次输出需要品牌身份,请提供品牌名称。 - path: identity.logo required: false question: 如果需要使用品牌 Logo,请提供 Logo 地址或文件路径。 - path: brand.tone required: false question: 如果已有品牌语气或审美关键词,请提供它们。 preferences: namespace: lov_repo2docs fields: - path: user.language required: false question: 希望使用哪种语言输出? - path: user.timezone required: false question: 需要使用哪个时区处理日期和时间? interaction: ask_missing: true max_questions: 1
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.