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