@dominikcz/greg 0.9.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +397 -0
  2. package/bin/greg.js +241 -0
  3. package/bin/init.js +351 -0
  4. package/bin/templates/docs/getting-started.md +47 -0
  5. package/bin/templates/docs/index.md +11 -0
  6. package/bin/templates/greg.config.js +39 -0
  7. package/bin/templates/greg.config.ts +38 -0
  8. package/bin/templates/index.html +16 -0
  9. package/bin/templates/src/App.svelte +5 -0
  10. package/bin/templates/src/app.css +20 -0
  11. package/bin/templates/src/main.js +9 -0
  12. package/bin/templates/svelte.config.js +1 -0
  13. package/bin/templates/tsconfig.json +21 -0
  14. package/bin/templates/vite.config.js +23 -0
  15. package/docs/__partials/markdown/examples/basic.md +4 -0
  16. package/docs/__partials/markdown/examples/diff.md +10 -0
  17. package/docs/__partials/markdown/examples/focus.md +5 -0
  18. package/docs/__partials/markdown/examples/language-title.md +3 -0
  19. package/docs/__partials/markdown/examples/line-highlighting.md +5 -0
  20. package/docs/__partials/markdown/examples/line-numbers.md +5 -0
  21. package/docs/__partials/note.md +4 -0
  22. package/docs/guide/__shared-warning.md +4 -0
  23. package/docs/guide/asset-handling.md +88 -0
  24. package/docs/guide/deploying.md +162 -0
  25. package/docs/guide/getting-started.md +334 -0
  26. package/docs/guide/index.md +23 -0
  27. package/docs/guide/localization.md +290 -0
  28. package/docs/guide/markdown/code.md +95 -0
  29. package/docs/guide/markdown/components-and-mermaid.md +43 -0
  30. package/docs/guide/markdown/containers.md +110 -0
  31. package/docs/guide/markdown/header-anchors.md +34 -0
  32. package/docs/guide/markdown/includes.md +84 -0
  33. package/docs/guide/markdown/index.md +20 -0
  34. package/docs/guide/markdown/inline-attributes.md +21 -0
  35. package/docs/guide/markdown/links-and-toc.md +64 -0
  36. package/docs/guide/markdown/math.md +54 -0
  37. package/docs/guide/markdown/syntax-highlighting.md +75 -0
  38. package/docs/guide/routing.md +150 -0
  39. package/docs/guide/using-svelte.md +88 -0
  40. package/docs/guide/versioning.md +281 -0
  41. package/docs/incompatibilities.md +48 -0
  42. package/docs/index.md +43 -0
  43. package/docs/reference/badge.md +100 -0
  44. package/docs/reference/carbon-ads.md +46 -0
  45. package/docs/reference/code-group.md +126 -0
  46. package/docs/reference/home-page.md +232 -0
  47. package/docs/reference/index.md +18 -0
  48. package/docs/reference/markdowndocs.md +275 -0
  49. package/docs/reference/outline.md +79 -0
  50. package/docs/reference/search.md +263 -0
  51. package/docs/reference/steps.md +200 -0
  52. package/docs/reference/team-page.md +189 -0
  53. package/docs/reference/theme.md +150 -0
  54. package/fakeDocsGenerator/generate_docs.js +310 -0
  55. package/package.json +92 -0
  56. package/scripts/build-versions.js +609 -0
  57. package/scripts/generate-static.js +79 -0
  58. package/scripts/render-markdown.js +420 -0
  59. package/src/lib/MarkdownDocs/AiChat.svelte +936 -0
  60. package/src/lib/MarkdownDocs/BackToTop.svelte +68 -0
  61. package/src/lib/MarkdownDocs/Breadcrumb.svelte +68 -0
  62. package/src/lib/MarkdownDocs/DocsNavigation.svelte +149 -0
  63. package/src/lib/MarkdownDocs/DocsSiteHeader.svelte +758 -0
  64. package/src/lib/MarkdownDocs/DocsVersionSwitcher.svelte +103 -0
  65. package/src/lib/MarkdownDocs/MarkdownDocs.svelte +2115 -0
  66. package/src/lib/MarkdownDocs/MarkdownRenderer.svelte +487 -0
  67. package/src/lib/MarkdownDocs/Outline.svelte +238 -0
  68. package/src/lib/MarkdownDocs/PrevNext.svelte +115 -0
  69. package/src/lib/MarkdownDocs/SearchModal.svelte +1241 -0
  70. package/src/lib/MarkdownDocs/TreeView.svelte +32 -0
  71. package/src/lib/MarkdownDocs/TreeViewItem.svelte +219 -0
  72. package/src/lib/MarkdownDocs/VersionOutdatedNotice.svelte +72 -0
  73. package/src/lib/MarkdownDocs/__tests__/codeDirectives.test.js +54 -0
  74. package/src/lib/MarkdownDocs/__tests__/common.test.js +41 -0
  75. package/src/lib/MarkdownDocs/__tests__/docsExamplesLint.test.js +77 -0
  76. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/__partial-basic.md +3 -0
  77. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/snippet.js +9 -0
  78. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/part.md +11 -0
  79. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/wrapper.md +5 -0
  80. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.js +8 -0
  81. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.md +5 -0
  82. package/src/lib/MarkdownDocs/__tests__/helpers.js +67 -0
  83. package/src/lib/MarkdownDocs/__tests__/localeUtils.test.js +204 -0
  84. package/src/lib/MarkdownDocs/__tests__/markdown.test.js +704 -0
  85. package/src/lib/MarkdownDocs/__tests__/markdownRendererRuntime.test.js +65 -0
  86. package/src/lib/MarkdownDocs/__tests__/searchIndexBuilder.test.js +117 -0
  87. package/src/lib/MarkdownDocs/__tests__/sqliteStore.test.js +202 -0
  88. package/src/lib/MarkdownDocs/__tests__/useRouter.test.js +16 -0
  89. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.js +14 -0
  90. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.ts +43 -0
  91. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.js +81 -0
  92. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.ts +116 -0
  93. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.js +92 -0
  94. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.ts +137 -0
  95. package/src/lib/MarkdownDocs/ai/aiProvider.ts +31 -0
  96. package/src/lib/MarkdownDocs/ai/characters.js +52 -0
  97. package/src/lib/MarkdownDocs/ai/characters.ts +69 -0
  98. package/src/lib/MarkdownDocs/ai/chunkStore.ts +25 -0
  99. package/src/lib/MarkdownDocs/ai/chunker.js +85 -0
  100. package/src/lib/MarkdownDocs/ai/chunker.ts +135 -0
  101. package/src/lib/MarkdownDocs/ai/docLinker.js +26 -0
  102. package/src/lib/MarkdownDocs/ai/docLinker.ts +36 -0
  103. package/src/lib/MarkdownDocs/ai/promptBuilder.js +33 -0
  104. package/src/lib/MarkdownDocs/ai/promptBuilder.ts +53 -0
  105. package/src/lib/MarkdownDocs/ai/ragPipeline.js +54 -0
  106. package/src/lib/MarkdownDocs/ai/ragPipeline.ts +106 -0
  107. package/src/lib/MarkdownDocs/ai/stores/memoryStore.js +88 -0
  108. package/src/lib/MarkdownDocs/ai/stores/memoryStore.ts +112 -0
  109. package/src/lib/MarkdownDocs/ai/stores/sqliteStore.ts +372 -0
  110. package/src/lib/MarkdownDocs/ai/types.ts +71 -0
  111. package/src/lib/MarkdownDocs/aiServer.js +288 -0
  112. package/src/lib/MarkdownDocs/codeDirectives.js +191 -0
  113. package/src/lib/MarkdownDocs/codeFenceInfo.js +45 -0
  114. package/src/lib/MarkdownDocs/codeGroup.ts +46 -0
  115. package/src/lib/MarkdownDocs/common.ts +47 -0
  116. package/src/lib/MarkdownDocs/docsUtils.js +281 -0
  117. package/src/lib/MarkdownDocs/index.plugins.js +22 -0
  118. package/src/lib/MarkdownDocs/layouts/LayoutDoc.svelte +8 -0
  119. package/src/lib/MarkdownDocs/layouts/LayoutHome.svelte +58 -0
  120. package/src/lib/MarkdownDocs/layouts/LayoutPage.svelte +9 -0
  121. package/src/lib/MarkdownDocs/loadGregConfig.js +82 -0
  122. package/src/lib/MarkdownDocs/localeUtils.ts +682 -0
  123. package/src/lib/MarkdownDocs/markdownRendererRuntime.ts +314 -0
  124. package/src/lib/MarkdownDocs/mermaidThemes.js +319 -0
  125. package/src/lib/MarkdownDocs/navigationUtils.js +22 -0
  126. package/src/lib/MarkdownDocs/rehypeCodeGroup.js +326 -0
  127. package/src/lib/MarkdownDocs/rehypeCodeTitle.js +96 -0
  128. package/src/lib/MarkdownDocs/rehypeToc.js +170 -0
  129. package/src/lib/MarkdownDocs/remarkCodeMeta.js +22 -0
  130. package/src/lib/MarkdownDocs/remarkContainers.js +329 -0
  131. package/src/lib/MarkdownDocs/remarkCustomAnchors.js +42 -0
  132. package/src/lib/MarkdownDocs/remarkEscapeSvelte.js +33 -0
  133. package/src/lib/MarkdownDocs/remarkGlobalComponents.js +65 -0
  134. package/src/lib/MarkdownDocs/remarkImports.js +461 -0
  135. package/src/lib/MarkdownDocs/remarkImportsBrowser.js +349 -0
  136. package/src/lib/MarkdownDocs/remarkInlineAttrs.js +95 -0
  137. package/src/lib/MarkdownDocs/remarkMathToHtml.js +138 -0
  138. package/src/lib/MarkdownDocs/searchIndexBuilder.js +497 -0
  139. package/src/lib/MarkdownDocs/searchServer.js +263 -0
  140. package/src/lib/MarkdownDocs/treeViewTypes.ts +11 -0
  141. package/src/lib/MarkdownDocs/useRouter.svelte.ts +114 -0
  142. package/src/lib/MarkdownDocs/useSplitter.svelte.ts +33 -0
  143. package/src/lib/MarkdownDocs/versioningDefaults.js +20 -0
  144. package/src/lib/MarkdownDocs/vitePluginAiServer.js +204 -0
  145. package/src/lib/MarkdownDocs/vitePluginCopyDocs.js +153 -0
  146. package/src/lib/MarkdownDocs/vitePluginFrontmatter.js +109 -0
  147. package/src/lib/MarkdownDocs/vitePluginGregConfig.js +108 -0
  148. package/src/lib/MarkdownDocs/vitePluginSearchIndex.js +57 -0
  149. package/src/lib/MarkdownDocs/vitePluginSearchServer.js +190 -0
  150. package/src/lib/components/Badge.svelte +59 -0
  151. package/src/lib/components/Button.svelte +138 -0
  152. package/src/lib/components/CarbonAds.svelte +99 -0
  153. package/src/lib/components/CodeGroup.svelte +102 -0
  154. package/src/lib/components/Feature.svelte +209 -0
  155. package/src/lib/components/Features.svelte +123 -0
  156. package/src/lib/components/Hero.svelte +399 -0
  157. package/src/lib/components/Image.svelte +128 -0
  158. package/src/lib/components/Link.svelte +105 -0
  159. package/src/lib/components/SocialLink.svelte +84 -0
  160. package/src/lib/components/SocialLinks.svelte +33 -0
  161. package/src/lib/components/Steps.svelte +143 -0
  162. package/src/lib/components/TeamMember.svelte +273 -0
  163. package/src/lib/components/TeamMembers.svelte +81 -0
  164. package/src/lib/components/TeamPage.svelte +65 -0
  165. package/src/lib/components/TeamPageSection.svelte +108 -0
  166. package/src/lib/components/TeamPageTitle.svelte +89 -0
  167. package/src/lib/components/index.js +24 -0
  168. package/src/lib/portal/context.js +12 -0
  169. package/src/lib/portal/index.js +3 -0
  170. package/src/lib/portal/portal.svelte +14 -0
  171. package/src/lib/portal/slot.svelte +8 -0
  172. package/src/lib/scss/__code.scss +128 -0
  173. package/src/lib/scss/__containers.scss +99 -0
  174. package/src/lib/scss/__markdown.scss +447 -0
  175. package/src/lib/scss/__scrollbar.scss +60 -0
  176. package/src/lib/scss/__steps.scss +100 -0
  177. package/src/lib/scss/__theme.scss +238 -0
  178. package/src/lib/scss/__toc.scss +55 -0
  179. package/src/lib/scss/__utilities.scss +7 -0
  180. package/src/lib/scss/greg.scss +9 -0
  181. package/src/lib/spinner/spinner.svelte +42 -0
  182. package/svelte.config.js +146 -0
  183. package/types/index.d.ts +456 -0
