Skip to main content
On this page

generate-reference-docs.mjs

Helper script generate-reference-docs.mjs.

Source

scripts/generate-reference-docs.mjs

Contents

JavaScript
#!/usr/bin/env node// generate-reference-docs.mjs — v3 reference-pages generator.//// Reads://   skills/adk-<name>/SKILL.md//   agents-claude/agents/adk-agent-<name>.md//   mcp/adk-mcp-<name>.json//   scripts/<name>.py (+ scripts/<name>.mjs)//   hooks/hooks.json + hooks/banner.sh//   shared/{constitution,advisor,question-first,decision-log-schema,edit-format,plan-act-mode,model-depth}.md//   shared/{personas,workflows,input-classifiers,guidelines}/<name>.md//   agents-cursor/, agents-codex/, agents-junie/ (env wrappers)//// Writes://   docs/reference/skills/<name>.md//   docs/reference/agents/<name>.md//   docs/reference/mcp/<name>.md//   docs/reference/scripts/<name>.md//   docs/reference/hooks.md//   docs/reference/shared/<group>/<file>.md//   docs/reference/agent-envs/<env>.md//// Idempotent: clears all generated dirs first, then re-writes.import {  existsSync,  mkdirSync,  readdirSync,  readFileSync,  rmSync,  writeFileSync,} from "node:fs";import { basename, dirname, join, relative, resolve } from "node:path";import { fileURLToPath } from "node:url";const __dirname = dirname(fileURLToPath(import.meta.url));const repoRoot = resolve(__dirname, "..");const docsRefDir = join(repoRoot, "docs", "reference");const GENERATED_DIRS = [  "skills",  "agents",  "mcp",  "scripts",  "agent-envs",  "shared/personas",  "shared/workflows",  "shared/input-classifiers",  "shared/guidelines",];const ORDER_OFFSET = {  skills: 1000,  agents: 2000,  mcp: 3000,  scripts: 4000,  "agent-envs": 5000,  "shared/personas": 6000,  "shared/workflows": 6100,  "shared/input-classifiers": 6200,  "shared/guidelines": 6300,};function readText(path) {  return readFileSync(path, "utf8");}function writeFile(path, contents) {  mkdirSync(dirname(path), { recursive: true });  writeFileSync(path, `${contents.replace(/\s+$/u, "")}\n`);}function resetGeneratedDirs() {  for (const dir of GENERATED_DIRS) {    const fullDir = join(docsRefDir, dir);    if (existsSync(fullDir)) rmSync(fullDir, { recursive: true, force: true });    mkdirSync(fullDir, { recursive: true });  }}function escapeYaml(value) {  return `'${String(value ?? "").replace(/'/gu, "''").replace(/\n/gu, " ")}'`;}function frontmatter(fields) {  const lines = ["---"];  for (const [key, value] of Object.entries(fields)) {    if (value === undefined || value === null) continue;    lines.push(`${key}: ${typeof value === "number" ? value : escapeYaml(value)}`);  }  lines.push("---", "");  return lines.join("\n");}function parseFrontmatter(text) {  const match = /^---\s*\n([\s\S]*?)\n---\s*\n?/u.exec(text);  if (!match) return { data: {}, body: text };  const data = {};  let activeKey = null;  let inBlock = false;  for (const line of match[1].split("\n")) {    const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);    if (kv) {      activeKey = kv[1];      const v = kv[2].trim();      if (v === "|" || v === ">") {        data[activeKey] = "";        inBlock = true;      } else {        data[activeKey] = v.replace(/^['"]|['"]$/g, "");        inBlock = false;      }    } else if (inBlock && activeKey && /^\s+/.test(line)) {      data[activeKey] = (data[activeKey] ? data[activeKey] + " " : "") + line.trim();    }  }  return { data, body: text.slice(match[0].length) };}function compactDesc(value, maxLength = 220) {  const compact = String(value ?? "").replace(/\s+/gu, " ").trim();  if (compact.length <= maxLength) return compact;  return `${compact.slice(0, maxLength - 3).replace(/\s+\S*$/u, "")}...`;}function langForFile(name) {  if (name.endsWith(".py")) return "python";  if (name.endsWith(".mjs") || name.endsWith(".js")) return "js";  if (name.endsWith(".sh")) return "bash";  if (name.endsWith(".json")) return "json";  if (name.endsWith(".json5")) return "json5";  if (name.endsWith(".yaml") || name.endsWith(".yml")) return "yaml";  if (name.endsWith(".toml")) return "toml";  return "text";}// ----- Skills --------------------------------------------------------------function generateSkillPages() {  const skillsDir = join(repoRoot, "skills");  if (!existsSync(skillsDir)) return [];  const entries = readdirSync(skillsDir, { withFileTypes: true })    .filter((e) => e.isDirectory() && e.name.startsWith("adk-"))    .sort((a, b) => a.name.localeCompare(b.name));  const generated = [];  for (const [i, entry] of entries.entries()) {    const skillDir = join(skillsDir, entry.name);    const skillMd = join(skillDir, "SKILL.md");    if (!existsSync(skillMd)) continue;    const text = readText(skillMd);    const { data, body } = parseFrontmatter(text);    const name = data.name || entry.name;    const description = data.description || "";    const referencesDir = join(skillDir, "references");    const refs = existsSync(referencesDir)      ? readdirSync(referencesDir).filter((f) => f.endsWith(".md")).sort()      : [];    const refsList = refs.length      ? refs.map((r) => `- \`references/${r}\` — see [\`/${entry.name}/references/${r}\`](https://github.com/sujeet-pro/agents-devkit/blob/main/skills/${entry.name}/references/${r})`).join("\n")      : "_(References authored on first real use of each sub-flow.)_";    const md = `${frontmatter({      title: name,      description: compactDesc(description),      skill: name,      source: relative(repoRoot, skillMd),      group: "skills",      order: ORDER_OFFSET.skills + i,    })}# ${name}${description}## Source\`${relative(repoRoot, skillMd)}\`## Frontmatter\`\`\`yaml${(text.match(/^---\s*\n([\s\S]*?)\n---/) || ["", ""])[1]}\`\`\`## Workflow body${body.trim()}## References shipped${refsList}`;    writeFile(join(docsRefDir, "skills", `${entry.name}.md`), md);    generated.push({ name: entry.name, description, file: `${entry.name}.md` });  }  return generated;}// ----- Agents --------------------------------------------------------------function generateAgentPages() {  const dir = join(repoRoot, "agents-claude", "agents");  if (!existsSync(dir)) return [];  const files = readdirSync(dir)    .filter((f) => f.startsWith("adk-agent-") && f.endsWith(".md"))    .sort();  const generated = [];  for (const [i, file] of files.entries()) {    const full = join(dir, file);    const text = readText(full);    const { data, body } = parseFrontmatter(text);    const name = data.name || basename(file, ".md");    const description = data.description || "";    const md = `${frontmatter({      title: name,      description: compactDesc(description),      agent: name,      source: relative(repoRoot, full),      group: "agents",      order: ORDER_OFFSET.agents + i,    })}# ${name}${description}## Source\`${relative(repoRoot, full)}\`## Frontmatter\`\`\`yaml${(text.match(/^---\s*\n([\s\S]*?)\n---/) || ["", ""])[1]}\`\`\`## Body${body.trim()}`;    writeFile(join(docsRefDir, "agents", file), md);    generated.push({ name, description, file });  }  return generated;}// ----- MCPs ----------------------------------------------------------------function extractEnvRefs(value) {  const text = JSON.stringify(value);  const refs = new Set();  for (const m of text.matchAll(/\$\{([A-Z0-9_]+)(?::-[^}]*)?\}/g)) refs.add(m[1]);  return [...refs].sort();}function generateMcpPages() {  const dir = join(repoRoot, "mcp");  if (!existsSync(dir)) return [];  const files = readdirSync(dir)    .filter((f) => f.startsWith("adk-mcp-") && f.endsWith(".json"))    .sort();  const generated = [];  for (const [i, file] of files.entries()) {    const full = join(dir, file);    const cfg = JSON.parse(readText(full));    const name = cfg.name || basename(file, ".json");    const description = cfg.description || "";    const envs = extractEnvRefs(cfg);    const md = `${frontmatter({      title: name,      description: compactDesc(description),      mcp: name,      source: relative(repoRoot, full),      group: "mcp",      order: ORDER_OFFSET.mcp + i,    })}# ${name}${description}## Source\`${relative(repoRoot, full)}\`## Environment variables referenced${envs.length ? envs.map((e) => `- \`${e}\``).join("\n") : "_(none)_"}## Configuration\`\`\`json${JSON.stringify(cfg, null, 2)}\`\`\``;    writeFile(join(docsRefDir, "mcp", file.replace(/\.json$/, ".md")), md);    generated.push({ name, description, file: file.replace(/\.json$/, ".md") });  }  return generated;}// ----- Scripts -------------------------------------------------------------function generateScriptPages() {  const dir = join(repoRoot, "scripts");  if (!existsSync(dir)) return [];  const files = readdirSync(dir)    .filter((f) => /\.(py|mjs)$/.test(f))    .sort();  const generated = [];  for (const [i, file] of files.entries()) {    const full = join(dir, file);    const text = readText(full);    // Pull the docstring (Python) or top comment (JS)    let summary = "";    const pyDoc = /^#!.*\n+"""([\s\S]*?)"""/m.exec(text);    const jsDoc = /^#!.*\n+\/\*\*([\s\S]*?)\*\//m.exec(text);    if (pyDoc) summary = pyDoc[1].split("\n")[0].trim();    else if (jsDoc) summary = jsDoc[1].split("\n").map((l) => l.replace(/^\s*\*\s?/, "")).filter(Boolean)[0] || "";    if (!summary) summary = `Helper script ${file}.`;    const md = `${frontmatter({      title: file,      description: compactDesc(summary),      script: file,      source: relative(repoRoot, full),      group: "scripts",      order: ORDER_OFFSET.scripts + i,    })}# ${file}${summary}## Source\`${relative(repoRoot, full)}\`## Contents\`\`\`${langForFile(file)}${text}\`\`\``;    writeFile(join(docsRefDir, "scripts", file.replace(/\.(py|mjs)$/, ".md")), md);    generated.push({ name: file, description: summary, file: file.replace(/\.(py|mjs)$/, ".md") });  }  return generated;}// ----- Hooks ---------------------------------------------------------------function generateHooksPage() {  const hooksJson = join(repoRoot, "hooks", "hooks.json");  const bannerSh = join(repoRoot, "hooks", "banner.sh");  if (!existsSync(hooksJson)) return null;  const json = JSON.parse(readText(hooksJson));  const banner = existsSync(bannerSh) ? readText(bannerSh) : "";  const md = `${frontmatter({    title: "Hooks",    description: "Deterministic enforcement of the constitution. PreToolUse:Bash safety, PostToolUse:Edit validator, SessionStart banner. Wired into ~/.claude/settings.json by install.sh.",    source: "hooks/",    group: "hooks",    order: 5500,  })}# HooksDeterministic enforcement of \`shared/constitution.md\`. Three events:- **PreToolUse:Bash** — blocks force-push, hard-reset on protected branches, \`rm -rf $HOME\`, unrequested PR merges, writes to \`~/.agents-devkit/improve/learning/archive/\`, \`--no-verify\` bypasses.- **PostToolUse:Edit\\|Write** — validates SKILL.md frontmatter on writes; touches \`.temp/<task-slug>/.last-modified\`; refuses raw-token writes to \`~/.agents-devkit/config/overrides.yaml\`.- **SessionStart** — prints the adk status banner.\`install.sh\` merges these into \`~/.claude/settings.json\` with an \`_adk_managed: true\` tag so they're idempotent and removable on uninstall.## hooks/hooks.json\`\`\`json${JSON.stringify(json, null, 2)}\`\`\`## hooks/banner.sh\`\`\`bash${banner}\`\`\``;  writeFile(join(docsRefDir, "hooks.md"), md);  return { file: "hooks.md" };}// ----- Shared --------------------------------------------------------------function generateSharedPages() {  const out = {};  const sharedDir = join(repoRoot, "shared");  if (!existsSync(sharedDir)) return out;  const topLevel = [    "constitution.md",    "advisor.md",    "question-first.md",    "decision-log-schema.md",    "edit-format.md",    "plan-act-mode.md",    "model-depth.md",  ];  for (const [i, f] of topLevel.entries()) {    const full = join(sharedDir, f);    if (!existsSync(full)) continue;    const text = readText(full);    const title = `shared/${f.replace(/\.md$/, "")}`;    const firstLine = text.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">")) || "";    const md = `${frontmatter({      title,      description: compactDesc(firstLine),      source: `shared/${f}`,      group: "shared",      order: 5600 + i,    })}# ${title}> Source: \`shared/${f}\`${text}`;    writeFile(join(docsRefDir, "shared", f), md);    out[f] = `shared/${f}`;  }  // Subgroups  const subgroups = ["personas", "workflows", "input-classifiers", "guidelines"];  for (const group of subgroups) {    const groupDir = join(sharedDir, group);    if (!existsSync(groupDir)) continue;    const files = readdirSync(groupDir).filter((f) => f.endsWith(".md")).sort();    out[group] = [];    for (const [i, f] of files.entries()) {      const full = join(groupDir, f);      const text = readText(full);      const firstLine = text.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">")) || "";      const md = `${frontmatter({        title: `${group}/${f.replace(/\.md$/, "")}`,        description: compactDesc(firstLine),        source: `shared/${group}/${f}`,        group: `shared-${group}`,        order: ORDER_OFFSET[`shared/${group}`] + i,      })}# shared/${group}/${f.replace(/\.md$/, "")}> Source: \`shared/${group}/${f}\`${text}`;      writeFile(join(docsRefDir, "shared", group, f), md);      out[group].push(f);    }  }  return out;}// ----- Per-agent-env wrappers ---------------------------------------------function generateAgentEnvPages() {  const envs = ["claude", "cursor", "codex", "junie"];  for (const [i, env] of envs.entries()) {    const dir = join(repoRoot, `agents-${env}`);    if (!existsSync(dir)) continue;    const readme = join(dir, "README.md");    let bodyExtra = "";    if (existsSync(readme)) {      bodyExtra = `\n\n## README\n\n${readText(readme)}`;    }    // List the wrapper files installed for this env    function listDir(sub) {      const p = join(dir, sub);      if (!existsSync(p)) return [];      return readdirSync(p).filter((f) => !f.startsWith(".")).sort();    }    const sections = [];    const agentFiles = listDir("agents");    if (agentFiles.length) sections.push(`### Subagents (${agentFiles.length})\n\n${agentFiles.map((f) => `- \`agents-${env}/agents/${f}\``).join("\n")}`);    const cmdFiles = listDir("commands");    if (cmdFiles.length) sections.push(`### Commands (${cmdFiles.length})\n\n${cmdFiles.map((f) => `- \`agents-${env}/commands/${f}\``).join("\n")}`);    const ruleFiles = listDir("rules");    if (ruleFiles.length) sections.push(`### Rules (${ruleFiles.length})\n\n${ruleFiles.map((f) => `- \`agents-${env}/rules/${f}\``).join("\n")}`);    const promptFiles = listDir("prompts");    if (promptFiles.length) sections.push(`### Prompts (${promptFiles.length})\n\n${promptFiles.map((f) => `- \`agents-${env}/prompts/${f}\``).join("\n")}`);    const templates = readdirSync(dir).filter((f) => f.endsWith(".tmpl") || f.endsWith(".append"));    if (templates.length) sections.push(`### Append templates\n\n${templates.map((f) => `- \`agents-${env}/${f}\``).join("\n")}`);    const md = `${frontmatter({      title: `agents-${env}`,      description: `adk wrappers for ${env}. See agents-${env}/README.md (if present) for capability status.`,      env,      source: `agents-${env}/`,      group: "agent-envs",      order: ORDER_OFFSET["agent-envs"] + i,    })}# agents-${env}Wrappers that install adk into \`${env}\` at user level. Installed via \`./install.sh --target ${env}\`.${sections.join("\n\n")}${bodyExtra}`;    writeFile(join(docsRefDir, "agent-envs", `${env}.md`), md);  }}// ----- Main ---------------------------------------------------------------function main() {  resetGeneratedDirs();  const skills = generateSkillPages();  const agents = generateAgentPages();  const mcps = generateMcpPages();  const scripts = generateScriptPages();  generateHooksPage();  generateSharedPages();  generateAgentEnvPages();  console.log(`generated:`);  console.log(`  skills:  ${skills.length}`);  console.log(`  agents:  ${agents.length}`);  console.log(`  mcp:     ${mcps.length}`);  console.log(`  scripts: ${scripts.length}`);  console.log(`  hooks:   1`);  console.log(`  shared:  shared/, shared/personas/, shared/workflows/, shared/input-classifiers/, shared/guidelines/`);  console.log(`  agent-envs: claude / cursor / codex / junie`);}main();