release/v0.7.4
claude/smbcloud-github-app-k9641p
development
feature/apps
feature/build-release
feature/ci-docs
feature/ci-ruby-gems
feature/cicd
feature/completion
feature/dist-nuget
feature/fix-ci-sdk-pypi-rubygem
feature/fix-clippy-on-stable-channel
feature/fix-deployment-script
feature/github-email
feature/gresiq-v1
feature/project-new-structure
feature/rsync-deploy
feature/ruby-runner
feature/sdk-npm
feature/sdk-workflows
feature/smbcloud-cli-release-npm
feature/smbcloud-deploy
feature/wasm
main
release/v0.7.4
| 1 | #!/usr/bin/env node |
| 2 | /** |
| 3 | * Generate the /developer docs from the sibling `docs/` tree. |
| 4 | * |
| 5 | * The CLI / tooling documentation lives in `docs/` at the smbcloud-cli repo root |
| 6 | * (this site is `website/` in the same repo), so it is read in place — no clone, |
| 7 | * no network. This runs automatically on `predev`/`prebuild`; the generated |
| 8 | * pages are gitignored, leaving `docs/` as the single source of truth. |
| 9 | * |
| 10 | * pnpm docs:generate |
| 11 | * |
| 12 | * Hand-written index pages (content/developer/**\/index.mdx) are NOT touched. |
| 13 | * Only the files listed in MANIFEST and the generated _meta.js are written. |
| 14 | */ |
| 15 | import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' |
| 16 | import { join, dirname } from 'node:path' |
| 17 | import { fileURLToPath } from 'node:url' |
| 18 | |
| 19 | const BRANCH = 'main' |
| 20 | const SRC_DIR = 'docs' |
| 21 | |
| 22 | const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') |
| 23 | // `docs/` sits next to `website/` at the smbcloud-cli repo root. |
| 24 | const SRC = join(ROOT, '..', SRC_DIR) |
| 25 | const DEST = join(ROOT, 'content', 'developer') |
| 26 | |
| 27 | // Source file -> where it lands, how it's labelled in the sidebar, and its |
| 28 | // meta description (frontmatter -> real <title>/<meta name="description"> |
| 29 | // tags via Nextra's per-page metadata — see app/[[...mdxPath]]/page.jsx). |
| 30 | // `mail-service.md` is intentionally excluded: it's a scratch notes file in the |
| 31 | // source repo, not publishable documentation. |
| 32 | const MANIFEST = { |
| 33 | cli: { |
| 34 | title: 'CLI', |
| 35 | pages: [ |
| 36 | { |
| 37 | src: 'cli-install.md', |
| 38 | slug: 'native', |
| 39 | title: 'Native (Cargo)', |
| 40 | description: 'Install the smbCloud CLI (smb) from crates.io with Cargo.' |
| 41 | }, |
| 42 | { |
| 43 | src: 'cli-install-homebrew.md', |
| 44 | slug: 'homebrew', |
| 45 | title: 'Homebrew', |
| 46 | description: 'Install the smbCloud CLI (smb) on macOS or Linux with Homebrew.' |
| 47 | }, |
| 48 | { |
| 49 | src: 'cli-install-npm.md', |
| 50 | slug: 'npm', |
| 51 | title: 'npm', |
| 52 | description: 'Install the smbCloud CLI (smb) from npm for JavaScript toolchains.' |
| 53 | }, |
| 54 | { |
| 55 | src: 'cli-install-pypi.md', |
| 56 | slug: 'pypi', |
| 57 | title: 'PyPI', |
| 58 | description: 'Install the smbCloud CLI (smb) from PyPI for Python toolchains.' |
| 59 | }, |
| 60 | { |
| 61 | src: 'interfaces.md', |
| 62 | slug: 'interfaces', |
| 63 | title: 'Interfaces', |
| 64 | description: |
| 65 | 'How the smbCloud CLI’s three interfaces — headless, --tui, and --mcp — work, and when to use each.' |
| 66 | }, |
| 67 | { |
| 68 | src: 'mcp.md', |
| 69 | slug: 'mcp', |
| 70 | title: 'MCP Server', |
| 71 | description: |
| 72 | 'Run the smbCloud CLI as an MCP server and connect Claude Desktop, Claude Code, Cursor, or any MCP client to manage smbCloud resources, simulators, and ControlKit runners. Setup guide plus the full tool reference.' |
| 73 | }, |
| 74 | { |
| 75 | src: 'controlkit.md', |
| 76 | slug: 'controlkit', |
| 77 | title: 'ControlKit runners', |
| 78 | description: |
| 79 | 'Build and connect the native ControlKit runners for iOS, tvOS, visionOS, watchOS, and macOS through the smbCloud and xcrs MCP servers.' |
| 80 | }, |
| 81 | { |
| 82 | src: 'ci.md', |
| 83 | slug: 'ci', |
| 84 | title: 'CI', |
| 85 | description: |
| 86 | 'Run the smbCloud CLI non-interactively with --ci and deploy from GitHub Actions using smbcloud-deploy-action.' |
| 87 | } |
| 88 | ] |
| 89 | }, |
| 90 | contributing: { |
| 91 | title: 'Contributing', |
| 92 | pages: [ |
| 93 | { |
| 94 | src: 'debugging.md', |
| 95 | slug: 'debugging', |
| 96 | title: 'VSCode Debugging', |
| 97 | description: 'Debug the smbCloud CLI in VS Code with CodeLLDB and cargo debugger.' |
| 98 | }, |
| 99 | { |
| 100 | src: 'development.md', |
| 101 | slug: 'development', |
| 102 | title: 'Releasing & ADRs', |
| 103 | description: 'The smbCloud CLI release process and its architecture decision records.' |
| 104 | }, |
| 105 | { |
| 106 | src: 'gems.md', |
| 107 | slug: 'gems', |
| 108 | title: 'Publishing the gem', |
| 109 | description: 'Publish the smbcloud-model Ruby gem alongside its Rust crate release.' |
| 110 | } |
| 111 | ] |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | // Cross-links between docs pages use plain relative `./file.md` links — |
| 116 | // correct as-is on GitHub, where these files are read directly. Rewrite them |
| 117 | // to absolute site paths so they also resolve on the published site. |
| 118 | const SITE_PATH_BY_SRC = Object.fromEntries( |
| 119 | Object.entries(MANIFEST).flatMap(([section, { pages }]) => |
| 120 | pages.map(p => [p.src, `/developer/${section}/${p.slug}`]) |
| 121 | ) |
| 122 | ) |
| 123 | |
| 124 | function rewriteLinks(markdown) { |
| 125 | return markdown.replace(/\]\((?:\.\/)?([\w-]+\.md)\)/g, (match, src) => { |
| 126 | const sitePath = SITE_PATH_BY_SRC[src] |
| 127 | return sitePath ? `](${sitePath})` : match |
| 128 | }) |
| 129 | } |
| 130 | |
| 131 | const blobUrl = src => |
| 132 | `https://github.com/smbcloudXYZ/smbcloud-cli/blob/${BRANCH}/${SRC_DIR}/${src}` |
| 133 | |
| 134 | /** |
| 135 | * Wrap generated markdown: frontmatter (for real per-page SEO metadata), |
| 136 | * header note, and a source callout after the first H1. |
| 137 | */ |
| 138 | function decorate(markdown, page) { |
| 139 | markdown = rewriteLinks(markdown) |
| 140 | const frontmatter = [ |
| 141 | '---', |
| 142 | `title: ${JSON.stringify(page.title)}`, |
| 143 | `description: ${JSON.stringify(page.description)}`, |
| 144 | '---' |
| 145 | ].join('\n') |
| 146 | const note = `{/* AUTO-GENERATED — do not edit. Generated by scripts/generate-developer-docs.mjs from ../${SRC_DIR}/${page.src}. */}` |
| 147 | const callout = [ |
| 148 | "import { Callout } from 'nextra/components'", |
| 149 | '', |
| 150 | '<Callout type="info">', |
| 151 | ` Generated from [\`${SRC_DIR}/${page.src}\`](${blobUrl(page.src)}) in`, |
| 152 | ' [smbcloud-cli](https://github.com/smbcloudXYZ/smbcloud-cli). Edit it there.', |
| 153 | '</Callout>' |
| 154 | ].join('\n') |
| 155 | |
| 156 | const lines = markdown.replace(/\r\n/g, '\n').trimEnd().split('\n') |
| 157 | const h1 = lines.findIndex(l => /^#\s/.test(l)) |
| 158 | if (h1 === -1) { |
| 159 | return `${frontmatter}\n\n${note}\n\n${callout}\n\n${lines.join('\n')}\n` |
| 160 | } |
| 161 | const before = lines.slice(0, h1 + 1).join('\n') |
| 162 | const after = lines.slice(h1 + 1).join('\n') |
| 163 | return `${frontmatter}\n\n${note}\n\n${before}\n\n${callout}\n${after}\n` |
| 164 | } |
| 165 | |
| 166 | // Note: the folder's own index page uses `asIndexPage` frontmatter, so it must |
| 167 | // NOT be listed as an `index` key here — Nextra surfaces it via the parent. |
| 168 | function metaFile(pages) { |
| 169 | const entries = pages.map(p => ` ${p.slug}: ${JSON.stringify(p.title)},`) |
| 170 | return `export default {\n${entries.join('\n')}\n}\n` |
| 171 | } |
| 172 | |
| 173 | function main() { |
| 174 | let count = 0 |
| 175 | for (const [section, { title, pages }] of Object.entries(MANIFEST)) { |
| 176 | const dir = join(DEST, section) |
| 177 | mkdirSync(dir, { recursive: true }) |
| 178 | for (const page of pages) { |
| 179 | const raw = readFileSync(join(SRC, page.src), 'utf8') |
| 180 | writeFileSync(join(dir, `${page.slug}.mdx`), decorate(raw, page)) |
| 181 | count++ |
| 182 | } |
| 183 | writeFileSync(join(dir, '_meta.js'), metaFile(pages)) |
| 184 | console.log(` ${section}: ${pages.length} pages (${title})`) |
| 185 | } |
| 186 | |
| 187 | // Top-level developer nav: each section (the overview is asIndexPage). |
| 188 | const top = [] |
| 189 | for (const [section, { title }] of Object.entries(MANIFEST)) { |
| 190 | top.push(` ${section}: ${JSON.stringify(title)},`) |
| 191 | } |
| 192 | writeFileSync(join(DEST, '_meta.js'), `export default {\n${top.join('\n')}\n}\n`) |
| 193 | |
| 194 | console.log(`\nGenerated ${count} pages into content/developer/ from ${SRC}.`) |
| 195 | } |
| 196 | |
| 197 | main() |