@clidey/dory 1.0.8 → 1.9.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 (198) hide show
  1. package/README.md +12 -1
  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/dory.js +252 -402
  20. package/bin/dist/dory.js.map +1 -1
  21. package/bin/dist/i18n-init.d.ts +6 -0
  22. package/bin/dist/i18n-init.d.ts.map +1 -0
  23. package/bin/dist/i18n-init.js +56 -0
  24. package/bin/dist/i18n-init.js.map +1 -0
  25. package/bin/dist/mcp-http.d.ts +8 -0
  26. package/bin/dist/mcp-http.d.ts.map +1 -0
  27. package/bin/dist/mcp-http.js +72 -0
  28. package/bin/dist/mcp-http.js.map +1 -0
  29. package/bin/dist/mcp.d.ts +2 -0
  30. package/bin/dist/mcp.d.ts.map +1 -0
  31. package/bin/dist/mcp.js +81 -0
  32. package/bin/dist/mcp.js.map +1 -0
  33. package/bin/dist/preview-entry.d.ts +2 -0
  34. package/bin/dist/preview-entry.d.ts.map +1 -0
  35. package/bin/dist/preview-entry.js +6 -0
  36. package/bin/dist/preview-entry.js.map +1 -0
  37. package/bin/dist/preview-server.d.ts +5 -0
  38. package/bin/dist/preview-server.d.ts.map +1 -0
  39. package/bin/dist/preview-server.js +159 -0
  40. package/bin/dist/preview-server.js.map +1 -0
  41. package/bin/dist/project-config.d.ts +22 -0
  42. package/bin/dist/project-config.d.ts.map +1 -0
  43. package/bin/dist/project-config.js +44 -0
  44. package/bin/dist/project-config.js.map +1 -0
  45. package/dist/index.html +57 -0
  46. package/dist/runtime/dory-runtime.css +2 -0
  47. package/dist/runtime/dory-runtime.html +14 -0
  48. package/dist/runtime/dory-runtime.js +5490 -0
  49. package/docs/api-playground-demo.mdx +119 -0
  50. package/docs/api-reference/asyncapi.json +324 -0
  51. package/docs/api-reference/openapi.json +366 -0
  52. package/docs/api-reference/operations/create.mdx +5 -0
  53. package/docs/api-reference/operations/delete.mdx +5 -0
  54. package/docs/api-reference/operations/get.mdx +5 -0
  55. package/docs/api-reference/operations/update.mdx +5 -0
  56. package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
  57. package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
  58. package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
  59. package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
  60. package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
  61. package/docs/assets/dory.jpeg +0 -0
  62. package/docs/components/accordion.mdx +145 -0
  63. package/docs/components/callouts.mdx +134 -0
  64. package/docs/components/cards.mdx +153 -0
  65. package/docs/components/code-blocks.mdx +281 -0
  66. package/docs/components/expandable.mdx +189 -0
  67. package/docs/components/icons.mdx +71 -0
  68. package/docs/components/images.mdx +82 -0
  69. package/docs/components/latex.mdx +148 -0
  70. package/docs/components/layout.mdx +175 -0
  71. package/docs/components/lists.mdx +134 -0
  72. package/docs/components/mermaid.mdx +149 -0
  73. package/docs/components/overview.mdx +81 -0
  74. package/docs/components/source.mdx +130 -0
  75. package/docs/components/steps.mdx +122 -0
  76. package/docs/components/tables.mdx +114 -0
  77. package/docs/components/tags.mdx +179 -0
  78. package/docs/components/typography.mdx +185 -0
  79. package/docs/configuration/analytics.mdx +45 -0
  80. package/docs/configuration/chat.mdx +59 -0
  81. package/docs/configuration/dory-json.mdx +278 -0
  82. package/docs/configuration/embed.mdx +73 -0
  83. package/docs/configuration/fonts.mdx +86 -0
  84. package/docs/configuration/internationalization.mdx +61 -0
  85. package/docs/configuration/navigation.mdx +217 -0
  86. package/docs/configuration/theming.mdx +166 -0
  87. package/docs/dory.json +129 -0
  88. package/docs/embed-example.html +176 -0
  89. package/docs/favicon.svg +18 -0
  90. package/docs/features.mdx +440 -0
  91. package/docs/getting-started/automation.mdx +76 -0
  92. package/docs/getting-started/configuration.mdx +95 -0
  93. package/docs/getting-started/installation.mdx +88 -0
  94. package/docs/getting-started/quick-start.mdx +53 -0
  95. package/docs/getting-started/whodb-ee-apps.mdx +122 -0
  96. package/docs/introduction.mdx +50 -0
  97. package/docs/logo/logo.svg +18 -0
  98. package/docs/playgrounds/api-playground.mdx +122 -0
  99. package/docs/playgrounds/websocket-playground.mdx +98 -0
  100. package/docs/social-card.png +0 -0
  101. package/docs/social-card.svg +17 -0
  102. package/docs/websocket-playground-demo.mdx +135 -0
  103. package/package.json +23 -17
  104. package/scripts/verify-runtime-parity.mjs +146 -0
  105. package/src/app.css +19 -1
  106. package/src/assets/fonts/HankenGrotesk.ttf +0 -0
  107. package/src/assets/fonts/OFL.txt +94 -0
  108. package/src/components/chat-assistant.tsx +229 -0
  109. package/src/components/dropdown.tsx +3 -3
  110. package/src/components/error-boundary.tsx +2 -2
  111. package/src/components/hooks.tsx +20 -1
  112. package/src/components/navigation.tsx +82 -51
  113. package/src/components/notification.tsx +2 -3
  114. package/src/components/routes.tsx +17 -19
  115. package/src/components/search-index.test.ts +15 -0
  116. package/src/components/search-index.ts +49 -48
  117. package/src/components/search.tsx +208 -104
  118. package/src/components/store.ts +27 -9
  119. package/src/config/mdx-common.ts +11 -0
  120. package/src/config/mdx.ts +10 -11
  121. package/src/embed/loader.ts +0 -3
  122. package/src/embed/types.ts +0 -2
  123. package/src/embed/widget-iframe.ts +19 -21
  124. package/src/embedded-runtime/config.ts +138 -0
  125. package/src/embedded-runtime/index.tsx +680 -0
  126. package/src/embedded-runtime/runtime.html +14 -0
  127. package/src/entry-server.tsx +46 -124
  128. package/src/main.tsx +16 -4
  129. package/src/mdx/accordion.tsx +3 -3
  130. package/src/mdx/api-playground.tsx +18 -16
  131. package/src/mdx/async-api.tsx +2 -2
  132. package/src/mdx/card.tsx +3 -2
  133. package/src/mdx/code.tsx +11 -9
  134. package/src/mdx/fence.tsx +6 -5
  135. package/src/mdx/icon-grid.tsx +4 -1
  136. package/src/mdx/icon-names.test.ts +15 -0
  137. package/src/mdx/icon-names.ts +60 -0
  138. package/src/mdx/icon-source-variants.ts +102 -0
  139. package/src/mdx/icon.tsx +12 -73
  140. package/src/mdx/latex.tsx +3 -2
  141. package/src/mdx/mdx.tsx +1 -1
  142. package/src/mdx/mermaid.tsx +12 -6
  143. package/src/mdx/open-api.tsx +130 -74
  144. package/src/mdx/preprocess.test.ts +16 -0
  145. package/src/mdx/preprocess.ts +61 -0
  146. package/src/mdx/processor.ts +4 -96
  147. package/src/mdx/source.tsx +5 -4
  148. package/src/mdx/specimen.tsx +38 -6
  149. package/src/mdx/tabs.tsx +29 -21
  150. package/src/mdx/text.tsx +9 -9
  151. package/src/mdx/try-it-panel.tsx +284 -0
  152. package/src/mdx/type-scale.tsx +4 -4
  153. package/src/mdx/websocket-playground.tsx +21 -14
  154. package/src/plugins/analytics-injector.ts +8 -0
  155. package/src/plugins/chat-dev-server.ts +32 -0
  156. package/src/plugins/docs-assets.test.ts +25 -0
  157. package/src/plugins/docs-assets.ts +6 -2
  158. package/src/plugins/frontmatter-dev-server.ts +2 -2
  159. package/src/plugins/frontmatter-generator.ts +1 -1
  160. package/src/plugins/html-files-middleware.test.ts +21 -1
  161. package/src/plugins/html-files-middleware.ts +26 -6
  162. package/src/plugins/html-metadata-injector.ts +24 -4
  163. package/src/plugins/llm-txt-dev-server.ts +2 -2
  164. package/src/plugins/llm-txt-generator.ts +21 -4
  165. package/src/plugins/prerender.ts +47 -20
  166. package/src/plugins/robots-generator.ts +1 -1
  167. package/src/plugins/sanitize.ts +1 -1
  168. package/src/plugins/shared.ts +6 -18
  169. package/src/plugins/sitemap-generator.ts +1 -1
  170. package/src/plugins/social-preview.test.ts +71 -0
  171. package/src/plugins/social-preview.ts +87 -0
  172. package/src/plugins/unified-mdx-plugin.ts +1 -1
  173. package/src/theme.scss +630 -207
  174. package/src/types/config.ts +53 -1
  175. package/src/ui/breadcrumb.tsx +17 -0
  176. package/src/ui/docs-header-view.tsx +32 -0
  177. package/src/ui/docs-navigation-view.tsx +69 -0
  178. package/src/ui/docs-page-frame.tsx +50 -0
  179. package/src/ui/dory-mark.tsx +9 -0
  180. package/src/ui/header.tsx +33 -90
  181. package/src/ui/layout.tsx +99 -153
  182. package/src/ui/mobile-navigation.tsx +10 -8
  183. package/src/ui/page-actions.tsx +67 -0
  184. package/src/ui/page-feedback.tsx +50 -0
  185. package/src/ui/page-footer.tsx +23 -0
  186. package/src/ui/prev-next-link.tsx +15 -69
  187. package/src/ui/prev-next-view.tsx +27 -0
  188. package/src/ui/table-of-content-view.tsx +167 -0
  189. package/src/ui/table-of-content.tsx +25 -183
  190. package/src/utils/analytics.test.ts +36 -0
  191. package/src/utils/analytics.ts +35 -0
  192. package/src/utils/colors.ts +7 -0
  193. package/src/utils/content-scope.test.ts +26 -0
  194. package/src/utils/content-scope.ts +39 -0
  195. package/src/utils/search-content.ts +12 -0
  196. package/src/utils/theme.ts +1 -7
  197. package/vite.config.ssr.ts +3 -3
  198. package/vite.config.ts +21 -15
