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