@@ -0,0 +1,609 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/build-versions.js
4
+ *
5
+ * Build multiple documentation versions using one of two strategies:
6
+ * - branches (default): extract docs snapshots from git refs
7
+ * - folders: read docs directly from configured directories
8
+ *
9
+ * Produces output under `<outDir>/__versions/<version>` by default and writes
10
+ * `<outDir>/__versions/versions.json` manifest.
11
+ */
12
+
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { spawnSync } from 'node:child_process';
16
+ import { createHash } from 'node:crypto';
17
+ import { loadGregConfig } from '../src/lib/MarkdownDocs/loadGregConfig.js';
18
+ import {
19
+ buildDefaultVersionPathPrefix,
20
+ DEFAULT_OUTPUT_ROOT,
21
+ DEFAULT_OUTPUT_BASE_DIR,
22
+ DEFAULT_PATH_PREFIX,
23
+ DEFAULT_VERSIONS_DIR_NAME,
24
+ LEGACY_VERSIONS_DIR_NAME,
25
+ normalizeBasePath,
26
+ } from '../src/lib/MarkdownDocs/versioningDefaults.js';
27
+
28
+ const PROJECT_ROOT = process.cwd();
29
+
30
+ function parseArgs(argv) {
31
+ const out = {
32
+ strategy: '',
33
+ cleanCache: false,
34
+ cleanVersions: false,
35
+ rebuildAll: false,
36
+ passthrough: [],
37
+ };
38
+
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const arg = argv[i];
41
+ if (arg === '--strategy' && argv[i + 1]) {
42
+ out.strategy = String(argv[++i]);
43
+ continue;
44
+ }
45
+ if (arg === '--clean-cache') {
46
+ out.cleanCache = true;
47
+ continue;
48
+ }
49
+ if (arg === '--clean-versions') {
50
+ out.cleanVersions = true;
51
+ continue;
52
+ }
53
+ if (arg === '--rebuild-all') {
54
+ out.rebuildAll = true;
55
+ continue;
56
+ }
57
+ out.passthrough.push(arg);
58
+ }
59
+
60
+ return out;
61
+ }
62
+
63
+ function normalizeSrcDir(value, fallback = '/') {
64
+ const cleaned = String(value || fallback).trim().replace(/^\/+|\/+$/g, '');
65
+ return '/' + cleaned;
66
+ }
67
+
68
+ function normalizePathPrefix(value, fallback = DEFAULT_PATH_PREFIX) {
69
+ const cleaned = String(value || fallback).trim().replace(/^\/+|\/+$/g, '');
70
+ return '/' + (cleaned || DEFAULT_VERSIONS_DIR_NAME);
71
+ }
72
+
73
+ function normalizeDocsBase(value, fallback = '/') {
74
+ const cleaned = String(value ?? fallback).trim().replace(/^\/+|\/+$/g, '');
75
+ return '/' + cleaned;
76
+ }
77
+
78
+ function sanitizeSegment(value, fallback = 'item') {
79
+ const cleaned = String(value || '').trim();
80
+ const safe = cleaned.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
81
+ return safe || fallback;
82
+ }
83
+
84
+ function ensureDir(dirPath) {
85
+ fs.mkdirSync(dirPath, { recursive: true });
86
+ }
87
+
88
+ function removeDir(dirPath) {
89
+ fs.rmSync(dirPath, { recursive: true, force: true });
90
+ }
91
+
92
+ function copyDir(src, dst) {
93
+ removeDir(dst);
94
+ ensureDir(path.dirname(dst));
95
+ fs.cpSync(src, dst, { recursive: true, force: true });
96
+ }
97
+
98
+ function overlayDir(src, dst) {
99
+ ensureDir(dst);
100
+ fs.cpSync(src, dst, { recursive: true, force: true });
101
+ }
102
+
103
+ function mergeVersionAssetsIntoHostingRoot(versionOutDir, hostingRoot) {
104
+ const versionAssetsDir = path.join(versionOutDir, 'assets');
105
+ if (!fs.existsSync(versionAssetsDir)) return;
106
+ const hostingAssetsDir = path.join(hostingRoot, 'assets');
107
+ overlayDir(versionAssetsDir, hostingAssetsDir);
108
+ }
109
+
110
+ function runCommand(command, args, options = {}) {
111
+ const result = spawnSync(command, args, {
112
+ stdio: options.stdio ?? 'pipe',
113
+ encoding: options.encoding,
114
+ shell: options.shell ?? false,
115
+ env: options.env ?? process.env,
116
+ maxBuffer: options.maxBuffer ?? 64 * 1024 * 1024,
117
+ cwd: options.cwd ?? PROJECT_ROOT,
118
+ });
119
+
120
+ const status = result.status ?? 0;
121
+ if (!options.allowFailure && status !== 0) {
122
+ const details = (result.stderr || result.stdout || '').toString().trim();
123
+ throw new Error(details || `${command} failed with exit code ${status}`);
124
+ }
125
+ return result;
126
+ }
127
+
128
+ function git(args, options = {}) {
129
+ return runCommand('git', args, {
130
+ ...options,
131
+ shell: false,
132
+ encoding: options.encoding ?? 'utf8',
133
+ });
134
+ }
135
+
136
+ function collectFilesRecursively(absPath) {
137
+ if (!fs.existsSync(absPath)) return [];
138
+ const stat = fs.statSync(absPath);
139
+ if (stat.isFile()) return [absPath];
140
+ if (!stat.isDirectory()) return [];
141
+
142
+ const out = [];
143
+ const queue = [absPath];
144
+
145
+ while (queue.length > 0) {
146
+ const current = queue.pop();
147
+ const entries = fs.readdirSync(current, { withFileTypes: true });
148
+ for (const entry of entries) {
149
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
150
+ const full = path.join(current, entry.name);
151
+ if (entry.isDirectory()) {
152
+ queue.push(full);
153
+ } else if (entry.isFile()) {
154
+ out.push(full);
155
+ }
156
+ }
157
+ }
158
+
159
+ return out;
160
+ }
161
+
162
+ function computeWorkspaceBuildFingerprint() {
163
+ const hash = createHash('sha1');
164
+ const inputs = [
165
+ 'vite.config.js',
166
+ 'vite.config.ts',
167
+ 'greg.config.js',
168
+ 'greg.config.ts',
169
+ 'prv/greg.config.js',
170
+ 'package.json',
171
+ 'src/main.js',
172
+ 'src/App.svelte',
173
+ 'src/lib/MarkdownDocs',
174
+ 'scripts/generate-static.js',
175
+ ];
176
+
177
+ const files = inputs
178
+ .flatMap((item) => collectFilesRecursively(path.resolve(PROJECT_ROOT, item)))
179
+ .sort((a, b) => a.localeCompare(b));
180
+
181
+ if (files.length === 0) return 'no-workspace-inputs';
182
+
183
+ for (const filePath of files) {
184
+ const rel = path.relative(PROJECT_ROOT, filePath).replace(/\\/g, '/');
185
+ hash.update(rel);
186
+ hash.update('\0');
187
+ hash.update(fs.readFileSync(filePath));
188
+ hash.update('\0');
189
+ }
190
+
191
+ return hash.digest('hex').slice(0, 12);
192
+ }
193
+
194
+ function getCurrentBranch() {
195
+ const out = git(['rev-parse', '--abbrev-ref', 'HEAD']);
196
+ return String(out.stdout || '').trim();
197
+ }
198
+
199
+ function getBranchSha(refName) {
200
+ const out = git(['rev-parse', refName]);
201
+ return String(out.stdout || '').trim();
202
+ }
203
+
204
+ function ensureBranchDocsSnapshot(args) {
205
+ const { branch, sha, docsDir, branchCacheDir } = args;
206
+ const docsRel = String(docsDir || 'docs').replace(/^\/+|\/+$/g, '') || 'docs';
207
+ const branchKey = sanitizeSegment(branch, 'branch');
208
+ const cacheRoot = path.resolve(PROJECT_ROOT, branchCacheDir, 'sources', branchKey, sha);
209
+ const marker = path.join(cacheRoot, '.snapshot-ok');
210
+ const docsSnapshotDir = path.join(cacheRoot, docsRel);
211
+
212
+ if (fs.existsSync(marker) && fs.existsSync(docsSnapshotDir)) {
213
+ return { docsDir: docsSnapshotDir, cacheHit: true };
214
+ }
215
+
216
+ removeDir(cacheRoot);
217
+ ensureDir(cacheRoot);
218
+
219
+ const listed = git(['ls-tree', '-r', '--name-only', sha, '--', docsRel]);
220
+ const files = String(listed.stdout || '')
221
+ .split(/\r?\n/)
222
+ .map((v) => v.trim())
223
+ .filter(Boolean)
224
+ .filter((rel) => rel.endsWith('.md') || rel.startsWith(docsRel + '/'));
225
+
226
+ if (files.length === 0) {
227
+ throw new Error(`No files found for '${docsRel}' in ref '${branch}' (${sha.slice(0, 8)}).`);
228
+ }
229
+
230
+ for (const rel of files) {
231
+ const blob = spawnSync('git', ['show', `${sha}:${rel}`], {
232
+ cwd: PROJECT_ROOT,
233
+ shell: false,
234
+ encoding: null,
235
+ maxBuffer: 64 * 1024 * 1024,
236
+ });
237
+ if ((blob.status ?? 0) !== 0) continue;
238
+
239
+ const outPath = path.join(cacheRoot, rel);
240
+ ensureDir(path.dirname(outPath));
241
+ fs.writeFileSync(outPath, blob.stdout);
242
+ }
243
+
244
+ fs.writeFileSync(marker, new Date().toISOString() + '\n', 'utf8');
245
+ return { docsDir: docsSnapshotDir, cacheHit: false };
246
+ }
247
+
248
+ function runViteBuild(args) {
249
+ const { docsDir, srcDir, outDir, passthrough } = args;
250
+ const env = {
251
+ ...process.env,
252
+ GREG_DOCS_DIR: docsDir,
253
+ GREG_DOCS_BASE: srcDir,
254
+ GREG_ROOT_PATH: srcDir,
255
+ PATH:
256
+ path.resolve(PROJECT_ROOT, 'node_modules/.bin') +
257
+ (process.platform === 'win32' ? ';' : ':') +
258
+ (process.env.PATH ?? ''),
259
+ };
260
+
261
+ console.log(`[greg] versions: vite build -> ${path.relative(PROJECT_ROOT, outDir)}`);
262
+ runCommand('vite', ['build', '--outDir', outDir, ...passthrough], {
263
+ stdio: 'inherit',
264
+ shell: true,
265
+ env,
266
+ });
267
+
268
+ const staticScript = path.resolve(PROJECT_ROOT, 'scripts/generate-static.js');
269
+ runCommand(process.execPath, [
270
+ staticScript,
271
+ '--docsDir', docsDir,
272
+ '--srcDir', srcDir,
273
+ '--distDir', outDir,
274
+ ], {
275
+ stdio: 'inherit',
276
+ shell: false,
277
+ env,
278
+ });
279
+ }
280
+
281
+ function resolveFolderEntries(versioning, globalSrcDir) {
282
+ if (Array.isArray(versioning.folders) && versioning.folders.length > 0) {
283
+ return versioning.folders.map((entry, index) => {
284
+ const version = resolveVersionId(entry, `versioning.folders[${index}]`);
285
+ return {
286
+ version,
287
+ title: entry.title ? String(entry.title) : version,
288
+ docsDir: path.resolve(PROJECT_ROOT, String(entry.dir)),
289
+ srcDir: normalizeSrcDir(entry.srcDir, globalSrcDir),
290
+ };
291
+ });
292
+ }
293
+
294
+ const foldersDir = path.resolve(PROJECT_ROOT, versioning.foldersDir || 'versions');
295
+ if (!fs.existsSync(foldersDir)) return [];
296
+
297
+ const entries = fs.readdirSync(foldersDir, { withFileTypes: true })
298
+ .filter((d) => d.isDirectory() && !d.name.startsWith('.'))
299
+ .map((d) => {
300
+ const docsDir = path.join(foldersDir, d.name, 'docs');
301
+ return {
302
+ version: d.name,
303
+ title: d.name,
304
+ docsDir,
305
+ srcDir: normalizeSrcDir(globalSrcDir, '/docs'),
306
+ };
307
+ })
308
+ .filter((entry) => fs.existsSync(entry.docsDir));
309
+
310
+ return entries;
311
+ }
312
+
313
+ function resolveBranchEntries(versioning, globalSrcDir) {
314
+ const branchSources = Array.isArray(versioning.branches) ? versioning.branches : [];
315
+ if (branchSources.length > 0) {
316
+ return branchSources.map((entry, index) => {
317
+ const version = resolveVersionId(entry, `versioning.branches[${index}]`);
318
+ return {
319
+ version,
320
+ title: entry.title ? String(entry.title) : version,
321
+ branch: String(entry.branch),
322
+ docsDir: String(entry.docsDir || 'docs'),
323
+ srcDir: normalizeSrcDir(entry.srcDir, globalSrcDir),
324
+ };
325
+ });
326
+ }
327
+
328
+ const current = getCurrentBranch();
329
+ return [{
330
+ version: 'latest',
331
+ title: 'latest',
332
+ branch: current,
333
+ docsDir: 'docs',
334
+ srcDir: normalizeSrcDir(globalSrcDir, '/docs'),
335
+ }];
336
+ }
337
+
338
+ function resolveVersionId(entry, context) {
339
+ const raw = entry?.version;
340
+ const value = String(raw ?? '').trim();
341
+ if (!value) {
342
+ throw new Error(`Missing version id in ${context}. Use 'version'.`);
343
+ }
344
+ return value;
345
+ }
346
+
347
+ function assertNoKeys(obj, forbiddenKeys, context) {
348
+ if (!obj || typeof obj !== 'object') return;
349
+ for (const key of forbiddenKeys) {
350
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
351
+ throw new Error(`Unsupported key '${context}.${key}'. Remove it and use the current versioning schema.`);
352
+ }
353
+ }
354
+ }
355
+
356
+ function validateEntry(entry, context, mode) {
357
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
358
+ throw new Error(`${context} must be an object.`);
359
+ }
360
+
361
+ assertNoKeys(entry, ['id', 'label', 'current'], context);
362
+
363
+ const version = String(entry.version ?? '').trim();
364
+ if (!version) {
365
+ throw new Error(`${context}.version is required.`);
366
+ }
367
+
368
+ if (mode === 'branches') {
369
+ const branch = String(entry.branch ?? '').trim();
370
+ if (!branch) {
371
+ throw new Error(`${context}.branch is required for branch strategy.`);
372
+ }
373
+ }
374
+
375
+ if (mode === 'folders') {
376
+ const dir = String(entry.dir ?? '').trim();
377
+ if (!dir) {
378
+ throw new Error(`${context}.dir is required for folder strategy.`);
379
+ }
380
+ }
381
+ }
382
+
383
+ function validateVersioningConfig(versioning, strategy) {
384
+ if (!versioning || typeof versioning !== 'object' || Array.isArray(versioning)) {
385
+ throw new Error('versioning config must be an object.');
386
+ }
387
+
388
+ assertNoKeys(versioning, ['current'], 'versioning');
389
+
390
+ if (versioning.default != null && !String(versioning.default).trim()) {
391
+ throw new Error('versioning.default cannot be empty.');
392
+ }
393
+
394
+ if (versioning.aliases != null) {
395
+ if (typeof versioning.aliases !== 'object' || Array.isArray(versioning.aliases)) {
396
+ throw new Error('versioning.aliases must be an object map of alias -> version.');
397
+ }
398
+ for (const [alias, target] of Object.entries(versioning.aliases)) {
399
+ if (!String(alias).trim() || !String(target ?? '').trim()) {
400
+ throw new Error('versioning.aliases cannot contain empty alias names or targets.');
401
+ }
402
+ }
403
+ }
404
+
405
+ if (strategy === 'branches' && Array.isArray(versioning.branches)) {
406
+ versioning.branches.forEach((entry, index) => validateEntry(entry, `versioning.branches[${index}]`, 'branches'));
407
+ }
408
+
409
+ if (strategy === 'folders' && Array.isArray(versioning.folders)) {
410
+ versioning.folders.forEach((entry, index) => validateEntry(entry, `versioning.folders[${index}]`, 'folders'));
411
+ }
412
+ }
413
+
414
+ function buildAliasMap(aliasConfig, versions) {
415
+ if (!aliasConfig || typeof aliasConfig !== 'object' || Array.isArray(aliasConfig)) {
416
+ return {};
417
+ }
418
+
419
+ const versionIds = new Set(versions.map((v) => v.version));
420
+ const aliases = {};
421
+
422
+ for (const [aliasName, rawTarget] of Object.entries(aliasConfig)) {
423
+ const alias = String(aliasName || '').trim();
424
+ const target = String(rawTarget || '').trim();
425
+ if (!alias || !target) continue;
426
+
427
+ if (!versionIds.has(target)) {
428
+ throw new Error(`Alias '${alias}' points to '${target}', but no matching version exists.`);
429
+ }
430
+ aliases[alias] = target;
431
+ }
432
+
433
+ return aliases;
434
+ }
435
+
436
+ function resolveDefaultVersionId(manifest) {
437
+ const selected = String(manifest.default || '').trim();
438
+ if (!selected) return null;
439
+ return manifest.aliases[selected] || selected;
440
+ }
441
+
442
+ async function main() {
443
+ const args = parseArgs(process.argv.slice(2));
444
+ const config = await loadGregConfig();
445
+ const versioning = config.versioning ?? {};
446
+
447
+ const strategy = args.strategy || versioning.strategy || 'branches';
448
+ validateVersioningConfig(versioning, strategy);
449
+ const configuredDocsBase = Object.prototype.hasOwnProperty.call(config, 'docsBase')
450
+ ? config.docsBase
451
+ : '/';
452
+ const globalSrcDir = normalizeDocsBase(configuredDocsBase, '/');
453
+ const siteOutDir = String(config.outDir || DEFAULT_OUTPUT_BASE_DIR).trim() || DEFAULT_OUTPUT_BASE_DIR;
454
+ const siteBase = normalizeBasePath(config.base);
455
+ const defaultOutputRoot = path.join(siteOutDir, DEFAULT_VERSIONS_DIR_NAME);
456
+ const defaultPathPrefix = buildDefaultVersionPathPrefix(siteBase);
457
+
458
+ const versionPathPrefix = normalizePathPrefix(versioning.pathPrefix, defaultPathPrefix || DEFAULT_PATH_PREFIX);
459
+ const outputRoot = path.resolve(PROJECT_ROOT, versioning.outDir || defaultOutputRoot || DEFAULT_OUTPUT_ROOT);
460
+ const usingDefaultOutDir = !versioning.outDir;
461
+ const legacyOutputRoot = path.resolve(PROJECT_ROOT, path.dirname(outputRoot), LEGACY_VERSIONS_DIR_NAME);
462
+ const outputBaseName = path.basename(outputRoot).toLowerCase();
463
+ const defaultHostingRoot = (outputBaseName === LEGACY_VERSIONS_DIR_NAME || outputBaseName === DEFAULT_VERSIONS_DIR_NAME)
464
+ ? path.dirname(outputRoot)
465
+ : outputRoot;
466
+ const hostingRoot = path.resolve(PROJECT_ROOT, versioning.hostOutDir || defaultHostingRoot);
467
+ const workRoot = path.resolve(PROJECT_ROOT, '.greg/version-build');
468
+ const branchCacheDir = path.resolve(PROJECT_ROOT, versioning.branchCacheDir || '.greg/version-cache');
469
+ const workspaceBuildFingerprint = computeWorkspaceBuildFingerprint();
470
+
471
+ if (args.cleanCache) removeDir(branchCacheDir);
472
+ if (args.cleanVersions) removeDir(outputRoot);
473
+
474
+ if (usingDefaultOutDir && fs.existsSync(legacyOutputRoot) && legacyOutputRoot !== outputRoot) {
475
+ removeDir(legacyOutputRoot);
476
+ console.log(`[greg] versions: removed legacy output -> ${path.relative(PROJECT_ROOT, legacyOutputRoot)}`);
477
+ }
478
+
479
+ ensureDir(outputRoot);
480
+ ensureDir(workRoot);
481
+ ensureDir(branchCacheDir);
482
+
483
+ const manifest = {
484
+ default: versioning.default || null,
485
+ versions: [],
486
+ aliases: {},
487
+ };
488
+
489
+ if (strategy === 'folders') {
490
+ const entries = resolveFolderEntries(versioning, globalSrcDir);
491
+ if (entries.length === 0) {
492
+ throw new Error('No folder versions configured. Set versioning.folders or create versioning.foldersDir children.');
493
+ }
494
+
495
+ for (const entry of entries) {
496
+ if (!fs.existsSync(entry.docsDir)) {
497
+ throw new Error(`Folder docs source not found for version '${entry.version}': ${entry.docsDir}`);
498
+ }
499
+
500
+ const tempOut = path.join(workRoot, sanitizeSegment(entry.version));
501
+ removeDir(tempOut);
502
+ ensureDir(tempOut);
503
+
504
+ console.log(`[greg] versions: building '${entry.version}' from folder ${path.relative(PROJECT_ROOT, entry.docsDir)}`);
505
+ runViteBuild({
506
+ docsDir: entry.docsDir,
507
+ srcDir: entry.srcDir,
508
+ outDir: tempOut,
509
+ passthrough: args.passthrough,
510
+ });
511
+
512
+ const targetOut = path.join(outputRoot, entry.version);
513
+ copyDir(tempOut, targetOut);
514
+ mergeVersionAssetsIntoHostingRoot(targetOut, hostingRoot);
515
+
516
+ manifest.versions.push({
517
+ version: entry.version,
518
+ title: entry.title,
519
+ path: `${versionPathPrefix}/${entry.version}/`,
520
+ });
521
+ }
522
+ } else if (strategy === 'branches') {
523
+ const entries = resolveBranchEntries(versioning, globalSrcDir);
524
+ if (entries.length === 0) {
525
+ throw new Error('No branch versions configured. Set versioning.branches in greg.config.*');
526
+ }
527
+
528
+ for (const entry of entries) {
529
+ const sha = getBranchSha(entry.branch);
530
+ const snapshot = ensureBranchDocsSnapshot({
531
+ branch: entry.branch,
532
+ sha,
533
+ docsDir: entry.docsDir,
534
+ branchCacheDir,
535
+ });
536
+
537
+ const buildCache = path.join(
538
+ branchCacheDir,
539
+ 'builds',
540
+ sanitizeSegment(entry.version),
541
+ `${sha}-${workspaceBuildFingerprint}`,
542
+ );
543
+ const hasBuildCache = !args.rebuildAll && fs.existsSync(path.join(buildCache, 'index.html'));
544
+
545
+ if (!hasBuildCache) {
546
+ console.log(`[greg] versions: building '${entry.version}' from ${entry.branch} (${sha.slice(0, 8)})`);
547
+ runViteBuild({
548
+ docsDir: snapshot.docsDir,
549
+ srcDir: entry.srcDir,
550
+ outDir: buildCache,
551
+ passthrough: args.passthrough,
552
+ });
553
+ } else {
554
+ console.log(`[greg] versions: cache hit '${entry.version}' from ${entry.branch} (${sha.slice(0, 8)})`);
555
+ }
556
+
557
+ const targetOut = path.join(outputRoot, entry.version);
558
+ copyDir(buildCache, targetOut);
559
+ mergeVersionAssetsIntoHostingRoot(targetOut, hostingRoot);
560
+
561
+ manifest.versions.push({
562
+ version: entry.version,
563
+ title: entry.title,
564
+ path: `${versionPathPrefix}/${entry.version}/`,
565
+ });
566
+ }
567
+ } else {
568
+ throw new Error(`Unsupported versioning strategy '${strategy}'. Use 'branches' or 'folders'.`);
569
+ }
570
+
571
+ const seen = new Set();
572
+ for (const version of manifest.versions) {
573
+ if (seen.has(version.version)) {
574
+ throw new Error(`Duplicate version id '${version.version}' in versioning config.`);
575
+ }
576
+ seen.add(version.version);
577
+ }
578
+
579
+ manifest.aliases = buildAliasMap(versioning.aliases, manifest.versions);
580
+
581
+ if (!manifest.default && manifest.versions.length > 0) {
582
+ manifest.default = manifest.versions[0].version;
583
+ }
584
+
585
+ if (manifest.default && !seen.has(manifest.default) && !(manifest.default in manifest.aliases)) {
586
+ throw new Error(`Default version '${manifest.default}' does not match any version or alias.`);
587
+ }
588
+
589
+ const manifestPath = path.join(outputRoot, 'versions.json');
590
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
591
+
592
+ const defaultVersionId = resolveDefaultVersionId(manifest);
593
+ if (defaultVersionId) {
594
+ const defaultSourceDir = path.join(outputRoot, defaultVersionId);
595
+ if (!fs.existsSync(defaultSourceDir)) {
596
+ throw new Error(`Default version output not found: ${defaultSourceDir}`);
597
+ }
598
+ overlayDir(defaultSourceDir, hostingRoot);
599
+ console.log(`[greg] versions: default '${defaultVersionId}' synced to ${path.relative(PROJECT_ROOT, hostingRoot)}`);
600
+ }
601
+
602
+ console.log(`[greg] versions: done -> ${path.relative(PROJECT_ROOT, outputRoot)}`);
603
+ console.log(`[greg] versions: manifest -> ${path.relative(PROJECT_ROOT, manifestPath)}`);
604
+ }
605
+
606
+ main().catch((error) => {
607
+ console.error('[greg] versions failed:', error.message || error);
608
+ process.exit(1);
609
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * scripts/generate-static.js
3
+ *
4
+ * Post-build step: copies dist/index.html to dist/{route}/index.html for every
5
+ * known docs route, so the site works on static hosts without rewrite rules.
6
+ *
7
+ * Usage: node scripts/generate-static.js
8
+ * (Called automatically via the "postbuild" npm script.)
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ function parseArgs(argv) {
15
+ const out = { docsDir: 'docs', srcDir: '/', distDir: 'dist' };
16
+ for (let i = 0; i < argv.length; i++) {
17
+ const a = argv[i];
18
+ if (a === '--docsDir' && argv[i + 1]) out.docsDir = argv[++i];
19
+ if (a === '--srcDir' && argv[i + 1]) out.srcDir = argv[++i];
20
+ if (a === '--distDir' && argv[i + 1]) out.distDir = argv[++i];
21
+ }
22
+ return out;
23
+ }
24
+
25
+ const { docsDir, srcDir, distDir } = parseArgs(process.argv.slice(2));
26
+ const DIST = path.resolve(distDir);
27
+ const DOCS = path.resolve(docsDir);
28
+ const ROOT_PATH = srcDir;
29
+
30
+ // ── Collect routes from the docs/ folder ────────────────────────────────────
31
+
32
+ function collectRoutes(dir, base) {
33
+ const routes = [];
34
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
35
+ if (entry.name.startsWith('__')) continue;
36
+ const full = path.join(dir, entry.name);
37
+ if (entry.isDirectory()) {
38
+ routes.push(...collectRoutes(full, `${base}/${entry.name}`));
39
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
40
+ if (entry.name === 'index.md') {
41
+ // /docs/guide/index.md → /docs/guide
42
+ routes.push(base);
43
+ } else {
44
+ // /docs/guide/routing.md → /docs/guide/routing
45
+ routes.push(`${base}/${entry.name.replace(/\.md$/, '')}`);
46
+ }
47
+ }
48
+ }
49
+ return routes;
50
+ }
51
+
52
+ const routes = [...new Set([
53
+ ROOT_PATH, // /docs (root index)
54
+ ...collectRoutes(DOCS, ROOT_PATH),
55
+ ])];
56
+
57
+ // ── Copy index.html to each route ───────────────────────────────────────────
58
+
59
+ const src = path.join(DIST, 'index.html');
60
+ if (!fs.existsSync(src)) {
61
+ console.error('dist/index.html not found – run npm run build first.');
62
+ process.exit(1);
63
+ }
64
+
65
+ let count = 0;
66
+ for (const route of routes) {
67
+ const outDir = path.join(DIST, route);
68
+ const outFile = path.join(outDir, 'index.html');
69
+
70
+ // Skip if it's the actual dist root (already exists as dist/index.html)
71
+ if (path.resolve(outDir) === DIST) continue;
72
+
73
+ fs.mkdirSync(outDir, { recursive: true });
74
+ fs.copyFileSync(src, outFile);
75
+ count++;
76
+ console.log(` ✓ ${route}/index.html`);
77
+ }
78
+
79
+ console.log(`\nStatic export: ${count} routes written to dist/`);