@docmd/core 0.4.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.
@@ -0,0 +1,353 @@
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
+
3
+ const http = require('http');
4
+ const WebSocket = require('ws');
5
+ const chokidar = require('chokidar');
6
+ const path = require('path');
7
+ const fs = require('../utils/fs-utils');
8
+ const chalk = require('chalk');
9
+ const os = require('os');
10
+ const readline = require('readline');
11
+ const { buildSite } = require('./build');
12
+ const { loadConfig } = require('../utils/config-loader');
13
+
14
+ // --- 1. Native Static File Server ---
15
+ const MIME_TYPES = {
16
+ '.html': 'text/html',
17
+ '.js': 'text/javascript',
18
+ '.css': 'text/css',
19
+ '.json': 'application/json',
20
+ '.png': 'image/png',
21
+ '.jpg': 'image/jpg',
22
+ '.jpeg': 'image/jpg',
23
+ '.gif': 'image/gif',
24
+ '.svg': 'image/svg+xml',
25
+ '.ico': 'image/x-icon',
26
+ '.woff': 'application/font-woff',
27
+ '.woff2': 'font/woff2',
28
+ '.ttf': 'application/font-ttf',
29
+ '.txt': 'text/plain',
30
+ };
31
+
32
+ async function serveStatic(req, res, rootDir) {
33
+ // Normalize path and remove query strings
34
+ let safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, '').split('?')[0].split('#')[0];
35
+ if (safePath === '/' || safePath === '\\') safePath = 'index.html';
36
+
37
+ let filePath = path.join(rootDir, safePath);
38
+
39
+ try {
40
+ let stats;
41
+ try {
42
+ stats = await fs.stat(filePath);
43
+ } catch (e) {
44
+ // If direct path fails, try appending .html (clean URLs support)
45
+ if (path.extname(filePath) === '') {
46
+ filePath += '.html';
47
+ stats = await fs.stat(filePath);
48
+ } else {
49
+ throw e;
50
+ }
51
+ }
52
+
53
+ if (stats.isDirectory()) {
54
+ filePath = path.join(filePath, 'index.html');
55
+ await fs.stat(filePath);
56
+ }
57
+
58
+ const ext = path.extname(filePath).toLowerCase();
59
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
60
+ const content = await fs.readFile(filePath);
61
+
62
+ res.writeHead(200, { 'Content-Type': contentType });
63
+
64
+ // Inject Live Reload Script into HTML files only
65
+ if (contentType === 'text/html') {
66
+ const htmlStr = content.toString('utf-8');
67
+ const liveReloadScript = `
68
+ <script>
69
+ (function() {
70
+ let socket;
71
+ let retryCount = 0;
72
+ const maxRetries = 50;
73
+
74
+ function connect() {
75
+ // Avoid connecting if already connected
76
+ if (socket && (socket.readyState === 0 || socket.readyState === 1)) return;
77
+
78
+ socket = new WebSocket('ws://' + window.location.host);
79
+
80
+ socket.onopen = () => {
81
+ console.log('⚡ docmd connected');
82
+ retryCount = 0;
83
+ };
84
+
85
+ socket.onmessage = (e) => {
86
+ if(e.data === 'reload') window.location.reload();
87
+ };
88
+
89
+ socket.onclose = () => {
90
+ // Exponential backoff for reconnection
91
+ if (retryCount < maxRetries) {
92
+ retryCount++;
93
+ const delay = Math.min(1000 * (1.5 ** retryCount), 5000);
94
+ setTimeout(connect, delay);
95
+ }
96
+ };
97
+
98
+ socket.onerror = (err) => {
99
+ // Ignore errors, let onclose handle retry
100
+ };
101
+ }
102
+ // Delay initial connection slightly to ensure page load
103
+ setTimeout(connect, 500);
104
+ })();
105
+ </script></body>`;
106
+ res.end(htmlStr.replace('</body>', liveReloadScript));
107
+ } else {
108
+ res.end(content);
109
+ }
110
+
111
+ } catch (err) {
112
+ if (err.code === 'ENOENT') {
113
+ console.log(chalk.yellow(`⚠️ 404 Not Found: ${req.url}`));
114
+ res.writeHead(404, { 'Content-Type': 'text/html' });
115
+ res.end('<h1>404 Not Found</h1><p>docmd dev server</p>');
116
+ } else {
117
+ res.writeHead(500);
118
+ res.end(`Server Error: ${err.code}`);
119
+ }
120
+ }
121
+ }
122
+
123
+ // --- 2. Helper Utilities ---
124
+
125
+ function formatPathForDisplay(absolutePath, cwd) {
126
+ const relativePath = path.relative(cwd, absolutePath);
127
+ if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
128
+ return `./${relativePath}`;
129
+ }
130
+ return relativePath;
131
+ }
132
+
133
+ function getNetworkIp() {
134
+ const interfaces = os.networkInterfaces();
135
+ for (const name of Object.keys(interfaces)) {
136
+ for (const iface of interfaces[name]) {
137
+ if (iface.family === 'IPv4' && !iface.internal) {
138
+ return iface.address;
139
+ }
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+
145
+ // --- 3. Main Dev Function ---
146
+
147
+ async function startDevServer(configPathOption, options = { preserve: false, port: undefined }) {
148
+ let config = await loadConfig(configPathOption);
149
+ const CWD = process.cwd();
150
+
151
+ // Config Fallback Logic
152
+ let actualConfigPath = path.resolve(CWD, configPathOption);
153
+ if (configPathOption === 'docmd.config.js' && !await fs.pathExists(actualConfigPath)) {
154
+ const legacyPath = path.resolve(CWD, 'config.js');
155
+ if (await fs.pathExists(legacyPath)) {
156
+ actualConfigPath = legacyPath;
157
+ }
158
+ }
159
+
160
+ const resolveConfigPaths = (currentConfig) => {
161
+ return {
162
+ outputDir: path.resolve(CWD, currentConfig.outputDir),
163
+ srcDirToWatch: path.resolve(CWD, currentConfig.srcDir),
164
+ configFileToWatch: actualConfigPath,
165
+ userAssetsDir: path.resolve(CWD, 'assets'),
166
+ };
167
+ };
168
+
169
+ let paths = resolveConfigPaths(config);
170
+ const DOCMD_ROOT = path.resolve(__dirname, '..');
171
+
172
+ // --- Create Native Server ---
173
+ const server = http.createServer((req, res) => {
174
+ serveStatic(req, res, paths.outputDir);
175
+ });
176
+
177
+ let wss; // WebSocket instance (initialized later)
178
+
179
+ function broadcastReload() {
180
+ if (wss) {
181
+ wss.clients.forEach((client) => {
182
+ if (client.readyState === WebSocket.OPEN) {
183
+ client.send('reload');
184
+ }
185
+ });
186
+ }
187
+ }
188
+
189
+ // --- Initial Build ---
190
+ console.log(chalk.blue('🚀 Performing initial build...'));
191
+ try {
192
+ await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true });
193
+ } catch (error) {
194
+ console.error(chalk.red('❌ Initial build failed:'), error.message);
195
+ }
196
+
197
+ // --- Watcher Setup ---
198
+ const userAssetsDirExists = await fs.pathExists(paths.userAssetsDir);
199
+ const watchedPaths = [paths.srcDirToWatch, paths.configFileToWatch];
200
+ if (userAssetsDirExists) watchedPaths.push(paths.userAssetsDir);
201
+
202
+ if (process.env.DOCMD_DEV === 'true') {
203
+ watchedPaths.push(
204
+ path.join(DOCMD_ROOT, 'templates'),
205
+ path.join(DOCMD_ROOT, 'assets'),
206
+ path.join(DOCMD_ROOT, 'core'),
207
+ path.join(DOCMD_ROOT, 'plugins')
208
+ );
209
+ }
210
+
211
+ console.log(chalk.dim('\n👀 Watching for changes in:'));
212
+ console.log(chalk.dim(` - Source: ${chalk.cyan(formatPathForDisplay(paths.srcDirToWatch, CWD))}`));
213
+ console.log(chalk.dim(` - Config: ${chalk.cyan(formatPathForDisplay(paths.configFileToWatch, CWD))}`));
214
+ if (userAssetsDirExists) {
215
+ console.log(chalk.dim(` - Assets: ${chalk.cyan(formatPathForDisplay(paths.userAssetsDir, CWD))}`));
216
+ }
217
+ console.log('');
218
+
219
+ const watcher = chokidar.watch(watchedPaths, {
220
+ ignored: /(^|[\/\\])\../,
221
+ persistent: true,
222
+ ignoreInitial: true,
223
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 }
224
+ });
225
+
226
+ watcher.on('all', async (event, filePath) => {
227
+ const relativeFilePath = path.relative(CWD, filePath);
228
+ process.stdout.write(chalk.dim(`↻ Change in ${relativeFilePath}... `));
229
+
230
+ try {
231
+ if (filePath === paths.configFileToWatch) {
232
+ config = await loadConfig(configPathOption);
233
+ // Note: With native server, we don't need to restart middleware,
234
+ // serveStatic reads from disk dynamically on every request.
235
+ paths = resolveConfigPaths(config);
236
+ }
237
+
238
+ await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true });
239
+ broadcastReload();
240
+ process.stdout.write(chalk.green('Done.\n'));
241
+
242
+ } catch (error) {
243
+ console.error(chalk.red('\n❌ Rebuild failed:'), error.message);
244
+ }
245
+ });
246
+
247
+ // --- Server Startup Logic (Port Checking) ---
248
+ const PORT = parseInt(options.port || process.env.PORT || 3000, 10);
249
+ const MAX_PORT_ATTEMPTS = 10;
250
+
251
+ function checkPortInUse(port) {
252
+ return new Promise((resolve) => {
253
+ const tester = http.createServer()
254
+ .once('error', (err) => {
255
+ if (err.code === 'EADDRINUSE') resolve(true);
256
+ else resolve(false);
257
+ })
258
+ .once('listening', () => {
259
+ tester.close(() => resolve(false));
260
+ })
261
+ .listen(port, '0.0.0.0');
262
+ });
263
+ }
264
+
265
+ function askUserConfirmation() {
266
+ return new Promise((resolve) => {
267
+ const rl = readline.createInterface({
268
+ input: process.stdin,
269
+ output: process.stdout
270
+ });
271
+
272
+ console.log(chalk.yellow(`\n⚠️ Port ${PORT} is already in use.`));
273
+ console.log(chalk.yellow(` Another instance of docmd (or another app) might be running.`));
274
+
275
+ rl.question(' Do you want to start another instance on a different port? (Y/n) ', (answer) => {
276
+ rl.close();
277
+ const isYes = answer.trim().toLowerCase() === 'y' || answer.trim() === '';
278
+ resolve(isYes);
279
+ });
280
+ });
281
+ }
282
+
283
+ function tryStartServer(port, attempt = 1) {
284
+ server.listen(port, '0.0.0.0')
285
+ .on('listening', async () => {
286
+ // Initialize WebSocket Server only AFTER successful listen
287
+ wss = new WebSocket.Server({ server });
288
+ wss.on('error', (e) => console.error('WebSocket Error:', e.message));
289
+
290
+ const indexHtmlPath = path.join(paths.outputDir, 'index.html');
291
+ const networkIp = getNetworkIp();
292
+
293
+ const localUrl = `http://127.0.0.1:${port}`;
294
+ const networkUrl = networkIp ? `http://${networkIp}:${port}` : null;
295
+
296
+ const border = chalk.gray('────────────────────────────────────────');
297
+ console.log(border);
298
+ console.log(` ${chalk.bold.green('SERVER RUNNING')} ${chalk.dim(`(v${require('../../package.json').version})`)}`);
299
+ console.log('');
300
+ console.log(` ${chalk.bold('Local:')} ${chalk.cyan(localUrl)}`);
301
+ if (networkUrl) {
302
+ console.log(` ${chalk.bold('Network:')} ${chalk.cyan(networkUrl)}`);
303
+ }
304
+ console.log('');
305
+ console.log(` ${chalk.dim('Serving:')} ${formatPathForDisplay(paths.outputDir, CWD)}`);
306
+ console.log(border);
307
+ console.log('');
308
+
309
+ if (!await fs.pathExists(indexHtmlPath)) {
310
+ console.warn(chalk.yellow(`⚠️ Warning: Root index.html not found.`));
311
+ }
312
+ })
313
+ .on('error', (err) => {
314
+ if (err.code === 'EADDRINUSE') {
315
+ server.close();
316
+ tryStartServer(port + 1);
317
+ } else {
318
+ console.error(chalk.red(`Failed to start server: ${err.message}`));
319
+ process.exit(1);
320
+ }
321
+ });
322
+ }
323
+
324
+ // --- Main Execution Flow ---
325
+ (async () => {
326
+ // Skip check if user manually specified port flag
327
+ if (options.port) {
328
+ tryStartServer(PORT);
329
+ return;
330
+ }
331
+
332
+ const isBusy = await checkPortInUse(PORT);
333
+
334
+ if (isBusy) {
335
+ const shouldProceed = await askUserConfirmation();
336
+ if (!shouldProceed) {
337
+ console.log(chalk.dim('Cancelled.'));
338
+ process.exit(0);
339
+ }
340
+ tryStartServer(PORT + 1);
341
+ } else {
342
+ tryStartServer(PORT);
343
+ }
344
+ })();
345
+
346
+ process.on('SIGINT', () => {
347
+ console.log(chalk.yellow('\n🛑 Shutting down...'));
348
+ watcher.close();
349
+ process.exit(0);
350
+ });
351
+ }
352
+
353
+ module.exports = { startDevServer };
@@ -0,0 +1,277 @@
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
+
3
+ const fs = require('../utils/fs-utils');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const { version } = require('../../package.json');
7
+
8
+ const defaultConfigContent = `// docmd.config.js
9
+ module.exports = {
10
+ // --- Core Metadata ---
11
+ siteTitle: 'My Documentation',
12
+ siteUrl: '', // e.g. https://mysite.com (Critical for SEO/Sitemap)
13
+
14
+ // --- Branding ---
15
+ logo: {
16
+ light: 'assets/images/docmd-logo-dark.png',
17
+ dark: 'assets/images/docmd-logo-light.png',
18
+ alt: 'Logo',
19
+ href: './',
20
+ },
21
+ favicon: 'assets/favicon.ico',
22
+
23
+ // --- Source & Output ---
24
+ srcDir: 'docs',
25
+ outputDir: 'site',
26
+
27
+ // --- Theme & Layout ---
28
+ theme: {
29
+ name: 'sky', // Options: 'default', 'sky', 'ruby', 'retro'
30
+ defaultMode: 'system', // 'light', 'dark', or 'system'
31
+ enableModeToggle: true, // Show mode toggle button
32
+ positionMode: 'top', // 'top' or 'bottom'
33
+ codeHighlight: true, // Enable Highlight.js
34
+ customCss: [], // e.g. ['assets/css/custom.css']
35
+ },
36
+
37
+ // --- Features ---
38
+ search: true, // Built-in offline search
39
+ minify: true, // Minify HTML/CSS/JS in build
40
+ autoTitleFromH1: true, // Auto-generate page title from first H1
41
+ copyCode: true, // Show "copy" button on code blocks
42
+ pageNavigation: true, // Prev/Next buttons at bottom
43
+
44
+ // --- Navigation (Sidebar) ---
45
+ navigation: [
46
+ { title: 'Introduction', path: '/', icon: 'home' },
47
+ {
48
+ title: 'Guide',
49
+ icon: 'book-open',
50
+ collapsible: true,
51
+ children: [
52
+ { title: 'Getting Started', path: 'https://docs.docmd.io/getting-started/installation', icon: 'rocket', external: true },
53
+ { title: 'Configuration', path: 'https://docs.docmd.io/configuration', icon: 'settings', external: true },
54
+ ],
55
+ },
56
+ { title: 'Live Editor', path: 'https://live.docmd.io', icon: 'pencil-ruler', external: true },
57
+ { title: 'GitHub', path: 'https://github.com/docmd-io/docmd', icon: 'github', external: true },
58
+ ],
59
+
60
+ // --- Plugins ---
61
+ plugins: {
62
+ seo: {
63
+ defaultDescription: 'Documentation built with docmd.',
64
+ openGraph: {
65
+ defaultImage: '', // e.g. 'assets/images/og-image.png'
66
+ },
67
+ twitter: {
68
+ cardType: 'summary_large_image',
69
+ }
70
+ },
71
+ analytics: {
72
+ googleV4: {
73
+ measurementId: 'G-X9WTDL262N' // Replace with your Google Analytics Measurement ID
74
+ }
75
+ },
76
+ sitemap: {
77
+ defaultChangefreq: 'weekly', // e.g. 'daily', 'weekly', 'monthly'
78
+ defaultPriority: 0.8 // Priority between 0.0 and 1.0
79
+ }
80
+ },
81
+
82
+ // --- Footer ---
83
+ footer: '© ' + new Date().getFullYear() + ' My Project. Built with [docmd](https://docmd.io).',
84
+
85
+ // --- Edit Link ---
86
+ editLink: {
87
+ enabled: false,
88
+ baseUrl: 'https://github.com/USERNAME/REPO/edit/main/docs',
89
+ text: 'Edit this page'
90
+ }
91
+ };
92
+ `;
93
+
94
+ const defaultIndexMdContent = `---
95
+ title: "Welcome"
96
+ description: "Welcome to your new documentation site."
97
+ ---
98
+
99
+ # Welcome to Your Docs
100
+
101
+ Congratulations! You have successfully initialized a new **docmd** project.
102
+
103
+ ## 🚀 Quick Start
104
+
105
+ You are currently viewing the content of \`docs/index.md\`.
106
+
107
+ \`\`\`bash
108
+ npm start # Start the dev server
109
+ docmd build # Build for production
110
+ \`\`\`
111
+
112
+ ## ✨ Features Demo
113
+
114
+ docmd comes with built-in components to make your documentation beautiful.
115
+
116
+ ::: callout tip
117
+ **Try this:** Edit this file and save it. The browser will live reload instantly!
118
+ :::
119
+
120
+ ### Container Examples
121
+
122
+ ::: card Flexible Structure
123
+ **Organize your way.**
124
+ Create Markdown files in the \`docs/\` folder and map them in \`docmd.config.js\`.
125
+ :::
126
+
127
+ ::: tabs
128
+ == tab "Simple"
129
+ This is a simple tab content.
130
+
131
+ == tab "Nested"
132
+ ::: callout info
133
+ You can even nest other components inside tabs!
134
+ :::
135
+ :::
136
+
137
+ ## 📚 Next Steps
138
+
139
+ * [Check the Official Documentation](https://docs.docmd.io)
140
+ * [Customize your Theme](https://docs.docmd.io/theming)
141
+ * [Deploy to GitHub Pages](https://docs.docmd.io/deployment)
142
+ `;
143
+
144
+ const defaultPackageJson = {
145
+ name: "my-docs",
146
+ version: "0.0.1",
147
+ private: true,
148
+ scripts: {
149
+ "dev": "docmd dev",
150
+ "build": "docmd build",
151
+ "preview": "npx serve site"
152
+ },
153
+ dependencies: {
154
+ "@docmd/core": `^${version}`
155
+ }
156
+ };
157
+
158
+ async function initProject() {
159
+ const baseDir = process.cwd();
160
+ const packageJsonFile = path.join(baseDir, 'package.json');
161
+ const configFile = path.join(baseDir, 'docmd.config.js');
162
+ const docsDir = path.join(baseDir, 'docs');
163
+ const indexMdFile = path.join(docsDir, 'index.md');
164
+ const assetsDir = path.join(baseDir, 'assets');
165
+ const assetsCssDir = path.join(assetsDir, 'css');
166
+ const assetsJsDir = path.join(assetsDir, 'js');
167
+ const assetsImagesDir = path.join(assetsDir, 'images');
168
+
169
+ const existingFiles = [];
170
+ const dirExists = {
171
+ docs: false,
172
+ assets: false
173
+ };
174
+
175
+ // Check if package.json exists
176
+ if (!await fs.pathExists(packageJsonFile)) {
177
+ await fs.writeJson(packageJsonFile, defaultPackageJson, { spaces: 2 });
178
+ console.log('📦 Created `package.json` (Deployment Ready)');
179
+ } else {
180
+ console.log('⏭️ Skipped existing `package.json`');
181
+ }
182
+
183
+ // Check each file individually
184
+ if (await fs.pathExists(configFile)) {
185
+ existingFiles.push('docmd.config.js');
186
+ }
187
+
188
+ // Check for the legacy config.js
189
+ const oldConfigFile = path.join(baseDir, 'config.js');
190
+ if (await fs.pathExists(oldConfigFile)) {
191
+ existingFiles.push('config.js');
192
+ }
193
+
194
+ // Check if docs directory exists
195
+ if (await fs.pathExists(docsDir)) {
196
+ dirExists.docs = true;
197
+ if (await fs.pathExists(indexMdFile)) {
198
+ existingFiles.push('docs/index.md');
199
+ }
200
+ }
201
+
202
+ // Check if assets directory exists
203
+ if (await fs.pathExists(assetsDir)) {
204
+ dirExists.assets = true;
205
+ }
206
+
207
+ // Determine if we should override existing files
208
+ let shouldOverride = false;
209
+ if (existingFiles.length > 0) {
210
+ console.warn('⚠️ The following files already exist:');
211
+ existingFiles.forEach(file => console.warn(` - ${file}`));
212
+
213
+ const rl = readline.createInterface({
214
+ input: process.stdin,
215
+ output: process.stdout
216
+ });
217
+
218
+ const answer = await new Promise(resolve => {
219
+ rl.question('Do you want to override these files? (y/N): ', resolve);
220
+ });
221
+
222
+ rl.close();
223
+
224
+ shouldOverride = answer.toLowerCase() === 'y';
225
+
226
+ if (!shouldOverride) {
227
+ console.log('⏭️ Skipping existing files. Will only create new files.');
228
+ }
229
+ }
230
+
231
+ // Create docs directory if it doesn't exist
232
+ if (!dirExists.docs) {
233
+ await fs.ensureDir(docsDir);
234
+ console.log('📁 Created `docs/` directory');
235
+ } else {
236
+ console.log('📁 Using existing `docs/` directory');
237
+ }
238
+
239
+ // Create assets directory structure if it doesn't exist
240
+ if (!dirExists.assets) {
241
+ await fs.ensureDir(assetsDir);
242
+ await fs.ensureDir(assetsCssDir);
243
+ await fs.ensureDir(assetsJsDir);
244
+ await fs.ensureDir(assetsImagesDir);
245
+ console.log('📁 Created `assets/` directory with css, js, and images subdirectories');
246
+ } else {
247
+ console.log('📁 Using existing `assets/` directory');
248
+
249
+ if (!await fs.pathExists(assetsCssDir)) await fs.ensureDir(assetsCssDir);
250
+ if (!await fs.pathExists(assetsJsDir)) await fs.ensureDir(assetsJsDir);
251
+ if (!await fs.pathExists(assetsImagesDir)) await fs.ensureDir(assetsImagesDir);
252
+ }
253
+
254
+ // Write config file if it doesn't exist or user confirmed override
255
+ if (!await fs.pathExists(configFile) || shouldOverride) {
256
+ await fs.writeFile(configFile, defaultConfigContent, 'utf8');
257
+ console.log(`📄 ${shouldOverride ? 'Updated' : 'Created'} \`docmd.config.js\``);
258
+ } else {
259
+ console.log('⏭️ Skipped existing `docmd.config.js`');
260
+ }
261
+
262
+ // Write index.md file if it doesn't exist or user confirmed override
263
+ if (!await fs.pathExists(indexMdFile) || shouldOverride) {
264
+ await fs.writeFile(indexMdFile, defaultIndexMdContent, 'utf8');
265
+ console.log('📄 Created `docs/index.md`');
266
+ } else if (shouldOverride) {
267
+ await fs.writeFile(indexMdFile, defaultIndexMdContent, 'utf8');
268
+ console.log('📄 Updated `docs/index.md`');
269
+ } else {
270
+ console.log('⏭️ Skipped existing `docs/index.md`');
271
+ }
272
+
273
+ console.log('✅ Project initialization complete!');
274
+ console.log('👉 Run `npm install` to setup dependencies.');
275
+ }
276
+
277
+ module.exports = { initProject };
@@ -0,0 +1,15 @@
1
+ async function buildLive(options = {}) {
2
+ // Delegate to the standalone package
3
+ const livePkg = require('@docmd/live');
4
+
5
+ // If explicitly asked NOT to serve (for testing), just build
6
+ if (options.serve === false) {
7
+ console.log('🔨 Building Live Editor ...');
8
+ await livePkg.build();
9
+ } else {
10
+ // Default behavior: Build + Serve
11
+ await livePkg.start();
12
+ }
13
+ }
14
+
15
+ module.exports = { buildLive };
@@ -0,0 +1,38 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const { validateConfig } = require('@docmd/parser');
4
+
5
+ async function loadConfig(configPath) {
6
+ const cwd = process.cwd();
7
+ let absoluteConfigPath = path.resolve(cwd, configPath);
8
+
9
+ // Fallback for default filename
10
+ if (!fs.existsSync(absoluteConfigPath) && configPath === 'docmd.config.js') {
11
+ const legacyPath = path.resolve(cwd, 'config.js');
12
+ if (fs.existsSync(legacyPath)) absoluteConfigPath = legacyPath;
13
+ else throw new Error(`Configuration file not found at: ${absoluteConfigPath}\nRun "docmd init" to create one.`);
14
+ }
15
+
16
+ try {
17
+ delete require.cache[require.resolve(absoluteConfigPath)];
18
+ const config = require(absoluteConfigPath);
19
+
20
+ // Validate using Parser
21
+ validateConfig(config);
22
+
23
+ // Apply Defaults
24
+ return {
25
+ base: '/',
26
+ srcDir: 'docs',
27
+ outputDir: 'site',
28
+ ...config,
29
+ theme: { defaultMode: 'light', ...config.theme },
30
+ navigation: config.navigation || [{ title: 'Home', path: '/' }]
31
+ };
32
+ } catch (e) {
33
+ if (e.message === 'Invalid configuration file.') throw e;
34
+ throw new Error(`Error parsing config file: ${e.message}`);
35
+ }
36
+ }
37
+
38
+ module.exports = { loadConfig };