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()