package/bin/dist/dory.js CHANGED
@@ -1,11 +1,15 @@
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, symlinkSync, 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';
9
13
  const __filename = fileURLToPath(import.meta.url);
10
14
  const __dirname = dirname(__filename);
11
15
  // Get the root directory of the Dory package
@@ -18,437 +22,260 @@ const getDoryRoot = () => {
18
22
  };
19
23
  // Get the user's current working directory
20
24
  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;
25
+ const workspaceFiles = [
26
+ 'src', 'bin', 'scripts', 'index.html', 'package.json', 'vite.config.ts',
27
+ 'vite.config.ssr.ts', 'vite.config.embed-loader.ts', 'vite.config.embed-widget.ts',
28
+ ];
29
+ const projectDocsDir = (userRoot) => {
30
+ const docsDir = resolve(userRoot, 'docs');
31
+ return existsSync(docsDir) ? docsDir : userRoot;
33
32
  };
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
- }
33
+ const findDependencyDir = (doryRoot) => {
34
+ let current = doryRoot;
35
+ while (true) {
36
+ const candidate = resolve(current, 'node_modules');
37
+ if (existsSync(resolve(candidate, 'vite', 'bin', 'vite.js')) && existsSync(resolve(candidate, 'preact', 'package.json')))
38
+ return candidate;
39
+ const parent = dirname(current);
40
+ if (parent === current)
41
+ break;
42
+ current = parent;
52
43
  }
44
+ throw new Error('Dory dependencies were not found. Install @clidey/dory in this project first.');
53
45
  };
