@clidey/dory 1.0.8 → 1.10.0

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 (210) hide show
  1. package/README.md +59 -164
  2. package/bin/chat-server.ts +270 -0
  3. package/bin/dist/analytics-server.d.ts +11 -0
  4. package/bin/dist/analytics-server.d.ts.map +1 -0
  5. package/bin/dist/analytics-server.js +121 -0
  6. package/bin/dist/analytics-server.js.map +1 -0
  7. package/bin/dist/automation.d.ts +35 -0
  8. package/bin/dist/automation.d.ts.map +1 -0
  9. package/bin/dist/automation.js +259 -0
  10. package/bin/dist/automation.js.map +1 -0
  11. package/bin/dist/chat-server.d.ts +8 -0
  12. package/bin/dist/chat-server.d.ts.map +1 -0
  13. package/bin/dist/chat-server.js +322 -0
  14. package/bin/dist/chat-server.js.map +1 -0
  15. package/bin/dist/check.d.ts +16 -0
  16. package/bin/dist/check.d.ts.map +1 -0
  17. package/bin/dist/check.js +247 -0
  18. package/bin/dist/check.js.map +1 -0
  19. package/bin/dist/dependency-overlay.d.ts +7 -0
  20. package/bin/dist/dependency-overlay.d.ts.map +1 -0
  21. package/bin/dist/dependency-overlay.js +137 -0
  22. package/bin/dist/dependency-overlay.js.map +1 -0
  23. package/bin/dist/dory.js +254 -402
  24. package/bin/dist/dory.js.map +1 -1
  25. package/bin/dist/i18n-init.d.ts +6 -0
  26. package/bin/dist/i18n-init.d.ts.map +1 -0
  27. package/bin/dist/i18n-init.js +56 -0
  28. package/bin/dist/i18n-init.js.map +1 -0
  29. package/bin/dist/mcp-http.d.ts +8 -0
  30. package/bin/dist/mcp-http.d.ts.map +1 -0
  31. package/bin/dist/mcp-http.js +72 -0
  32. package/bin/dist/mcp-http.js.map +1 -0
  33. package/bin/dist/mcp.d.ts +2 -0
  34. package/bin/dist/mcp.d.ts.map +1 -0
  35. package/bin/dist/mcp.js +81 -0
  36. package/bin/dist/mcp.js.map +1 -0
  37. package/bin/dist/preview-entry.d.ts +2 -0
  38. package/bin/dist/preview-entry.d.ts.map +1 -0
  39. package/bin/dist/preview-entry.js +6 -0
  40. package/bin/dist/preview-entry.js.map +1 -0
  41. package/bin/dist/preview-server.d.ts +5 -0
  42. package/bin/dist/preview-server.d.ts.map +1 -0
  43. package/bin/dist/preview-server.js +146 -0
  44. package/bin/dist/preview-server.js.map +1 -0
  45. package/bin/dist/preview-static.d.ts +7 -0
  46. package/bin/dist/preview-static.d.ts.map +1 -0
  47. package/bin/dist/preview-static.js +70 -0
  48. package/bin/dist/preview-static.js.map +1 -0
  49. package/bin/dist/project-config.d.ts +22 -0
  50. package/bin/dist/project-config.d.ts.map +1 -0
  51. package/bin/dist/project-config.js +44 -0
  52. package/bin/dist/project-config.js.map +1 -0
  53. package/dist/index.html +57 -0
  54. package/dist/runtime/dory-runtime.css +2 -0
  55. package/dist/runtime/dory-runtime.html +14 -0
  56. package/dist/runtime/dory-runtime.js +5490 -0
  57. package/docs/api-playground-demo.mdx +119 -0
  58. package/docs/api-reference/asyncapi.json +324 -0
  59. package/docs/api-reference/openapi.json +366 -0
  60. package/docs/api-reference/operations/create.mdx +5 -0
  61. package/docs/api-reference/operations/delete.mdx +5 -0
  62. package/docs/api-reference/operations/get.mdx +5 -0
  63. package/docs/api-reference/operations/update.mdx +5 -0
  64. package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
  65. package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
  66. package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
  67. package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
  68. package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
  69. package/docs/assets/dory.jpeg +0 -0
  70. package/docs/components/accordion.mdx +145 -0
  71. package/docs/components/callouts.mdx +134 -0
  72. package/docs/components/cards.mdx +153 -0
  73. package/docs/components/code-blocks.mdx +281 -0
  74. package/docs/components/expandable.mdx +189 -0
  75. package/docs/components/icons.mdx +71 -0
  76. package/docs/components/images.mdx +82 -0
  77. package/docs/components/latex.mdx +148 -0
  78. package/docs/components/layout.mdx +178 -0
  79. package/docs/components/lists.mdx +134 -0
  80. package/docs/components/mermaid.mdx +149 -0
  81. package/docs/components/overview.mdx +81 -0
  82. package/docs/components/source.mdx +130 -0
  83. package/docs/components/steps.mdx +122 -0
  84. package/docs/components/tables.mdx +114 -0
  85. package/docs/components/tags.mdx +179 -0
  86. package/docs/components/typography.mdx +185 -0
  87. package/docs/configuration/analytics.mdx +45 -0
  88. package/docs/configuration/chat.mdx +59 -0
  89. package/docs/configuration/dory-json.mdx +278 -0
  90. package/docs/configuration/embed.mdx +73 -0
  91. package/docs/configuration/fonts.mdx +86 -0
  92. package/docs/configuration/internationalization.mdx +61 -0
  93. package/docs/configuration/navigation.mdx +217 -0
  94. package/docs/configuration/theming.mdx +166 -0
  95. package/docs/contributing.mdx +47 -0
  96. package/docs/dory.json +136 -0
  97. package/docs/embed-example.html +176 -0
  98. package/docs/favicon.svg +18 -0
  99. package/docs/features.mdx +440 -0
  100. package/docs/getting-started/automation.mdx +76 -0
  101. package/docs/getting-started/configuration.mdx +95 -0
  102. package/docs/getting-started/custom-components.mdx +60 -0
  103. package/docs/getting-started/installation.mdx +88 -0
  104. package/docs/getting-started/quick-start.mdx +53 -0
  105. package/docs/introduction.mdx +50 -0
  106. package/docs/logo/logo.svg +18 -0
  107. package/docs/playgrounds/api-playground.mdx +122 -0
  108. package/docs/playgrounds/websocket-playground.mdx +98 -0
  109. package/docs/social-card.png +0 -0
  110. package/docs/social-card.svg +17 -0
  111. package/docs/websocket-playground-demo.mdx +135 -0
  112. package/package.json +23 -17
  113. package/scripts/verify-component-examples.mjs +126 -0
  114. package/scripts/verify-runtime-parity.mjs +147 -0
  115. package/src/app.css +22 -1
  116. package/src/assets/fonts/HankenGrotesk.ttf +0 -0
  117. package/src/assets/fonts/OFL.txt +94 -0
  118. package/src/components/chat-assistant.tsx +229 -0
  119. package/src/components/dropdown.tsx +3 -3
  120. package/src/components/error-boundary.tsx +2 -2
  121. package/src/components/hooks.tsx +20 -1
  122. package/src/components/navigation.tsx +82 -51
  123. package/src/components/notification.tsx +2 -3
  124. package/src/components/routes.tsx +17 -19
  125. package/src/components/search-index.test.ts +15 -0
  126. package/src/components/search-index.ts +49 -48
  127. package/src/components/search.tsx +208 -104
  128. package/src/components/store.ts +27 -9
  129. package/src/config/mdx-common.ts +11 -0
  130. package/src/config/mdx.ts +10 -11
  131. package/src/embed/loader.ts +0 -3
  132. package/src/embed/types.ts +0 -2
  133. package/src/embed/widget-iframe.ts +19 -21
  134. package/src/embedded-runtime/config.ts +138 -0
  135. package/src/embedded-runtime/index.tsx +680 -0
  136. package/src/embedded-runtime/runtime.html +14 -0
  137. package/src/entry-server.tsx +46 -124
  138. package/src/main.tsx +17 -5
  139. package/src/mdx/accordion.tsx +3 -3
  140. package/src/mdx/api-playground.tsx +18 -16
  141. package/src/mdx/async-api.tsx +2 -2
  142. package/src/mdx/card.tsx +3 -2
  143. package/src/mdx/code.tsx +11 -9
  144. package/src/mdx/fence.tsx +6 -5
  145. package/src/mdx/icon-grid.tsx +4 -1
  146. package/src/mdx/icon-names.test.ts +15 -0
  147. package/src/mdx/icon-names.ts +60 -0
  148. package/src/mdx/icon-source-variants.ts +102 -0
  149. package/src/mdx/icon.tsx +12 -73
  150. package/src/mdx/latex.test.ts +38 -0
  151. package/src/mdx/latex.tsx +42 -64
  152. package/src/mdx/mdx.tsx +1 -1
  153. package/src/mdx/mermaid.tsx +12 -6
  154. package/src/mdx/open-api.tsx +130 -74
  155. package/src/mdx/preprocess.test.ts +16 -0
  156. package/src/mdx/preprocess.ts +61 -0
  157. package/src/mdx/processor.test.ts +135 -37
  158. package/src/mdx/processor.ts +54 -92
  159. package/src/mdx/source.tsx +5 -4
  160. package/src/mdx/specimen.tsx +57 -43
  161. package/src/mdx/tabs.tsx +32 -21
  162. package/src/mdx/text.tsx +11 -11
  163. package/src/mdx/try-it-panel.tsx +284 -0
  164. package/src/mdx/type-scale.tsx +7 -7
  165. package/src/mdx/websocket-playground.tsx +21 -14
  166. package/src/plugins/analytics-injector.ts +8 -0
  167. package/src/plugins/chat-dev-server.ts +32 -0
  168. package/src/plugins/docs-assets.test.ts +25 -0
  169. package/src/plugins/docs-assets.ts +6 -2
  170. package/src/plugins/frontmatter-dev-server.ts +2 -2
  171. package/src/plugins/frontmatter-generator.ts +1 -1
  172. package/src/plugins/html-files-middleware.test.ts +21 -1
  173. package/src/plugins/html-files-middleware.ts +26 -6
  174. package/src/plugins/html-metadata-injector.ts +24 -4
  175. package/src/plugins/llm-txt-dev-server.ts +2 -2
  176. package/src/plugins/llm-txt-generator.ts +21 -4
  177. package/src/plugins/prerender.ts +47 -20
  178. package/src/plugins/robots-generator.ts +1 -1
  179. package/src/plugins/sanitize.ts +6 -5
  180. package/src/plugins/shared.ts +6 -18
  181. package/src/plugins/sitemap-generator.ts +1 -1
  182. package/src/plugins/social-preview.test.ts +71 -0
  183. package/src/plugins/social-preview.ts +87 -0
  184. package/src/plugins/unified-mdx-plugin.ts +1 -1
  185. package/src/theme.scss +630 -207
  186. package/src/types/config.ts +53 -1
  187. package/src/ui/breadcrumb.tsx +17 -0
  188. package/src/ui/docs-header-view.tsx +32 -0
  189. package/src/ui/docs-navigation-view.tsx +69 -0
  190. package/src/ui/docs-page-frame.tsx +50 -0
  191. package/src/ui/dory-mark.tsx +9 -0
  192. package/src/ui/header.tsx +33 -90
  193. package/src/ui/layout.tsx +99 -153
  194. package/src/ui/mobile-navigation.tsx +10 -8
  195. package/src/ui/page-actions.tsx +67 -0
  196. package/src/ui/page-feedback.tsx +50 -0
  197. package/src/ui/page-footer.tsx +23 -0
  198. package/src/ui/prev-next-link.tsx +15 -69
  199. package/src/ui/prev-next-view.tsx +27 -0
  200. package/src/ui/table-of-content-view.tsx +167 -0
  201. package/src/ui/table-of-content.tsx +25 -183
  202. package/src/utils/analytics.test.ts +36 -0
  203. package/src/utils/analytics.ts +35 -0
  204. package/src/utils/colors.ts +7 -0
  205. package/src/utils/content-scope.test.ts +26 -0
  206. package/src/utils/content-scope.ts +39 -0
  207. package/src/utils/search-content.ts +12 -0
  208. package/src/utils/theme.ts +1 -7
  209. package/vite.config.ssr.ts +4 -4
  210. package/vite.config.ts +22 -16
