@litodocs/cli 0.6.0 → 1.0.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.
@@ -5,10 +5,10 @@ import pc from 'picocolors';
5
5
  import { scaffoldProject, cleanupProject } from '../core/scaffold.js';
6
6
  import { syncDocs } from '../core/sync.js';
7
7
  import { generateConfig } from '../core/config.js';
8
- import { runAstroBuild } from '../core/astro.js';
9
8
  import { syncDocsConfig } from '../core/config-sync.js';
10
9
  import { copyOutput } from '../core/output.js';
11
10
  import { getTemplatePath } from '../core/template-fetcher.js';
11
+ import { detectFramework, runFrameworkBuild, getOutputDir, needsSearchIndex } from '../core/framework-runner.js';
12
12
 
13
13
  export async function buildCommand(options) {
14
14
  try {
@@ -29,28 +29,33 @@ export async function buildCommand(options) {
29
29
  const templatePath = await getTemplatePath(options.template, options.refresh);
30
30
  s.stop(templatePath ? `Using template: ${pc.cyan(templatePath)}` : 'Using bundled template');
31
31
 
32
- // Step 1: Scaffold temporary Astro project
33
- s.start('Setting up Astro project...');
32
+ // Step 1: Scaffold temporary project
33
+ s.start('Setting up project...');
34
34
  const projectDir = await scaffoldProject(templatePath);
35
- s.stop('Astro project scaffolded');
35
+ s.stop('Project scaffolded');
36
+
37
+ // Step 1.5: Detect framework
38
+ s.start('Detecting framework...');
39
+ const frameworkConfig = await detectFramework(projectDir);
40
+ s.stop(`Using framework: ${pc.cyan(frameworkConfig.name)}`);
36
41
 
37
42
  // Step 2: Prepare project (Install dependencies, Sync docs, Generate navigation)
38
43
  const { installDependencies, runBinary } = await import('../core/package-manager.js');
39
44
  s.start('Preparing project (installing dependencies, syncing files)...');
40
-
45
+
41
46
  const userConfigPath = resolve(options.input, 'docs-config.json');
42
47
 
43
48
  await Promise.all([
44
49
  installDependencies(projectDir, { silent: true }),
45
- syncDocs(inputPath, projectDir),
50
+ syncDocs(inputPath, projectDir, frameworkConfig),
46
51
  syncDocsConfig(projectDir, inputPath, userConfigPath)
47
52
  ]);
48
53
 
49
54
  s.stop('Project prepared (dependencies installed, docs synced, navigation generated)');
50
55
 
51
- // Step 4: Generate config
52
- s.start('Generating Astro configuration...');
53
- await generateConfig(projectDir, options);
56
+ // Step 4: Generate config (framework-aware)
57
+ s.start(`Generating ${frameworkConfig.name} configuration...`);
58
+ await generateConfig(projectDir, options, frameworkConfig);
54
59
  s.stop('Configuration generated');
55
60
 
56
61
  // Step 4.5: Configure for provider
@@ -61,20 +66,24 @@ export async function buildCommand(options) {
61
66
  s.stop(`Configured for ${options.provider}`);
62
67
  }
63
68
 
64
- // Step 5: Build with Astro
65
- s.start('Building site with Astro...');
66
- await runAstroBuild(projectDir);
69
+ // Step 5: Build with framework
70
+ s.start(`Building site with ${frameworkConfig.name}...`);
71
+ await runFrameworkBuild(projectDir, frameworkConfig);
67
72
  s.stop('Site built successfully');
68
73
 
69
- // Step 5.5: Generate Pagefind search index
70
- s.start('Generating search index...');
71
- await runBinary(projectDir, 'pagefind', ['--site', 'dist']);
72
- s.stop('Search index generated');
74
+ // Step 5.5: Generate Pagefind search index (only for static frameworks)
75
+ if (needsSearchIndex(frameworkConfig)) {
76
+ const outputDir = getOutputDir(frameworkConfig);
77
+ s.start('Generating search index...');
78
+ await runBinary(projectDir, 'pagefind', ['--site', outputDir]);
79
+ s.stop('Search index generated');
80
+ }
73
81
 
74
82
  // Step 6: Copy output
75
83
  const outputPath = resolve(options.output);
84
+ const frameworkOutputDir = getOutputDir(frameworkConfig);
76
85
  s.start(`Copying output to ${pc.cyan(outputPath)}...`);
77
- await copyOutput(projectDir, outputPath);
86
+ await copyOutput(projectDir, outputPath, frameworkOutputDir);
78
87
  s.stop('Output copied');
79
88
 
80
89
  // Cleanup temp directory
@@ -6,9 +6,9 @@ import chokidar from 'chokidar';
6
6
  import { scaffoldProject, cleanupProject } from '../core/scaffold.js';
7
7
  import { syncDocs } from '../core/sync.js';
8
8
  import { generateConfig } from '../core/config.js';
9
- import { runAstroDev } from '../core/astro.js';
10
9
  import { syncDocsConfig } from '../core/config-sync.js';
11
10
  import { getTemplatePath } from '../core/template-fetcher.js';
11
+ import { detectFramework, runFrameworkDev } from '../core/framework-runner.js';
12
12
 
13
13
  export async function devCommand(options) {
14
14
  try {
@@ -29,10 +29,15 @@ export async function devCommand(options) {
29
29
  const templatePath = await getTemplatePath(options.template, options.refresh);
30
30
  s.stop(templatePath ? `Using template: ${pc.cyan(templatePath)}` : 'Using bundled template');
31
31
 
32
- // Step 1: Scaffold temporary Astro project
33
- s.start('Setting up Astro project...');
32
+ // Step 1: Scaffold temporary project
33
+ s.start('Setting up project...');
34
34
  const projectDir = await scaffoldProject(templatePath);
35
- s.stop('Astro project scaffolded');
35
+ s.stop('Project scaffolded');
36
+
37
+ // Step 1.5: Detect framework
38
+ s.start('Detecting framework...');
39
+ const frameworkConfig = await detectFramework(projectDir);
40
+ s.stop(`Using framework: ${pc.cyan(frameworkConfig.name)}`);
36
41
 
37
42
  // Register cleanup handlers
38
43
  const cleanup = async () => {
@@ -52,15 +57,15 @@ export async function devCommand(options) {
52
57
 
53
58
  await Promise.all([
54
59
  installDependencies(projectDir, { silent: true }),
55
- syncDocs(inputPath, projectDir),
60
+ syncDocs(inputPath, projectDir, frameworkConfig),
56
61
  syncDocsConfig(projectDir, inputPath, userConfigPath)
57
62
  ]);
58
63
 
59
64
  s.stop('Project prepared (dependencies installed, docs synced, navigation generated)');
60
65
 
61
- // Step 4: Generate config
62
- s.start('Generating Astro configuration...');
63
- await generateConfig(projectDir, options);
66
+ // Step 4: Generate config (framework-aware)
67
+ s.start(`Generating ${frameworkConfig.name} configuration...`);
68
+ await generateConfig(projectDir, options, frameworkConfig);
64
69
  s.stop('Configuration generated');
65
70
 
66
71
  // Step 4: Setup file watcher with debouncing
@@ -82,7 +87,7 @@ export async function devCommand(options) {
82
87
 
83
88
  try {
84
89
  await Promise.all([
85
- syncDocs(inputPath, projectDir),
90
+ syncDocs(inputPath, projectDir, frameworkConfig),
86
91
  syncDocsConfig(projectDir, inputPath, userConfigPath)
87
92
  ]);
88
93
  log.success('Documentation and config re-synced');
@@ -115,9 +120,9 @@ export async function devCommand(options) {
115
120
  debouncedSync();
116
121
  });
117
122
 
118
- // Step 5: Start Astro dev server
119
- note(`Starting Astro dev server at http://localhost:${options.port}`, 'Dev Server');
120
- await runAstroDev(projectDir, options.port);
123
+ // Step 5: Start framework dev server
124
+ note(`Starting ${frameworkConfig.name} dev server at http://localhost:${options.port}`, 'Dev Server');
125
+ await runFrameworkDev(projectDir, frameworkConfig, options.port);
121
126
 
122
127
  } catch (error) {
123
128
  if (isCancel(error)) {
@@ -0,0 +1,311 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
2
+ import { resolve, join, extname } from 'path';
3
+ import { intro, outro, log, spinner } from '@clack/prompts';
4
+ import pc from 'picocolors';
5
+ import { validateConfig } from '../core/config-validator.js';
6
+ import { TEMPLATE_REGISTRY } from '../core/template-registry.js';
7
+
8
+ /**
9
+ * Check result types
10
+ */
11
+ const CHECK_PASS = 'pass';
12
+ const CHECK_WARN = 'warn';
13
+ const CHECK_FAIL = 'fail';
14
+
15
+ /**
16
+ * Doctor command - Diagnose common issues
17
+ */
18
+ export async function doctorCommand(options) {
19
+ try {
20
+ const inputPath = options.input ? resolve(options.input) : process.cwd();
21
+
22
+ console.clear();
23
+ intro(pc.inverse(pc.cyan(' Lito - Doctor ')));
24
+
25
+ log.message(pc.dim(`Checking: ${inputPath}`));
26
+ log.message('');
27
+
28
+ const checks = [];
29
+
30
+ // Check 1: Directory exists
31
+ checks.push(await checkDirectoryExists(inputPath));
32
+
33
+ // Check 2: docs-config.json exists
34
+ checks.push(await checkConfigExists(inputPath));
35
+
36
+ // Check 3: Config is valid JSON
37
+ const configCheck = await checkConfigValid(inputPath);
38
+ checks.push(configCheck);
39
+
40
+ // Check 4: Config passes validation
41
+ if (configCheck.status === CHECK_PASS) {
42
+ checks.push(await checkConfigSchema(inputPath));
43
+ }
44
+
45
+ // Check 5: Content files exist
46
+ checks.push(await checkContentFiles(inputPath));
47
+
48
+ // Check 6: Check for common issues
49
+ checks.push(await checkCommonIssues(inputPath));
50
+
51
+ // Check 7: Template cache
52
+ checks.push(await checkTemplateCache());
53
+
54
+ // Check 8: Node.js version
55
+ checks.push(await checkNodeVersion());
56
+
57
+ // Print results
58
+ log.message('');
59
+ log.message(pc.bold('Diagnostic Results:'));
60
+ log.message('');
61
+
62
+ let hasErrors = false;
63
+ let hasWarnings = false;
64
+
65
+ for (const check of checks) {
66
+ const icon =
67
+ check.status === CHECK_PASS
68
+ ? pc.green('✓')
69
+ : check.status === CHECK_WARN
70
+ ? pc.yellow('!')
71
+ : pc.red('✗');
72
+
73
+ log.message(` ${icon} ${check.name}`);
74
+
75
+ if (check.message) {
76
+ log.message(` ${pc.dim(check.message)}`);
77
+ }
78
+
79
+ if (check.status === CHECK_FAIL) hasErrors = true;
80
+ if (check.status === CHECK_WARN) hasWarnings = true;
81
+ }
82
+
83
+ log.message('');
84
+
85
+ // Summary
86
+ if (hasErrors) {
87
+ log.error(pc.red('Some checks failed. Please fix the issues above.'));
88
+ process.exit(1);
89
+ } else if (hasWarnings) {
90
+ log.warn(pc.yellow('Some warnings found. Your project should still work.'));
91
+ outro(pc.yellow('Doctor completed with warnings'));
92
+ } else {
93
+ log.success(pc.green('All checks passed!'));
94
+ outro(pc.green('Your project looks healthy! 🎉'));
95
+ }
96
+ } catch (error) {
97
+ log.error(pc.red(error.message));
98
+ process.exit(1);
99
+ }
100
+ }
101
+
102
+ async function checkDirectoryExists(inputPath) {
103
+ if (!existsSync(inputPath)) {
104
+ return {
105
+ name: 'Directory exists',
106
+ status: CHECK_FAIL,
107
+ message: `Directory not found: ${inputPath}`,
108
+ };
109
+ }
110
+
111
+ const stat = statSync(inputPath);
112
+ if (!stat.isDirectory()) {
113
+ return {
114
+ name: 'Directory exists',
115
+ status: CHECK_FAIL,
116
+ message: `Path is not a directory: ${inputPath}`,
117
+ };
118
+ }
119
+
120
+ return {
121
+ name: 'Directory exists',
122
+ status: CHECK_PASS,
123
+ };
124
+ }
125
+
126
+ async function checkConfigExists(inputPath) {
127
+ const configPath = join(inputPath, 'docs-config.json');
128
+
129
+ if (!existsSync(configPath)) {
130
+ return {
131
+ name: 'docs-config.json exists',
132
+ status: CHECK_FAIL,
133
+ message: `Run 'lito init' to create a configuration file`,
134
+ };
135
+ }
136
+
137
+ return {
138
+ name: 'docs-config.json exists',
139
+ status: CHECK_PASS,
140
+ };
141
+ }
142
+
143
+ async function checkConfigValid(inputPath) {
144
+ const configPath = join(inputPath, 'docs-config.json');
145
+
146
+ if (!existsSync(configPath)) {
147
+ return {
148
+ name: 'Config is valid JSON',
149
+ status: CHECK_WARN,
150
+ message: 'Skipped (no config file)',
151
+ };
152
+ }
153
+
154
+ try {
155
+ JSON.parse(readFileSync(configPath, 'utf-8'));
156
+ return {
157
+ name: 'Config is valid JSON',
158
+ status: CHECK_PASS,
159
+ };
160
+ } catch (e) {
161
+ return {
162
+ name: 'Config is valid JSON',
163
+ status: CHECK_FAIL,
164
+ message: e.message,
165
+ };
166
+ }
167
+ }
168
+
169
+ async function checkConfigSchema(inputPath) {
170
+ const configPath = join(inputPath, 'docs-config.json');
171
+
172
+ try {
173
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
174
+ const result = validateConfig(config, inputPath, { silent: true });
175
+
176
+ if (!result.valid) {
177
+ return {
178
+ name: 'Config passes validation',
179
+ status: CHECK_FAIL,
180
+ message: result.errors.map((e) => e.message).join('; '),
181
+ };
182
+ }
183
+
184
+ return {
185
+ name: 'Config passes validation',
186
+ status: CHECK_PASS,
187
+ };
188
+ } catch (e) {
189
+ return {
190
+ name: 'Config passes validation',
191
+ status: CHECK_WARN,
192
+ message: 'Could not validate',
193
+ };
194
+ }
195
+ }
196
+
197
+ async function checkContentFiles(inputPath) {
198
+ try {
199
+ const files = readdirSync(inputPath);
200
+ const contentFiles = files.filter((f) => {
201
+ const ext = extname(f).toLowerCase();
202
+ return ['.md', '.mdx'].includes(ext);
203
+ });
204
+
205
+ if (contentFiles.length === 0) {
206
+ return {
207
+ name: 'Content files exist',
208
+ status: CHECK_WARN,
209
+ message: 'No .md or .mdx files found in root directory',
210
+ };
211
+ }
212
+
213
+ return {
214
+ name: 'Content files exist',
215
+ status: CHECK_PASS,
216
+ message: `Found ${contentFiles.length} content file(s)`,
217
+ };
218
+ } catch (e) {
219
+ return {
220
+ name: 'Content files exist',
221
+ status: CHECK_WARN,
222
+ message: 'Could not read directory',
223
+ };
224
+ }
225
+ }
226
+
227
+ async function checkCommonIssues(inputPath) {
228
+ const issues = [];
229
+
230
+ // Check for node_modules in docs folder (shouldn't be there)
231
+ if (existsSync(join(inputPath, 'node_modules'))) {
232
+ issues.push('node_modules folder found in docs (should be removed)');
233
+ }
234
+
235
+ // Check for package.json in docs folder (might cause conflicts)
236
+ if (existsSync(join(inputPath, 'package.json'))) {
237
+ issues.push('package.json in docs folder (may cause conflicts)');
238
+ }
239
+
240
+ // Check for .git in docs folder
241
+ if (existsSync(join(inputPath, '.git'))) {
242
+ issues.push('.git folder in docs (unusual structure)');
243
+ }
244
+
245
+ if (issues.length > 0) {
246
+ return {
247
+ name: 'No common issues',
248
+ status: CHECK_WARN,
249
+ message: issues.join('; '),
250
+ };
251
+ }
252
+
253
+ return {
254
+ name: 'No common issues',
255
+ status: CHECK_PASS,
256
+ };
257
+ }
258
+
259
+ async function checkTemplateCache() {
260
+ const cacheDir = join(process.env.HOME || '~', '.lito', 'templates');
261
+
262
+ if (!existsSync(cacheDir)) {
263
+ return {
264
+ name: 'Template cache',
265
+ status: CHECK_PASS,
266
+ message: 'No cached templates',
267
+ };
268
+ }
269
+
270
+ try {
271
+ const cached = readdirSync(cacheDir);
272
+ return {
273
+ name: 'Template cache',
274
+ status: CHECK_PASS,
275
+ message: `${cached.length} template(s) cached`,
276
+ };
277
+ } catch (e) {
278
+ return {
279
+ name: 'Template cache',
280
+ status: CHECK_WARN,
281
+ message: 'Could not read cache directory',
282
+ };
283
+ }
284
+ }
285
+
286
+ async function checkNodeVersion() {
287
+ const version = process.version;
288
+ const major = parseInt(version.slice(1).split('.')[0], 10);
289
+
290
+ if (major < 18) {
291
+ return {
292
+ name: 'Node.js version',
293
+ status: CHECK_FAIL,
294
+ message: `Node.js 18+ required (found ${version})`,
295
+ };
296
+ }
297
+
298
+ if (major < 20) {
299
+ return {
300
+ name: 'Node.js version',
301
+ status: CHECK_WARN,
302
+ message: `Node.js 20+ recommended (found ${version})`,
303
+ };
304
+ }
305
+
306
+ return {
307
+ name: 'Node.js version',
308
+ status: CHECK_PASS,
309
+ message: version,
310
+ };
311
+ }
@@ -0,0 +1,192 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
2
+ import { resolve, join, extname } from 'path';
3
+ import { intro, outro, log } from '@clack/prompts';
4
+ import pc from 'picocolors';
5
+ import { TEMPLATE_REGISTRY } from '../core/template-registry.js';
6
+ import { getCoreConfigKeys, getExtensionKeys, isPortableConfig } from '../core/config-validator.js';
7
+
8
+ /**
9
+ * Info command - Show project information
10
+ */
11
+ export async function infoCommand(options) {
12
+ try {
13
+ const inputPath = options.input ? resolve(options.input) : process.cwd();
14
+ const configPath = join(inputPath, 'docs-config.json');
15
+
16
+ console.clear();
17
+ intro(pc.inverse(pc.cyan(' Lito - Project Info ')));
18
+
19
+ log.message(pc.dim(`Path: ${inputPath}`));
20
+ log.message('');
21
+
22
+ // Check if config exists
23
+ if (!existsSync(configPath)) {
24
+ log.warn('No docs-config.json found in this directory.');
25
+ log.message('');
26
+ log.message(`Run ${pc.cyan('lito init')} to create a new project.`);
27
+ outro('');
28
+ return;
29
+ }
30
+
31
+ // Parse config
32
+ let config;
33
+ try {
34
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
35
+ } catch (e) {
36
+ log.error(`Failed to parse docs-config.json: ${e.message}`);
37
+ process.exit(1);
38
+ }
39
+
40
+ // Project info
41
+ log.message(pc.bold('📦 Project'));
42
+ log.message(` ${pc.cyan('Name:')} ${config.metadata?.name || pc.dim('(not set)')}`);
43
+ if (config.metadata?.description) {
44
+ log.message(` ${pc.cyan('Description:')} ${config.metadata.description}`);
45
+ }
46
+ log.message('');
47
+
48
+ // Content stats
49
+ const stats = getContentStats(inputPath);
50
+ log.message(pc.bold('📄 Content'));
51
+ log.message(` ${pc.cyan('Markdown files:')} ${stats.mdFiles}`);
52
+ log.message(` ${pc.cyan('MDX files:')} ${stats.mdxFiles}`);
53
+ log.message(` ${pc.cyan('Total pages:')} ${stats.mdFiles + stats.mdxFiles}`);
54
+ if (stats.directories > 0) {
55
+ log.message(` ${pc.cyan('Subdirectories:')} ${stats.directories}`);
56
+ }
57
+ log.message('');
58
+
59
+ // Navigation
60
+ if (config.navigation) {
61
+ log.message(pc.bold('🧭 Navigation'));
62
+ const sidebarGroups = config.navigation.sidebar?.length || 0;
63
+ const sidebarItems = config.navigation.sidebar?.reduce(
64
+ (acc, g) => acc + (g.items?.length || 0),
65
+ 0
66
+ ) || 0;
67
+ const navbarLinks = config.navigation.navbar?.links?.length || 0;
68
+
69
+ log.message(` ${pc.cyan('Sidebar groups:')} ${sidebarGroups}`);
70
+ log.message(` ${pc.cyan('Sidebar items:')} ${sidebarItems}`);
71
+ log.message(` ${pc.cyan('Navbar links:')} ${navbarLinks}`);
72
+ log.message('');
73
+ }
74
+
75
+ // Features
76
+ log.message(pc.bold('⚡ Features'));
77
+ const features = [
78
+ { name: 'Search', enabled: config.search?.enabled },
79
+ { name: 'i18n', enabled: config.i18n?.enabled },
80
+ { name: 'Versioning', enabled: config.versioning?.enabled },
81
+ { name: 'Dark Mode', enabled: config.theme?.darkMode !== false },
82
+ ];
83
+
84
+ for (const feature of features) {
85
+ const icon = feature.enabled ? pc.green('✓') : pc.dim('○');
86
+ log.message(` ${icon} ${feature.name}`);
87
+ }
88
+ log.message('');
89
+
90
+ // Branding
91
+ if (config.branding) {
92
+ log.message(pc.bold('🎨 Branding'));
93
+ if (config.branding.logo) {
94
+ const logo = config.branding.logo;
95
+ if (typeof logo === 'string') {
96
+ log.message(` ${pc.cyan('Logo:')} ${logo}`);
97
+ } else if (logo.light || logo.dark) {
98
+ log.message(` ${pc.cyan('Logo:')} ${logo.light || logo.dark} ${logo.light && logo.dark ? '(light/dark)' : ''}`);
99
+ }
100
+ }
101
+ if (config.branding.favicon) {
102
+ log.message(` ${pc.cyan('Favicon:')} ${config.branding.favicon}`);
103
+ }
104
+ if (config.branding.colors?.primary) {
105
+ log.message(` ${pc.cyan('Primary color:')} ${config.branding.colors.primary}`);
106
+ }
107
+ if (config.branding.colors?.accent) {
108
+ log.message(` ${pc.cyan('Accent color:')} ${config.branding.colors.accent}`);
109
+ }
110
+ log.message('');
111
+ }
112
+
113
+ // Config compatibility
114
+ log.message(pc.bold('🔧 Configuration'));
115
+ const portable = isPortableConfig(config);
116
+ const usedCoreKeys = getCoreConfigKeys().filter((k) => k in config);
117
+ const usedExtKeys = getExtensionKeys().filter((k) => k in config);
118
+
119
+ log.message(` ${pc.cyan('Core keys:')} ${usedCoreKeys.join(', ') || 'none'}`);
120
+ if (usedExtKeys.length > 0) {
121
+ log.message(` ${pc.cyan('Extension keys:')} ${usedExtKeys.join(', ')}`);
122
+ }
123
+ log.message(
124
+ ` ${pc.cyan('Portable:')} ${portable ? pc.green('Yes') : pc.yellow('No (uses extensions)')}`
125
+ );
126
+ log.message('');
127
+
128
+ // Available templates
129
+ log.message(pc.bold('📋 Available Templates'));
130
+ for (const [name, source] of Object.entries(TEMPLATE_REGISTRY)) {
131
+ log.message(` ${pc.cyan(name)}: ${pc.dim(source)}`);
132
+ }
133
+ log.message('');
134
+
135
+ // CLI info
136
+ log.message(pc.bold('🔧 Environment'));
137
+ log.message(` ${pc.cyan('Node.js:')} ${process.version}`);
138
+ log.message(` ${pc.cyan('Platform:')} ${process.platform}`);
139
+
140
+ const cacheDir = join(process.env.HOME || '~', '.lito', 'templates');
141
+ if (existsSync(cacheDir)) {
142
+ const cached = readdirSync(cacheDir).length;
143
+ log.message(` ${pc.cyan('Cached templates:')} ${cached}`);
144
+ }
145
+
146
+ outro('');
147
+ } catch (error) {
148
+ log.error(pc.red(error.message));
149
+ process.exit(1);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Get content statistics for a docs directory
155
+ */
156
+ function getContentStats(inputPath) {
157
+ const stats = {
158
+ mdFiles: 0,
159
+ mdxFiles: 0,
160
+ directories: 0,
161
+ };
162
+
163
+ function scan(dir) {
164
+ try {
165
+ const entries = readdirSync(dir);
166
+
167
+ for (const entry of entries) {
168
+ // Skip special directories
169
+ if (entry.startsWith('_') || entry.startsWith('.') || entry === 'node_modules') {
170
+ continue;
171
+ }
172
+
173
+ const fullPath = join(dir, entry);
174
+ const stat = statSync(fullPath);
175
+
176
+ if (stat.isDirectory()) {
177
+ stats.directories++;
178
+ scan(fullPath);
179
+ } else if (stat.isFile()) {
180
+ const ext = extname(entry).toLowerCase();
181
+ if (ext === '.md') stats.mdFiles++;
182
+ if (ext === '.mdx') stats.mdxFiles++;
183
+ }
184
+ }
185
+ } catch (e) {
186
+ // Ignore errors
187
+ }
188
+ }
189
+
190
+ scan(inputPath);
191
+ return stats;
192
+ }