Raw
1 #!/usr/bin/env node
2 /**
3 * Sync the /developer docs from the smbcloud-cli repository.
4 *
5 * The CLI / tooling documentation lives in `docs/` of smbcloud-cli and is the
6 * source of truth. This script pulls those files into content/developer/ so the
7 * docs site tracks the tool. Synced files are committed (vendored) so the site
8 * builds on CI without network access — re-run this script to refresh them.
9 *
10 * pnpm sync:developer
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 { execSync } from 'node:child_process'
16 import { mkdtempSync, rmSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
17 import { tmpdir } from 'node:os'
18 import { join, dirname } from 'node:path'
19 import { fileURLToPath } from 'node:url'
20
21 const REPO = 'https://github.com/smbcloudXYZ/smbcloud-cli.git'
22 const BRANCH = 'main'
23 const SRC_DIR = 'docs'
24
25 const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
26 const DEST = join(ROOT, 'content', 'developer')
27
28 // Source file -> where it lands and how it's labelled in the sidebar.
29 // `mail-service.md` is intentionally excluded: it's a scratch notes file in the
30 // source repo, not publishable documentation.
31 const MANIFEST = {
32 cli: {
33 title: 'CLI',
34 pages: [
35 { src: 'cli-install.md', slug: 'native', title: 'Native (Cargo)' },
36 { src: 'cli-install-homebrew.md', slug: 'homebrew', title: 'Homebrew' },
37 { src: 'cli-install-npm.md', slug: 'npm', title: 'npm' },
38 { src: 'cli-install-pypi.md', slug: 'pypi', title: 'PyPI' }
39 ]
40 },
41 contributing: {
42 title: 'Contributing',
43 pages: [
44 { src: 'debugging.md', slug: 'debugging', title: 'VSCode Debugging' },
45 { src: 'development.md', slug: 'development', title: 'Releasing & ADRs' },
46 { src: 'gems.md', slug: 'gems', title: 'Publishing the gem' }
47 ]
48 }
49 }
50
51 const blobUrl = src =>
52 `https://github.com/smbcloudXYZ/smbcloud-cli/blob/${BRANCH}/${SRC_DIR}/${src}`
53
54 /** Wrap synced markdown: header note + a source callout after the first H1. */
55 function decorate(markdown, src) {
56 const note = `{/* AUTO-GENERATED — do not edit. Synced by scripts/sync-developer-docs.mjs from ${SRC_DIR}/${src} (smbcloud-cli, ${BRANCH}). */}`
57 const callout = [
58 "import { Callout } from 'nextra/components'",
59 '',
60 '<Callout type="info">',
61 ` Synced from [\`${SRC_DIR}/${src}\`](${blobUrl(src)}) on the \`${BRANCH}\` branch of`,
62 ' [smbcloud-cli](https://github.com/smbcloudXYZ/smbcloud-cli). Edit it there.',
63 '</Callout>'
64 ].join('\n')
65
66 const lines = markdown.replace(/\r\n/g, '\n').trimEnd().split('\n')
67 const h1 = lines.findIndex(l => /^#\s/.test(l))
68 if (h1 === -1) {
69 return `${note}\n\n${callout}\n\n${lines.join('\n')}\n`
70 }
71 const before = lines.slice(0, h1 + 1).join('\n')
72 const after = lines.slice(h1 + 1).join('\n')
73 return `${note}\n\n${before}\n\n${callout}\n${after}\n`
74 }
75
76 // Note: the folder's own index page uses `asIndexPage` frontmatter, so it must
77 // NOT be listed as an `index` key here — Nextra surfaces it via the parent.
78 function metaFile(pages) {
79 const entries = pages.map(p => ` ${p.slug}: ${JSON.stringify(p.title)},`)
80 return `export default {\n${entries.join('\n')}\n}\n`
81 }
82
83 function main() {
84 const tmp = mkdtempSync(join(tmpdir(), 'smb-cli-docs-'))
85 try {
86 console.log(`Cloning ${REPO} (${BRANCH}, sparse: ${SRC_DIR})…`)
87 execSync(
88 `git clone --depth 1 --branch ${BRANCH} --filter=blob:none --sparse ${REPO} "${tmp}"`,
89 { stdio: 'inherit' }
90 )
91 execSync(`git -C "${tmp}" sparse-checkout set ${SRC_DIR}`, { stdio: 'inherit' })
92
93 let count = 0
94 for (const [section, { title, pages }] of Object.entries(MANIFEST)) {
95 const dir = join(DEST, section)
96 mkdirSync(dir, { recursive: true })
97 for (const page of pages) {
98 const raw = readFileSync(join(tmp, SRC_DIR, page.src), 'utf8')
99 writeFileSync(join(dir, `${page.slug}.mdx`), decorate(raw, page.src))
100 count++
101 }
102 writeFileSync(join(dir, '_meta.js'), metaFile(pages))
103 console.log(` ${section}: ${pages.length} pages (${title})`)
104 }
105
106 // Top-level developer nav: each section (the overview is asIndexPage).
107 const top = []
108 for (const [section, { title }] of Object.entries(MANIFEST)) {
109 top.push(` ${section}: ${JSON.stringify(title)},`)
110 }
111 writeFileSync(join(DEST, '_meta.js'), `export default {\n${top.join('\n')}\n}\n`)
112
113 console.log(`\nSynced ${count} pages into content/developer/.`)
114 } finally {
115 rmSync(tmp, { recursive: true, force: true })
116 }
117 }
118
119 main()