package/bin/dist/dory.js CHANGED
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { execSync } from 'child_process';
3
- import { existsSync, rmSync, mkdirSync, cpSync, readFileSync, readdirSync, writeFileSync } from 'fs';
2
+ import { execFileSync, spawn } from 'child_process';
3
+ import { existsSync, rmSync, mkdirSync, mkdtempSync, cpSync, readFileSync, readdirSync, lstatSync, writeFileSync, watch } from 'fs';
4
4
  import { resolve, dirname, basename } from 'path';
5
5
  import { tmpdir } from 'os';
6
6
  import { fileURLToPath, pathToFileURL } from 'url';
7
- import { createServer } from 'http';
8
- import sirv from 'sirv';
7
+ import { discoverSite, listPages, projectInfo, readPage, screenshot } from './automation.js';
8
+ import { serveMcp } from './mcp.js';
9
+ import { checkProject, formatCheckResult } from './check.js';
10
+ import { initLocale } from './i18n-init.js';
11
+ import { startPreview } from './preview-server.js';
12
+ import { validateDoryConfig } from './project-config.js';
13
+ import { createDependencyOverlay } from './dependency-overlay.js';
9
14
  const __filename = fileURLToPath(import.meta.url);
10
15
  const __dirname = dirname(__filename);
11
16
  // Get the root directory of the Dory package