54
- const validateDoryConfig = (config) => {
55
- if (!config || typeof config !== 'object') {
56
- throw new Error('Configuration must be a valid object');
46
+ const loadProjectEnv = (userRoot) => {
47
+ for (const name of ['.env', '.env.local']) {
48
+ const file = resolve(userRoot, name);
49
+ if (existsSync(file))
50
+ process.loadEnvFile(file);
57
51
  }
58
- if (config.basePath !== undefined && typeof config.basePath !== 'string') {
59
- throw new Error('"basePath" must be a string such as "/docs"');
52
+ };
53
+ const stageDocs = (userRoot, workspace) => {
54
+ const source = projectDocsDir(userRoot);
55
+ const destination = resolve(workspace, 'docs');
56
+ rmSync(destination, { recursive: true, force: true });
57
+ mkdirSync(destination, { recursive: true });
58
+ const excluded = new Set(['node_modules', 'dist', 'dist-ssr', '.git', '.github', '.DS_Store', '.env', '.env.local', 'pnpm-lock.yaml', 'package-lock.json', 'yarn.lock']);
59
+ for (const item of readdirSync(source)) {
60
+ if (excluded.has(item) || item.startsWith('.env.') || (source === userRoot && item === 'docs'))
61
+ continue;
62
+ cpSync(resolve(source, item), resolve(destination, item), {
63
+ recursive: true,
64
+ force: true,
65
+ filter: (path) => {
66
+ const name = basename(path);
67
+ if (excluded.has(name) || name.startsWith('.env.'))
68
+ return false;
69
+ if (!lstatSync(path).isSymbolicLink())
70
+ return true;
71
+ console.warn(`Skipping symbolic link in docs: ${path}`);
72
+ return false;
73
+ },
74
+ });
60
75
  }
76
+ cpSync(resolve(userRoot, 'dory.json'), resolve(destination, 'dory.json'), { force: true });
61
77
  };
62
- // Reads dory.json when present; preview must not fail without one.
63
- const readOptionalDoryConfig = (configPath) => {
64
- if (!existsSync(configPath))
65
- return null;
78
+ const createWorkspace = (userRoot) => {
79
+ const doryRoot = getDoryRoot();
80
+ const workspace = mkdtempSync(resolve(tmpdir(), 'dory-workspace-'));
66
81
  try {
67
- return JSON.parse(readFileSync(configPath, 'utf8'));
82
+ for (const item of workspaceFiles) {
83
+ const source = resolve(doryRoot, item);
84
+ if (!existsSync(source))
85
+ throw new Error(`Installed Dory package is missing ${item}`);
86
+ cpSync(source, resolve(workspace, item), { recursive: true, force: true });
87
+ }
88
+ symlinkSync(findDependencyDir(doryRoot), resolve(workspace, 'node_modules'), 'dir');
89
+ stageDocs(userRoot, workspace);
90
+ return workspace;
68
91
  }
69
- catch {
70
- return null;
92
+ catch (error) {
93
+ rmSync(workspace, { recursive: true, force: true });
94
+ throw error;
71
95
  }
72
96
  };
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
- };
97
+ const viteCli = (workspace) => resolve(workspace, 'node_modules', 'vite', 'bin', 'vite.js');
80
98
  const commands = {
81
- build: () => {
82
- console.log('🐟 Dory is ready to build your docs!');
99
+ reference: () => {
100
+ const root = resolve(getDoryRoot(), 'docs');
101
+ const page = process.argv[3];
102
+ console.log(page ? readPage(root, page) : listPages(root).join('\n'));
103
+ },
104
+ init: () => {
83
105
  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);
106
+ const configPath = resolve(userRoot, 'dory.json');
107
+ if (existsSync(configPath))
108
+ throw new Error('dory.json already exists in this directory');
109
+ const docsDir = resolve(userRoot, 'docs');
110
+ const introduction = resolve(docsDir, 'introduction.mdx');
111
+ if (existsSync(introduction))
112
+ throw new Error('docs/introduction.mdx already exists; initialization would overwrite it');
113
+ mkdirSync(docsDir, { recursive: true });
114
+ const name = basename(userRoot).replace(/[-_]/g, ' ').replace(/\b\w/g, letter => letter.toUpperCase());
115
+ const config = {
116
+ name,
117
+ description: `Documentation for ${name}`,
118
+ favicon: './logo.svg',
119
+ logo: { light: './logo.svg', dark: './logo.svg' },
120
+ navigation: { tabs: [{ tab: 'Guides', groups: [{ group: 'Getting started', pages: ['introduction'] }] }] },
174
121
  };
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);
122
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
123
+ writeFileSync(introduction, `---\ntitle: Introduction\ndescription: Get started with ${name}.\n---\n\n# Welcome to ${name}\n\nStart writing your documentation here.\n`);
124
+ const logoPath = resolve(docsDir, 'logo.svg');
125
+ if (!existsSync(logoPath))
126
+ 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');
127
+ console.log('Created dory.json and docs/introduction.mdx. Run `dory dev` to start writing.');
128
+ },
129
+ dev: () => {
130
+ const userRoot = getUserRoot();
131
+ const configPath = resolve(userRoot, 'dory.json');
132
+ if (!existsSync(configPath))
133
+ throw new Error('dory.json not found; run `dory init` first');
134
+ loadProjectEnv(userRoot);
135
+ const workspace = createWorkspace(userRoot);
136
+ const server = spawn(process.execPath, [viteCli(workspace)], {
137
+ cwd: workspace,
138
+ stdio: 'inherit',
139
+ env: { ...process.env, DORY_PROJECT_ROOT: userRoot },
140
+ });
141
+ let timer;
142
+ const sync = () => {
143
+ clearTimeout(timer);
144
+ timer = setTimeout(() => {
203
145
  try {
204
- cpSync(srcPath, destPath, { recursive: true, force: true });
205
- copiedCount++;
146
+ stageDocs(userRoot, workspace);
206
147
  }
207
148
  catch (error) {
208
- console.warn(`⚠️ Could not copy ${item}: ${error instanceof Error ? error.message : 'unknown error'}`);
149
+ console.error('Could not sync documentation:', error);
209
150
  }
151
+ }, 100);
152
+ };
153
+ const watchers = [watch(userRoot, (_event, file) => { if (file === 'dory.json')
154
+ sync(); })];
155
+ const docsDir = resolve(userRoot, 'docs');
156
+ if (existsSync(docsDir))
157
+ watchers.push(watch(docsDir, { recursive: true }, sync));
158
+ else
159
+ watchers.push(watch(userRoot, { recursive: true }, (_event, file) => {
160
+ if (file && !file.startsWith('dist/') && !file.startsWith('node_modules/'))
161
+ sync();
162
+ }));
163
+ const cleanup = () => {
164
+ clearTimeout(timer);
165
+ watchers.forEach(watcher => watcher.close());
166
+ if (!server.killed)
167
+ server.kill('SIGTERM');
168
+ rmSync(workspace, { recursive: true, force: true });
169
+ };
170
+ server.once('exit', code => { cleanup(); process.exitCode = code || 0; });
171
+ process.once('SIGINT', () => { cleanup(); process.exit(130); });
172
+ process.once('SIGTERM', () => { cleanup(); process.exit(143); });
173
+ },
174
+ check: () => {
175
+ const result = checkProject(getUserRoot());
176
+ console.log(formatCheckResult(result));
177
+ if (result.errors > 0)
178
+ process.exitCode = 1;
179
+ },
180
+ 'i18n:init': () => {
181
+ const locale = process.argv[3];
182
+ if (!locale)
183
+ throw new Error('Usage: dory i18n:init <locale>');
184
+ const result = initLocale(getUserRoot(), locale);
185
+ console.log(`Created ${result.templatesCreated} translation template(s) in ${result.directory}.`);
186
+ console.log('Translate each .mdx.template file, then rename it to .mdx to publish.');
187
+ },
188
+ inspect: () => {
189
+ console.log(JSON.stringify(projectInfo(getUserRoot()), null, 2));
190
+ },
191
+ discover: async () => {
192
+ const url = process.argv[3];
193
+ if (!url)
194
+ throw new Error('Usage: dory discover <site-url>');
195
+ console.log(JSON.stringify(await discoverSite(url), null, 2));
196
+ },
197
+ screenshot: async () => {
198
+ const args = process.argv.slice(3);
199
+ const value = (flag) => {
200
+ const index = args.indexOf(flag);
201
+ return index < 0 ? undefined : args[index + 1];
202
+ };
203
+ const width = value('--width');
204
+ const height = value('--height');
205
+ const theme = value('--theme');
206
+ if (theme && theme !== 'light' && theme !== 'dark')
207
+ throw new Error('--theme must be light or dark');
208
+ const result = await screenshot(getUserRoot(), {
209
+ route: value('--route'), output: value('--output'),
210
+ width: width ? Number(width) : undefined,
211
+ height: height ? Number(height) : undefined,
212
+ theme: theme,
213
+ });
214
+ console.log(`Screenshot saved: ${result.output}`);
215
+ },
216
+ mcp: () => {
217
+ const action = process.argv[3];
218
+ if (action === 'serve') {
219
+ const projectIndex = process.argv.indexOf('--project', 4);
220
+ if (projectIndex >= 0)
221
+ process.env.DORY_PROJECT_ROOT = resolve(process.argv[projectIndex + 1]);
222
+ const version = JSON.parse(readFileSync(resolve(getDoryRoot(), 'package.json'), 'utf8')).version;
223
+ return serveMcp(__filename, version);
224
+ }
225
+ if (action === 'config') {
226
+ console.log(JSON.stringify({ mcpServers: { dory: { command: process.execPath, args: [__filename, 'mcp', 'serve'], env: { DORY_PROJECT_ROOT: getUserRoot() } } } }, null, 2));
227
+ return;
228
+ }
229
+ throw new Error('Usage: dory mcp serve | dory mcp config');
230
+ },
231
+ build: () => {
232
+ const userRoot = getUserRoot();
233
+ const configPath = resolve(userRoot, 'dory.json');
234
+ if (!existsSync(configPath))
235
+ throw new Error(`dory.json not found: ${configPath}`);
236
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
237
+ validateDoryConfig(config);
238
+ const workspace = createWorkspace(userRoot);
239
+ try {
240
+ console.log(`Building ${config.name || 'Documentation'} from an isolated workspace...`);
241
+ const runVite = (...args) => execFileSync(process.execPath, [viteCli(workspace), ...args], { cwd: workspace, stdio: 'inherit', env: process.env });
242
+ runVite('build');
243
+ runVite('build', '--config', 'vite.config.ssr.ts');
244
+ execFileSync(process.execPath, [resolve(workspace, 'scripts', 'render-routes.mjs')], { cwd: workspace, stdio: 'inherit' });
245
+ const distDir = resolve(workspace, 'dist');
246
+ if (!existsSync(resolve(distDir, 'index.html'))) {
247
+ throw new Error('Build did not produce dist/index.html');
210
248
  }
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
249
  const hasSsrContent = (dir) => {
249
250
  for (const item of readdirSync(dir, { withFileTypes: true })) {
250
251
  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
- }
252
+ if (item.isDirectory() && hasSsrContent(itemPath))
253
+ return true;
254
+ if (item.name.endsWith('.html') && /<div id="app">\s*<\w/.test(readFileSync(itemPath, 'utf8')))
255
+ return true;
259
256
  }
260
257
  return false;
261
258
  };
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 });
259
+ if (!hasSsrContent(distDir)) {
260
+ throw new Error('Build verification failed: no server-rendered route content was found');
274
261
  }
275
- console.log('✨ Documentation ready in dist/');
276
- // Step 6.5: Build embed files
277
- console.log('📦 Building embed files...');
278
262
  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('');
263
+ runVite('build', '-c', 'vite.config.embed-loader.ts');
264
+ runVite('build', '-c', 'vite.config.embed-widget.ts');
319
265
  }
320
266
  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
- }
267
+ console.warn(`Embed build failed: ${error instanceof Error ? error.message : String(error)}`);
326
268
  }
269
+ const userDistDir = resolve(userRoot, 'dist');
270
+ rmSync(userDistDir, { recursive: true, force: true });
271
+ cpSync(distDir, userDistDir, { recursive: true });
272
+ console.log(`Documentation ready in ${userDistDir}`);
327
273
  }
328
274
  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);
275
+ rmSync(workspace, { recursive: true, force: true });
401
276
  }
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
277
  },
278
+ preview: () => startPreview({ version: JSON.parse(readFileSync(resolve(getDoryRoot(), 'package.json'), 'utf8')).version }),
452
279
  'verify:content': async () => {
453
280
  const args = process.argv.slice(3);
454
281
  let content = '';
@@ -535,6 +362,20 @@ Usage:
535
362
  dory <command> [options]
536
363
 
537
364
  Commands:
365
+ init Create dory.json and starter docs in the current directory
366
+ dev Start a live development server for this project
367
+ check Check navigation, links, and content before publishing
368
+ i18n:init <code> Create translation templates for a locale
369
+ reference [page] List or read packaged Dory MDX reference pages
370
+ inspect Show dory.json and local MDX pages as JSON
371
+ discover <url> Find llms.txt and MDX source links from one hosted docs URL
372
+ screenshot Capture a built route as a full-page PNG
373
+ Options: --route <path> --output <file> --theme light|dark
374
+ --width <pixels> --height <pixels>
375
+ mcp serve Run the local MCP server over stdio
376
+ Option: --project <directory>
377
+ mcp config Print an MCP client config for this project
378
+
538
379
  build Build your documentation site
539
380
  Requirements: dory.json in current directory
540
381
  Output: dist/ folder with static site
@@ -552,6 +393,15 @@ Commands:
552
393
  help Show this help message
553
394
 
554
395
  Examples:
396
+ dory init
397
+ dory dev
398
+ dory check
399
+ dory i18n:init es
400
+ dory reference components/cards.mdx
401
+ dory inspect
402
+ dory discover https://docs.example.com
403
+ dory build && dory screenshot --route /getting-started --theme dark
404
+ dory mcp config
555
405
  dory build
556
406
  dory preview
557
407
  dory verify:content --content "# Hello World"