@@ -18,437 +23,261 @@ const getDoryRoot = () => {
18
23
  };
19
24
  // Get the user's current working directory
20
25
  const getUserRoot = () => process.cwd();
21
- // Count files recursively in a directory (for backup verification)
22
- const countFiles = (dir) => {
23
- let count = 0;
24
- for (const item of readdirSync(dir, { withFileTypes: true })) {
25
- if (item.isDirectory()) {
26
- count += countFiles(resolve(dir, item.name));
27
- }
28
- else {
29
- count++;
30
- }
31
- }
32
- return count;
26
+ const workspaceFiles = [
27
+ 'src', 'bin', 'scripts', 'index.html', 'package.json', 'vite.config.ts',
28
+ 'vite.config.ssr.ts', 'vite.config.embed-loader.ts', 'vite.config.embed-widget.ts',
29
+ ];
30
+ const projectDocsDir = (userRoot) => {
31
+ const docsDir = resolve(userRoot, 'docs');
32
+ return existsSync(docsDir) ? docsDir : userRoot;
33
33
  };
34
- // Detect which package manager is available (prefer pnpm for speed, fallback to npm)
35
- const getPackageManager = () => {
36
- try {
37
- execSync('pnpm --version', { stdio: 'ignore' });
38
- return { run: 'pnpm run', exec: 'pnpm exec' };
39
- }
40
- catch {
41
- try {
42
- execSync('npm --version', { stdio: 'ignore' });
43
- return { run: 'npm run', exec: 'npx' };
44
- }
45
- catch {
46
- console.error('❌ No package manager found');
47
- console.error(' Install npm or pnpm to use Dory');
48
- console.error(' npm: https://nodejs.org/');
49
- console.error(' pnpm: npm install -g pnpm');
50
- process.exit(1);
51
- }
34
+ const findDependencyDir = (doryRoot) => {
35
+ let current = doryRoot;
36
+ while (true) {
37
+ const candidate = resolve(current, 'node_modules');
38
+ if (existsSync(resolve(candidate, 'vite', 'bin', 'vite.js')) && existsSync(resolve(candidate, 'preact', 'package.json')))
39
+ return candidate;
40
+ const parent = dirname(current);
41
+ if (parent === current)
42
+ break;
43
+ current = parent;
52
44
  }
45
+ throw new Error('Dory dependencies were not found. Install @clidey/dory in this project first.');
53
46
  };
54
- const validateDoryConfig = (config) => {
55
- if (!config || typeof config !== 'object') {
56
- throw new Error('Configuration must be a valid object');
47
+ const loadProjectEnv = (userRoot) => {
48
+ for (const name of ['.env', '.env.local']) {
49
+ const file = resolve(userRoot, name);
50
+ if (existsSync(file))
51
+ process.loadEnvFile(file);
57
52
  }
58
- if (config.basePath !== undefined && typeof config.basePath !== 'string') {
59
- throw new Error('"basePath" must be a string such as "/docs"');
53
+ };
54
+ const stageDocs = (userRoot, workspace) => {
55
+ const source = projectDocsDir(userRoot);
56
+ const destination = resolve(workspace, 'docs');
57
+ rmSync(destination, { recursive: true, force: true });
58
+ mkdirSync(destination, { recursive: true });
59
+ const excluded = new Set(['node_modules', 'dist', 'dist-ssr', '.git', '.github', '.DS_Store', '.env', '.env.local', 'pnpm-lock.yaml', 'package-lock.json', 'yarn.lock']);
60
+ for (const item of readdirSync(source)) {
61
+ if (excluded.has(item) || item.startsWith('.env.') || (source === userRoot && item === 'docs'))
62
+ continue;
63
+ cpSync(resolve(source, item), resolve(destination, item), {
64
+ recursive: true,
65
+ force: true,
66
+ filter: (path) => {
67
+ const name = basename(path);
68
+ if (excluded.has(name) || name.startsWith('.env.'))
69
+ return false;
70
+ if (!lstatSync(path).isSymbolicLink())
71
+ return true;
72
+ console.warn(`Skipping symbolic link in docs: ${path}`);
73
+ return false;
74
+ },
75
+ });
60
76
  }
77
+ cpSync(resolve(userRoot, 'dory.json'), resolve(destination, 'dory.json'), { force: true });
61
78
  };
62
- // Reads dory.json when present; preview must not fail without one.
63
- const readOptionalDoryConfig = (configPath) => {
64
- if (!existsSync(configPath))
65
- return null;
79
+ const createWorkspace = (userRoot) => {
80
+ const doryRoot = getDoryRoot();
81
+ const workspace = mkdtempSync(resolve(tmpdir(), 'dory-workspace-'));
66
82
  try {
67
- return JSON.parse(readFileSync(configPath, 'utf8'));
83
+ for (const item of workspaceFiles) {
84
+ const source = resolve(doryRoot, item);
85
+ if (!existsSync(source))
86
+ throw new Error(`Installed Dory package is missing ${item}`);
87
+ cpSync(source, resolve(workspace, item), { recursive: true, force: true });
88
+ }
89
+ createDependencyOverlay(resolve(workspace, 'node_modules'), findDependencyDir(doryRoot), userRoot, process.env.DORY_PACKAGE_OVERRIDES);
90
+ stageDocs(userRoot, workspace);
91
+ return workspace;
68
92
  }
69
- catch {
70
- return null;
93
+ catch (error) {
94
+ rmSync(workspace, { recursive: true, force: true });
95
+ throw error;
71
96
  }
72
97
  };
73
- // Same normalization as src/utils/base-path.ts (the CLI is compiled separately).
74
- const normalizeBasePath = (value) => {
75
- if (typeof value !== 'string')
76
- return '';
77
- const trimmed = value.trim().replace(/^\/+|\/+$/g, '');
78
- return trimmed ? `/${trimmed}` : '';
79
- };
98
+ const viteCli = (workspace) => resolve(workspace, 'node_modules', 'vite', 'bin', 'vite.js');
80
99
  const commands = {
81
- build: () => {
82
- console.log('🐟 Dory is ready to build your docs!');
100
+ capabilities: () => console.log(JSON.stringify({ customComponents: true, packageOverrides: true, specimenSourceLanguages: ['mdx', 'tsx', 'jsx'] })),
101
+ reference: () => {
102
+ const root = resolve(getDoryRoot(), 'docs');
103
+ const page = process.argv[3];
104
+ console.log(page ? readPage(root, page) : listPages(root).join('\n'));
105
+ },
106
+ init: () => {
83
107
  const userRoot = getUserRoot();
84
- const doryRoot = getDoryRoot();
85
- const doryConfigPath = resolve(userRoot, 'dory.json');
86
- const tempDocsDir = resolve(doryRoot, 'docs');
87
- const doryDistDir = resolve(doryRoot, 'dist');
88
- const userDistDir = resolve(userRoot, 'dist');
89
- // Step 1: Validate prerequisites before making any changes
90
- console.log('🔍 Validating project structure...');
91
- if (!existsSync(doryConfigPath)) {
92
- console.error('❌ dory.json not found in current directory');
93
- console.error(` Looking for: ${doryConfigPath}`);
94
- console.error(' Create a dory.json file to configure your documentation site');
95
- process.exit(1);
96
- }
97
- // Read and validate dory.json
98
- let config;
99
- try {
100
- const configContent = readFileSync(doryConfigPath, 'utf8');
101
- config = JSON.parse(configContent);
102
- validateDoryConfig(config);
103
- console.log(`📋 Building: ${config.name || 'Documentation'}`);
104
- }
105
- catch (error) {
106
- console.error('❌ Failed to parse dory.json:');
107
- if (error instanceof Error) {
108
- console.error(` ${error.message}`);
109
- }
110
- process.exit(1);
111
- }
112
- // The build stages user files into the package's own docs/ directory, so a
113
- // docs/ folder in the user's project cannot be included. Fail loudly
114
- // instead of silently dropping it.
115
- const userDocsDir = resolve(userRoot, 'docs');
116
- if (existsSync(userDocsDir) && userDocsDir !== tempDocsDir) {
117
- console.error('❌ Your project contains a docs/ directory');
118
- console.error(' Dory currently cannot include a top-level docs/ folder in the build');
119
- console.error(' Rename it (e.g. to guides/) and update dory.json navigation paths');
120
- process.exit(1);
121
- }
122
- // Detect package manager
123
- const pm = getPackageManager();
124
- // Guard against concurrent builds mutating the package's docs/ directory
125
- const lockPath = resolve(doryRoot, '.dory-build-lock');
126
- if (existsSync(lockPath)) {
127
- console.error('❌ Another dory build appears to be in progress');
128
- console.error(` Lock file: ${lockPath}`);
129
- console.error(' If no other build is running, delete the lock file and retry');
130
- process.exit(1);
131
- }
132
- writeFileSync(lockPath, String(process.pid), 'utf8');
133
- // Step 2: Backup existing docs directory to a safe temp location
134
- console.log('🧹 Preparing workspace...');
135
- const docsExistedBefore = existsSync(tempDocsDir);
136
- const safeBackupDir = docsExistedBefore
137
- ? resolve(tmpdir(), `dory-backup-${Date.now()}`)
138
- : null;
139
- if (docsExistedBefore && safeBackupDir) {
140
- console.log('💾 Backing up existing docs directory...');
141
- cpSync(tempDocsDir, safeBackupDir, { recursive: true, force: true });
142
- // Verify backup integrity by comparing file counts
143
- const origCount = countFiles(tempDocsDir);
144
- const backupCount = countFiles(safeBackupDir);
145
- if (origCount !== backupCount) {
146
- console.error(`❌ Backup verification failed (expected ${origCount} files, got ${backupCount})`);
147
- console.error(` Backup location: ${safeBackupDir}`);
148
- rmSync(lockPath, { force: true });
149
- process.exit(1);
150
- }
151
- rmSync(tempDocsDir, { recursive: true, force: true });
152
- }
153
- // If the process is interrupted while the package's docs/ is mutated,
154
- // restore the backup before exiting so no data is lost.
155
- const restoreOnSignal = () => {
156
- console.error('\n⚠️ Build interrupted — restoring docs directory...');
157
- try {
158
- if (docsExistedBefore && safeBackupDir && existsSync(safeBackupDir)) {
159
- rmSync(tempDocsDir, { recursive: true, force: true });
160
- cpSync(safeBackupDir, tempDocsDir, { recursive: true, force: true });
161
- rmSync(safeBackupDir, { recursive: true, force: true });
162
- }
163
- else if (!docsExistedBefore && existsSync(tempDocsDir)) {
164
- rmSync(tempDocsDir, { recursive: true, force: true });
165
- }
166
- }
167
- catch {
168
- if (safeBackupDir) {
169
- console.error(` Restore failed — backup preserved at: ${safeBackupDir}`);
170
- }
171
- }
172
- rmSync(lockPath, { force: true });
173
- process.exit(1);
108
+ const configPath = resolve(userRoot, 'dory.json');
109
+ if (existsSync(configPath))
110
+ throw new Error('dory.json already exists in this directory');
111
+ const docsDir = resolve(userRoot, 'docs');
112
+ const introduction = resolve(docsDir, 'introduction.mdx');
113
+ if (existsSync(introduction))
114
+ throw new Error('docs/introduction.mdx already exists; initialization would overwrite it');
115
+ mkdirSync(docsDir, { recursive: true });
116
+ const name = basename(userRoot).replace(/[-_]/g, ' ').replace(/\b\w/g, letter => letter.toUpperCase());
117
+ const config = {
118
+ name,
119
+ description: `Documentation for ${name}`,
120
+ favicon: './logo.svg',
121
+ logo: { light: './logo.svg', dark: './logo.svg' },
122
+ navigation: { tabs: [{ tab: 'Guides', groups: [{ group: 'Getting started', pages: ['introduction'] }] }] },
174
123
  };
175
- process.on('SIGINT', restoreOnSignal);
176
- process.on('SIGTERM', restoreOnSignal);
177
- try {
178
- // Step 3: Copy user files to temp docs directory
179
- mkdirSync(tempDocsDir, { recursive: true });
180
- console.log('📁 Gathering project files...');
181
- // Define files/dirs to exclude
182
- const excludeItems = new Set([
183
- 'node_modules',
184
- 'dist',
185
- '.git',
186
- '.github',
187
- 'docs',
188
- '.DS_Store',
189
- '.env',
190
- '.env.local',
191
- 'pnpm-lock.yaml',
192
- 'package-lock.json',
193
- 'yarn.lock'
194
- ]);
195
- const items = readdirSync(userRoot);
196
- let copiedCount = 0;
197
- for (const item of items) {
198
- if (excludeItems.has(item)) {
199
- continue;
200
- }
201
- const srcPath = resolve(userRoot, item);
202
- const destPath = resolve(tempDocsDir, item);
124
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
125
+ writeFileSync(introduction, `---\ntitle: Introduction\ndescription: Get started with ${name}.\n---\n\n# Welcome to ${name}\n\nStart writing your documentation here.\n`);
126
+ const logoPath = resolve(docsDir, 'logo.svg');
127
+ if (!existsSync(logoPath))
128
+ writeFileSync(logoPath, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#2C6BD4"/><text x="32" y="43" text-anchor="middle" fill="white" font-family="sans-serif" font-size="36" font-weight="700">D</text></svg>\n');
129
+ console.log('Created dory.json and docs/introduction.mdx. Run `dory dev` to start writing.');
130
+ },
131
+ dev: () => {
132
+ const userRoot = getUserRoot();
133
+ const configPath = resolve(userRoot, 'dory.json');
134
+ if (!existsSync(configPath))
135
+ throw new Error('dory.json not found; run `dory init` first');
136
+ loadProjectEnv(userRoot);
137
+ const workspace = createWorkspace(userRoot);
138
+ const server = spawn(process.execPath, [viteCli(workspace)], {
139
+ cwd: workspace,
140
+ stdio: 'inherit',
141
+ env: { ...process.env, DORY_PROJECT_ROOT: userRoot },
142
+ });
143
+ let timer;
144
+ const sync = () => {
145
+ clearTimeout(timer);
146
+ timer = setTimeout(() => {
203
147
  try {
204
- cpSync(srcPath, destPath, { recursive: true, force: true });
205
- copiedCount++;
148
+ stageDocs(userRoot, workspace);
206
149
  }
207
150
  catch (error) {
208
- console.warn(`⚠️ Could not copy ${item}: ${error instanceof Error ? error.message : 'unknown error'}`);
151
+ console.error('Could not sync documentation:', error);
209
152
  }
153
+ }, 100);
154
+ };
155
+ const watchers = [watch(userRoot, (_event, file) => { if (file === 'dory.json')
156
+ sync(); })];
157
+ const docsDir = resolve(userRoot, 'docs');
158
+ if (existsSync(docsDir))
159
+ watchers.push(watch(docsDir, { recursive: true }, sync));
160
+ else
161
+ watchers.push(watch(userRoot, { recursive: true }, (_event, file) => {
162
+ if (file && !file.startsWith('dist/') && !file.startsWith('node_modules/'))
163
+ sync();
164
+ }));
165
+ const cleanup = () => {
166
+ clearTimeout(timer);
167
+ watchers.forEach(watcher => watcher.close());
168
+ if (!server.killed)
169
+ server.kill('SIGTERM');
170
+ rmSync(workspace, { recursive: true, force: true });
171
+ };
172
+ server.once('exit', code => { cleanup(); process.exitCode = code || 0; });
173
+ process.once('SIGINT', () => { cleanup(); process.exit(130); });
174
+ process.once('SIGTERM', () => { cleanup(); process.exit(143); });
175
+ },
176
+ check: () => {
177
+ const result = checkProject(getUserRoot());
178
+ console.log(formatCheckResult(result));
179
+ if (result.errors > 0)
180
+ process.exitCode = 1;
181
+ },
182
+ 'i18n:init': () => {
183
+ const locale = process.argv[3];
184
+ if (!locale)
185
+ throw new Error('Usage: dory i18n:init <locale>');
186
+ const result = initLocale(getUserRoot(), locale);
187
+ console.log(`Created ${result.templatesCreated} translation template(s) in ${result.directory}.`);
188
+ console.log('Translate each .mdx.template file, then rename it to .mdx to publish.');
189
+ },
190
+ inspect: () => {
191
+ console.log(JSON.stringify(projectInfo(getUserRoot()), null, 2));
192
+ },
193
+ discover: async () => {
194
+ const url = process.argv[3];
195
+ if (!url)
196
+ throw new Error('Usage: dory discover <site-url>');
197
+ console.log(JSON.stringify(await discoverSite(url), null, 2));
198
+ },
199
+ screenshot: async () => {
200
+ const args = process.argv.slice(3);
201
+ const value = (flag) => {
202
+ const index = args.indexOf(flag);
203
+ return index < 0 ? undefined : args[index + 1];
204
+ };
205
+ const width = value('--width');
206
+ const height = value('--height');
207
+ const theme = value('--theme');
208
+ if (theme && theme !== 'light' && theme !== 'dark')
209
+ throw new Error('--theme must be light or dark');
210
+ const result = await screenshot(getUserRoot(), {
211
+ route: value('--route'), output: value('--output'),
212
+ width: width ? Number(width) : undefined,
213
+ height: height ? Number(height) : undefined,
214
+ theme: theme,
215
+ });
216
+ console.log(`Screenshot saved: ${result.output}`);
217
+ },
218
+ mcp: () => {
219
+ const action = process.argv[3];
220
+ if (action === 'serve') {
221
+ const projectIndex = process.argv.indexOf('--project', 4);
222
+ if (projectIndex >= 0)
223
+ process.env.DORY_PROJECT_ROOT = resolve(process.argv[projectIndex + 1]);
224
+ const version = JSON.parse(readFileSync(resolve(getDoryRoot(), 'package.json'), 'utf8')).version;
225
+ return serveMcp(__filename, version);
226
+ }
227
+ if (action === 'config') {
228
+ console.log(JSON.stringify({ mcpServers: { dory: { command: process.execPath, args: [__filename, 'mcp', 'serve'], env: { DORY_PROJECT_ROOT: getUserRoot() } } } }, null, 2));
229
+ return;
230
+ }
231
+ throw new Error('Usage: dory mcp serve | dory mcp config');
232
+ },
233
+ build: () => {
234
+ const userRoot = getUserRoot();
235
+ const configPath = resolve(userRoot, 'dory.json');
236
+ if (!existsSync(configPath))
237
+ throw new Error(`dory.json not found: ${configPath}`);
238
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
239
+ validateDoryConfig(config);
240
+ const workspace = createWorkspace(userRoot);
241
+ try {
242
+ console.log(`Building ${config.name || 'Documentation'} from an isolated workspace...`);
243
+ const runVite = (...args) => execFileSync(process.execPath, [viteCli(workspace), ...args], { cwd: workspace, stdio: 'inherit', env: process.env });
244
+ runVite('build');
245
+ runVite('build', '--config', 'vite.config.ssr.ts');
246
+ execFileSync(process.execPath, [resolve(workspace, 'scripts', 'render-routes.mjs')], { cwd: workspace, stdio: 'inherit' });
247
+ const distDir = resolve(workspace, 'dist');
248
+ if (!existsSync(resolve(distDir, 'index.html'))) {
249
+ throw new Error('Build did not produce dist/index.html');
210
250
  }
211
- console.log(`✅ Copied ${copiedCount} items`);
212
- // Verify critical files exist
213
- const doryJsonPath = resolve(tempDocsDir, 'dory.json');
214
- if (!existsSync(doryJsonPath)) {
215
- throw new Error(`dory.json was not copied to docs directory (expected: ${doryJsonPath})`);
216
- }
217
- // Step 4: Run the build
218
- console.log('⚡ Building documentation...');
219
- try {
220
- execSync(`${pm.run} build:docs`, {
221
- stdio: 'inherit',
222
- cwd: doryRoot,
223
- // Don't let corepack's strict packageManager pin fail the build for
224
- // users running a different pnpm version.
225
- env: { ...process.env, COREPACK_ENABLE_STRICT: '0' }
226
- });
227
- }
228
- catch (error) {
229
- console.error('❌ Build failed');
230
- console.error(' Check the error messages above for details');
231
- throw error;
232
- }
233
- // Step 5: Verify build output
234
- console.log('🔍 Verifying build output...');
235
- if (!existsSync(doryDistDir)) {
236
- throw new Error(`Build completed but dist folder was not created (expected: ${doryDistDir})`);
237
- }
238
- const indexHtml = resolve(doryDistDir, 'index.html');
239
- if (!existsSync(indexHtml)) {
240
- throw new Error('Build incomplete: index.html not found — the build may have failed silently');
241
- }
242
- // Check if dist has content
243
- const distFiles = readdirSync(doryDistDir);
244
- if (distFiles.length === 0) {
245
- throw new Error('Build completed but dist folder is empty');
246
- }
247
- // Verify SSR actually rendered content into at least one route
248
251
  const hasSsrContent = (dir) => {
249
252
  for (const item of readdirSync(dir, { withFileTypes: true })) {
250
253
  const itemPath = resolve(dir, item.name);
251
- if (item.isDirectory()) {
252
- if (hasSsrContent(itemPath))
253
- return true;
254
- }
255
- else if (item.name.endsWith('.html')) {
256
- if (/<div id="app">\s*<\w/.test(readFileSync(itemPath, 'utf8')))
257
- return true;
258
- }
254
+ if (item.isDirectory() && hasSsrContent(itemPath))
255
+ return true;
256
+ if (item.name.endsWith('.html') && /<div id="app">\s*<\w/.test(readFileSync(itemPath, 'utf8')))
257
+ return true;
259
258
  }
260
259
  return false;
261
260
  };
262
- if (!hasSsrContent(doryDistDir)) {
263
- throw new Error('Build verification failed: no route HTML contains server-rendered content in <div id="app">');
264
- }
265
- console.log('🎉 Build completed successfully!');
266
- // Step 6: Copy dist to user's directory
267
- console.log('📦 Copying build output...');
268
- // Only copy if they're different directories
269
- if (doryDistDir !== userDistDir) {
270
- if (existsSync(userDistDir)) {
271
- rmSync(userDistDir, { recursive: true, force: true });
272
- }
273
- cpSync(doryDistDir, userDistDir, { recursive: true, force: true });
261
+ if (!hasSsrContent(distDir)) {
262
+ throw new Error('Build verification failed: no server-rendered route content was found');
274
263
  }
275
- console.log('✨ Documentation ready in dist/');
276
- // Step 6.5: Build embed files
277
- console.log('📦 Building embed files...');
278
264
  try {
279
- // Build embed loader
280
- console.log(' Building embed loader...');
281
- execSync(`${pm.exec} vite build -c vite.config.embed-loader.ts`, {
282
- stdio: 'inherit',
283
- cwd: doryRoot,
284
- env: { ...process.env, COREPACK_ENABLE_STRICT: '0' }
285
- });
286
- // Build embed widget
287
- console.log(' Building embed widget...');
288
- execSync(`${pm.exec} vite build -c vite.config.embed-widget.ts`, {
289
- stdio: 'inherit',
290
- cwd: doryRoot,
291
- env: { ...process.env, COREPACK_ENABLE_STRICT: '0' }
292
- });
293
- // Copy embed files to user's dist if different
294
- if (doryDistDir !== userDistDir) {
295
- const embedFiles = ['embed.js', 'embed-widget.js', 'embed.css'];
296
- embedFiles.forEach(file => {
297
- const srcPath = resolve(doryDistDir, file);
298
- const destPath = resolve(userDistDir, file);
299
- if (existsSync(srcPath)) {
300
- cpSync(srcPath, destPath, { force: true });
301
- }
302
- });
303
- // Copy any chunk files (embed-*.js)
304
- const files = readdirSync(doryDistDir);
305
- files.forEach(file => {
306
- if (file.startsWith('embed-') && file.endsWith('.js')) {
307
- const srcPath = resolve(doryDistDir, file);
308
- const destPath = resolve(userDistDir, file);
309
- cpSync(srcPath, destPath, { force: true });
310
- }
311
- });
312
- }
313
- console.log('✨ Embed files built successfully!');
314
- console.log('');
315
- console.log(' 📝 Add to your site:');
316
- console.log(' <script src="https://your-docs.com/embed.js"></script>');
317
- console.log(' <button onclick="DoryDocs.open()">Help</button>');
318
- console.log('');
265
+ runVite('build', '-c', 'vite.config.embed-loader.ts');
266
+ runVite('build', '-c', 'vite.config.embed-widget.ts');
319
267
  }
320
268
  catch (error) {
321
- console.warn('⚠️ Embed build failed, but main build succeeded');
322
- console.warn(' Your documentation site is still available at dist/');
323
- if (error instanceof Error) {
324
- console.warn(` Error: ${error.message}`);
325
- }
269
+ console.warn(`Embed build failed: ${error instanceof Error ? error.message : String(error)}`);
326
270
  }
271
+ const userDistDir = resolve(userRoot, 'dist');
272
+ rmSync(userDistDir, { recursive: true, force: true });
273
+ cpSync(distDir, userDistDir, { recursive: true });
274
+ console.log(`Documentation ready in ${userDistDir}`);
327
275
  }
328
276
  finally {
329
- // Step 7: Always clean up and restore backup
330
- console.log('🧹 Cleaning up...');
331
- process.removeListener('SIGINT', restoreOnSignal);
332
- process.removeListener('SIGTERM', restoreOnSignal);
333
- if (docsExistedBefore && safeBackupDir && existsSync(safeBackupDir)) {
334
- console.log('📦 Restoring original docs directory...');
335
- try {
336
- if (existsSync(tempDocsDir)) {
337
- rmSync(tempDocsDir, { recursive: true, force: true });
338
- }
339
- cpSync(safeBackupDir, tempDocsDir, { recursive: true, force: true });
340
- // Verify restoration succeeded before deleting backup
341
- const restoredCount = countFiles(tempDocsDir);
342
- const backupCount = countFiles(safeBackupDir);
343
- if (restoredCount === backupCount) {
344
- rmSync(safeBackupDir, { recursive: true, force: true });
345
- }
346
- else {
347
- console.warn(`⚠️ Restore verification mismatch — backup preserved at: ${safeBackupDir}`);
348
- }
349
- }
350
- catch (error) {
351
- console.error('❌ Failed to restore docs directory!');
352
- console.error(` Backup is preserved at: ${safeBackupDir}`);
353
- console.error(' You can manually restore by running:');
354
- console.error(` cp -r "${safeBackupDir}" "${tempDocsDir}"`);
355
- if (error instanceof Error) {
356
- console.error(` Error: ${error.message}`);
357
- }
358
- }
359
- }
360
- else if (!docsExistedBefore) {
361
- try {
362
- if (existsSync(tempDocsDir)) {
363
- rmSync(tempDocsDir, { recursive: true, force: true });
364
- }
365
- }
366
- catch (error) {
367
- console.warn('⚠️ Could not remove temporary docs directory');
368
- }
369
- }
370
- // Clean up doryDistDir if it's different from userDistDir
371
- try {
372
- if (doryDistDir !== userDistDir && existsSync(doryDistDir)) {
373
- rmSync(doryDistDir, { recursive: true, force: true });
374
- }
375
- }
376
- catch (error) {
377
- console.warn('⚠️ Could not clean up dory dist directory');
378
- }
379
- rmSync(lockPath, { force: true });
380
- console.log('✅ Done!');
381
- }
382
- },
383
- preview: () => {
384
- console.log('👀 Starting docs preview...');
385
- const userRoot = getUserRoot();
386
- const distDir = resolve(userRoot, 'dist');
387
- const port = parseInt(process.env.PORT || '3000', 10);
388
- // Validate dist folder exists
389
- if (!existsSync(distDir)) {
390
- console.error('❌ dist folder not found');
391
- console.error(` Looking for: ${distDir}`);
392
- console.error(' Run "dory build" first to generate the documentation');
393
- process.exit(1);
394
- }
395
- // Check if dist has content
396
- const distFiles = readdirSync(distDir);
397
- if (distFiles.length === 0) {
398
- console.error('❌ dist folder is empty');
399
- console.error(' Run "dory build" to generate documentation');
400
- process.exit(1);
277
+ rmSync(workspace, { recursive: true, force: true });
401
278
  }
402
- const serve = sirv(distDir, {
403
- dev: false,
404
- single: true,
405
- etag: true,
406
- gzip: true,
407
- brotli: true,
408
- });
409
- // Mirror the production layout: a `basePath` build is served under that
410
- // prefix, so strip it before handing the request to sirv.
411
- const basePath = normalizeBasePath(readOptionalDoryConfig(resolve(userRoot, 'dory.json'))?.basePath);
412
- const maxAttempts = 20;
413
- const tryPort = (currentPort, attempt) => {
414
- // Fresh server per attempt — reusing one server object stacks listeners
415
- // and produces duplicate servers on retry.
416
- const server = createServer((req, res) => {
417
- if (basePath && req.url) {
418
- if (req.url === basePath || req.url.startsWith(`${basePath}/`) || req.url.startsWith(`${basePath}?`)) {
419
- req.url = req.url.slice(basePath.length) || '/';
420
- }
421
- else {
422
- res.statusCode = 302;
423
- res.setHeader('Location', `${basePath}/`);
424
- res.end();
425
- return;
426
- }
427
- }
428
- serve(req, res);
429
- });
430
- server.once('error', (err) => {
431
- if (err.code === 'EADDRINUSE' && attempt + 1 < maxAttempts) {
432
- console.log(`⚠️ Port ${currentPort} in use, trying ${currentPort + 1}...`);
433
- tryPort(currentPort + 1, attempt + 1);
434
- }
435
- else if (err.code === 'EADDRINUSE') {
436
- console.error(`❌ No free port found in range ${port}-${currentPort}`);
437
- process.exit(1);
438
- }
439
- else {
440
- console.error('❌ Failed to start server:', err.message);
441
- process.exit(1);
442
- }
443
- });
444
- server.once('listening', () => {
445
- console.log(`🚀 Documentation live at http://localhost:${currentPort}${basePath}`);
446
- console.log(' Press Ctrl+C to stop the server');
447
- });
448
- server.listen(currentPort);
449
- };
450
- tryPort(port, 0);
451
279
  },
280
+ preview: () => startPreview({ version: JSON.parse(readFileSync(resolve(getDoryRoot(), 'package.json'), 'utf8')).version }),
452
281
  'verify:content': async () => {
453
282
  const args = process.argv.slice(3);
454
283
  let content = '';
@@ -535,6 +364,20 @@ Usage:
535
364
  dory <command> [options]
536
365
 
537
366
  Commands:
367
+ init Create dory.json and starter docs in the current directory
368
+ dev Start a live development server for this project
369
+ check Check navigation, links, and content before publishing
370
+ i18n:init <code> Create translation templates for a locale
371
+ reference [page] List or read packaged Dory MDX reference pages
372
+ inspect Show dory.json and local MDX pages as JSON
373
+ discover <url> Find llms.txt and MDX source links from one hosted docs URL
374
+ screenshot Capture a built route as a full-page PNG
375
+ Options: --route <path> --output <file> --theme light|dark
376
+ --width <pixels> --height <pixels>
377
+ mcp serve Run the local MCP server over stdio
378
+ Option: --project <directory>
379
+ mcp config Print an MCP client config for this project
380
+
538
381
  build Build your documentation site
539
382
  Requirements: dory.json in current directory
540
383
  Output: dist/ folder with static site
@@ -552,6 +395,15 @@ Commands:
552
395
  help Show this help message
553
396
 
554
397
  Examples:
398
+ dory init
399
+ dory dev
400
+ dory check
401
+ dory i18n:init es
402
+ dory reference components/cards.mdx
403
+ dory inspect
404
+ dory discover https://docs.example.com
405
+ dory build && dory screenshot --route /getting-started --theme dark
406
+ dory mcp config
555
407
  dory build
556
408
  dory preview
557
409
  dory verify:content --content "# Hello World"