@litodocs/cli 0.6.0 → 0.7.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,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
+ }
@@ -0,0 +1,284 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { resolve, join } from 'path';
3
+ import { intro, outro, text, select, confirm, spinner, log, isCancel, cancel } from '@clack/prompts';
4
+ import pc from 'picocolors';
5
+ import { TEMPLATE_REGISTRY } from '../core/template-registry.js';
6
+
7
+ /**
8
+ * Default docs-config.json template
9
+ */
10
+ function createDefaultConfig(projectName, framework) {
11
+ return {
12
+ metadata: {
13
+ name: projectName,
14
+ description: `Documentation for ${projectName}`,
15
+ },
16
+ branding: {
17
+ colors: {
18
+ primary: '#10b981',
19
+ accent: '#3b82f6',
20
+ },
21
+ },
22
+ navigation: {
23
+ navbar: {
24
+ links: [
25
+ { label: 'Docs', href: '/' },
26
+ { label: 'GitHub', href: 'https://github.com' },
27
+ ],
28
+ },
29
+ sidebar: [
30
+ {
31
+ label: 'Getting Started',
32
+ items: [
33
+ { label: 'Introduction', href: '/introduction' },
34
+ { label: 'Quick Start', href: '/quickstart' },
35
+ ],
36
+ },
37
+ ],
38
+ },
39
+ search: {
40
+ enabled: true,
41
+ },
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Sample introduction page content
47
+ */
48
+ function createIntroductionPage(projectName) {
49
+ return `---
50
+ title: Introduction
51
+ description: Welcome to ${projectName}
52
+ ---
53
+
54
+ # Welcome to ${projectName}
55
+
56
+ This is your documentation home. Edit this file at \`introduction.mdx\` to get started.
57
+
58
+ ## Features
59
+
60
+ <CardGroup cols={2}>
61
+ <Card title="Fast" icon="bolt">
62
+ Built for speed with static site generation
63
+ </Card>
64
+ <Card title="Flexible" icon="puzzle-piece">
65
+ Customize with MDX components
66
+ </Card>
67
+ </CardGroup>
68
+
69
+ ## Next Steps
70
+
71
+ - Read the [Quick Start](/quickstart) guide
72
+ - Explore the MDX components available
73
+ - Customize your \`docs-config.json\`
74
+ `;
75
+ }
76
+
77
+ /**
78
+ * Sample quickstart page content
79
+ */
80
+ function createQuickstartPage(projectName) {
81
+ return `---
82
+ title: Quick Start
83
+ description: Get up and running with ${projectName}
84
+ ---
85
+
86
+ # Quick Start
87
+
88
+ Get your documentation site running in minutes.
89
+
90
+ ## Installation
91
+
92
+ <Steps>
93
+ <Step title="Install Lito CLI">
94
+ \`\`\`bash
95
+ npm install -g @aspect-ui/lito
96
+ \`\`\`
97
+ </Step>
98
+
99
+ <Step title="Start Development Server">
100
+ \`\`\`bash
101
+ lito dev -i ./docs
102
+ \`\`\`
103
+ </Step>
104
+
105
+ <Step title="Build for Production">
106
+ \`\`\`bash
107
+ lito build -i ./docs -o ./dist
108
+ \`\`\`
109
+ </Step>
110
+ </Steps>
111
+
112
+ ## Configuration
113
+
114
+ Edit \`docs-config.json\` to customize your site:
115
+
116
+ \`\`\`json
117
+ {
118
+ "metadata": {
119
+ "name": "${projectName}",
120
+ "description": "Your project description"
121
+ }
122
+ }
123
+ \`\`\`
124
+
125
+ > [!TIP]
126
+ > Run \`lito validate\` to check your configuration for errors.
127
+ `;
128
+ }
129
+
130
+ /**
131
+ * Init command - Initialize a new documentation project
132
+ */
133
+ export async function initCommand(options) {
134
+ try {
135
+ console.clear();
136
+ intro(pc.inverse(pc.cyan(' Lito - Initialize New Project ')));
137
+
138
+ // Determine output directory
139
+ let outputDir = options.output ? resolve(options.output) : null;
140
+
141
+ if (!outputDir) {
142
+ const dirAnswer = await text({
143
+ message: 'Where should we create your docs?',
144
+ placeholder: './docs',
145
+ defaultValue: './docs',
146
+ validate: (value) => {
147
+ if (!value) return 'Please enter a directory path';
148
+ },
149
+ });
150
+
151
+ if (isCancel(dirAnswer)) {
152
+ cancel('Operation cancelled.');
153
+ process.exit(0);
154
+ }
155
+
156
+ outputDir = resolve(dirAnswer);
157
+ }
158
+
159
+ // Check if directory exists and has content
160
+ if (existsSync(outputDir)) {
161
+ const files = await import('fs').then(fs => fs.readdirSync(outputDir));
162
+ if (files.length > 0) {
163
+ const overwrite = await confirm({
164
+ message: `Directory ${pc.cyan(outputDir)} is not empty. Continue anyway?`,
165
+ initialValue: false,
166
+ });
167
+
168
+ if (isCancel(overwrite) || !overwrite) {
169
+ cancel('Operation cancelled.');
170
+ process.exit(0);
171
+ }
172
+ }
173
+ }
174
+
175
+ // Project name
176
+ let projectName = options.name;
177
+ if (!projectName) {
178
+ const nameAnswer = await text({
179
+ message: 'What is your project name?',
180
+ placeholder: 'My Docs',
181
+ defaultValue: 'My Docs',
182
+ });
183
+
184
+ if (isCancel(nameAnswer)) {
185
+ cancel('Operation cancelled.');
186
+ process.exit(0);
187
+ }
188
+
189
+ projectName = nameAnswer;
190
+ }
191
+
192
+ // Framework/template selection
193
+ const templates = Object.keys(TEMPLATE_REGISTRY);
194
+ let selectedTemplate = options.template;
195
+
196
+ if (!selectedTemplate) {
197
+ const templateAnswer = await select({
198
+ message: 'Which template would you like to use?',
199
+ options: templates.map((t) => ({
200
+ value: t,
201
+ label: t === 'default' ? `${t} (Astro - Recommended)` : t,
202
+ hint: TEMPLATE_REGISTRY[t],
203
+ })),
204
+ });
205
+
206
+ if (isCancel(templateAnswer)) {
207
+ cancel('Operation cancelled.');
208
+ process.exit(0);
209
+ }
210
+
211
+ selectedTemplate = templateAnswer;
212
+ }
213
+
214
+ // Create sample content?
215
+ let createSample = options.sample !== false;
216
+ if (!options.sample) {
217
+ const sampleAnswer = await confirm({
218
+ message: 'Create sample documentation pages?',
219
+ initialValue: true,
220
+ });
221
+
222
+ if (isCancel(sampleAnswer)) {
223
+ cancel('Operation cancelled.');
224
+ process.exit(0);
225
+ }
226
+
227
+ createSample = sampleAnswer;
228
+ }
229
+
230
+ // Create project
231
+ const s = spinner();
232
+ s.start('Creating project structure...');
233
+
234
+ // Create directories
235
+ mkdirSync(outputDir, { recursive: true });
236
+ mkdirSync(join(outputDir, '_assets'), { recursive: true });
237
+ mkdirSync(join(outputDir, '_images'), { recursive: true });
238
+
239
+ // Create docs-config.json
240
+ const config = createDefaultConfig(projectName, selectedTemplate);
241
+ writeFileSync(
242
+ join(outputDir, 'docs-config.json'),
243
+ JSON.stringify(config, null, 2)
244
+ );
245
+
246
+ // Create sample pages if requested
247
+ if (createSample) {
248
+ writeFileSync(
249
+ join(outputDir, 'introduction.mdx'),
250
+ createIntroductionPage(projectName)
251
+ );
252
+ writeFileSync(
253
+ join(outputDir, 'quickstart.mdx'),
254
+ createQuickstartPage(projectName)
255
+ );
256
+ }
257
+
258
+ s.stop('Project created');
259
+
260
+ // Success message
261
+ log.success(pc.green('Project initialized successfully!'));
262
+ log.message('');
263
+ log.message(pc.bold('Next steps:'));
264
+ log.message('');
265
+ log.message(` ${pc.cyan('cd')} ${outputDir}`);
266
+ log.message(` ${pc.cyan('lito dev')} -i .`);
267
+ log.message('');
268
+ log.message(pc.dim(`Template: ${selectedTemplate}`));
269
+ log.message(pc.dim(`Config: ${join(outputDir, 'docs-config.json')}`));
270
+
271
+ outro(pc.green('Happy documenting! 📚'));
272
+ } catch (error) {
273
+ if (isCancel(error)) {
274
+ cancel('Operation cancelled.');
275
+ process.exit(0);
276
+ }
277
+
278
+ log.error(pc.red(error.message));
279
+ if (error.stack) {
280
+ log.error(pc.gray(error.stack));
281
+ }
282
+ process.exit(1);
283
+ }
284
+ }
@@ -0,0 +1,98 @@
1
+ import { existsSync } from 'fs';
2
+ import { resolve, join } from 'path';
3
+ import { intro, log, spinner, isCancel, cancel } from '@clack/prompts';
4
+ import pc from 'picocolors';
5
+ import { execa } from 'execa';
6
+
7
+ /**
8
+ * Preview command - Build and preview production site locally
9
+ */
10
+ export async function previewCommand(options) {
11
+ try {
12
+ const inputPath = options.input ? resolve(options.input) : null;
13
+ const outputPath = resolve(options.output || './dist');
14
+ const port = options.port || '4321';
15
+
16
+ console.clear();
17
+ intro(pc.inverse(pc.cyan(' Lito - Preview Production Build ')));
18
+
19
+ const s = spinner();
20
+
21
+ // Check if dist exists, if not run build first
22
+ if (!existsSync(outputPath)) {
23
+ if (!inputPath) {
24
+ log.error(`Output directory ${pc.cyan(outputPath)} does not exist.`);
25
+ log.message('');
26
+ log.message('Either:');
27
+ log.message(` 1. Run ${pc.cyan('lito build -i <docs>')} first`);
28
+ log.message(` 2. Use ${pc.cyan('lito preview -i <docs>')} to build and preview`);
29
+ process.exit(1);
30
+ }
31
+
32
+ log.warn(`Output directory not found. Building first...`);
33
+ log.message('');
34
+
35
+ // Run build command
36
+ const { buildCommand } = await import('./build.js');
37
+ await buildCommand({
38
+ input: inputPath,
39
+ output: outputPath,
40
+ template: options.template || 'default',
41
+ baseUrl: options.baseUrl || '/',
42
+ provider: 'static',
43
+ });
44
+
45
+ console.clear();
46
+ intro(pc.inverse(pc.cyan(' Lito - Preview Production Build ')));
47
+ }
48
+
49
+ // Check for index.html in output
50
+ const indexPath = join(outputPath, 'index.html');
51
+ if (!existsSync(indexPath)) {
52
+ log.error(`No index.html found in ${pc.cyan(outputPath)}`);
53
+ log.message('This directory may not contain a valid build output.');
54
+ process.exit(1);
55
+ }
56
+
57
+ log.success(`Serving ${pc.cyan(outputPath)}`);
58
+ log.message('');
59
+ log.message(` ${pc.bold('Local:')} ${pc.cyan(`http://localhost:${port}`)}`);
60
+ log.message('');
61
+ log.message(pc.dim('Press Ctrl+C to stop'));
62
+ log.message('');
63
+
64
+ // Use npx serve or a simple HTTP server
65
+ try {
66
+ // Try using 'serve' package
67
+ await execa('npx', ['serve', outputPath, '-l', port], {
68
+ stdio: 'inherit',
69
+ cwd: process.cwd(),
70
+ });
71
+ } catch (serveError) {
72
+ // Fallback to Python's http.server if serve isn't available
73
+ try {
74
+ await execa('python3', ['-m', 'http.server', port, '-d', outputPath], {
75
+ stdio: 'inherit',
76
+ cwd: process.cwd(),
77
+ });
78
+ } catch (pythonError) {
79
+ log.error('Could not start preview server.');
80
+ log.message('');
81
+ log.message('Please install a static server:');
82
+ log.message(` ${pc.cyan('npm install -g serve')}`);
83
+ log.message('');
84
+ log.message('Or manually serve the output:');
85
+ log.message(` ${pc.cyan(`npx serve ${outputPath}`)}`);
86
+ process.exit(1);
87
+ }
88
+ }
89
+ } catch (error) {
90
+ if (isCancel(error)) {
91
+ cancel('Preview stopped.');
92
+ process.exit(0);
93
+ }
94
+
95
+ log.error(pc.red(error.message));
96
+ process.exit(1);
97
+ }
98
+ }
@@ -0,0 +1,124 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { resolve, join } from 'path';
3
+ import { intro, outro, log, spinner } from '@clack/prompts';
4
+ import pc from 'picocolors';
5
+ import { validateConfig, isPortableConfig, getCoreConfigKeys, getExtensionKeys } from '../core/config-validator.js';
6
+
7
+ /**
8
+ * Validate command - Validate docs-config.json
9
+ */
10
+ export async function validateCommand(options) {
11
+ try {
12
+ const inputPath = options.input ? resolve(options.input) : process.cwd();
13
+ const configPath = join(inputPath, 'docs-config.json');
14
+
15
+ // Quick mode for CI - just exit with code
16
+ if (options.quiet) {
17
+ if (!existsSync(configPath)) {
18
+ process.exit(1);
19
+ }
20
+ try {
21
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
22
+ const result = validateConfig(config, inputPath, { silent: true });
23
+ process.exit(result.valid ? 0 : 1);
24
+ } catch (e) {
25
+ process.exit(1);
26
+ }
27
+ }
28
+
29
+ console.clear();
30
+ intro(pc.inverse(pc.cyan(' Lito - Validate Configuration ')));
31
+
32
+ const s = spinner();
33
+
34
+ // Check if config file exists
35
+ s.start('Looking for docs-config.json...');
36
+
37
+ if (!existsSync(configPath)) {
38
+ s.stop(pc.red('Configuration file not found'));
39
+ log.error(`No docs-config.json found at ${pc.cyan(inputPath)}`);
40
+ log.message('');
41
+ log.message(`Run ${pc.cyan('lito init')} to create a new project with a config file.`);
42
+ process.exit(1);
43
+ }
44
+
45
+ s.stop(`Found: ${pc.cyan(configPath)}`);
46
+
47
+ // Parse JSON
48
+ s.start('Parsing configuration...');
49
+ let config;
50
+ try {
51
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
52
+ s.stop('Configuration parsed');
53
+ } catch (parseError) {
54
+ s.stop(pc.red('Invalid JSON'));
55
+ log.error('Failed to parse docs-config.json:');
56
+ log.error(pc.red(parseError.message));
57
+ process.exit(1);
58
+ }
59
+
60
+ // Validate
61
+ s.start('Validating configuration...');
62
+ const result = validateConfig(config, inputPath, { silent: true });
63
+
64
+ if (!result.valid) {
65
+ s.stop(pc.red('Validation failed'));
66
+ log.message('');
67
+ log.error(pc.bold('Configuration errors:'));
68
+ for (const error of result.errors) {
69
+ log.error(` ${pc.red('•')} ${pc.yellow(error.path)}: ${error.message}`);
70
+ }
71
+ log.message('');
72
+ process.exit(1);
73
+ }
74
+
75
+ s.stop(pc.green('Configuration is valid'));
76
+
77
+ // Show summary
78
+ log.message('');
79
+ log.success(pc.bold('Configuration Summary:'));
80
+ log.message('');
81
+
82
+ // Metadata
83
+ if (config.metadata) {
84
+ log.message(` ${pc.cyan('Name:')} ${config.metadata.name || pc.dim('(not set)')}`);
85
+ if (config.metadata.description) {
86
+ log.message(` ${pc.cyan('Description:')} ${config.metadata.description}`);
87
+ }
88
+ }
89
+
90
+ // Navigation
91
+ if (config.navigation) {
92
+ const sidebarGroups = config.navigation.sidebar?.length || 0;
93
+ const navbarLinks = config.navigation.navbar?.links?.length || 0;
94
+ log.message(` ${pc.cyan('Sidebar groups:')} ${sidebarGroups}`);
95
+ log.message(` ${pc.cyan('Navbar links:')} ${navbarLinks}`);
96
+ }
97
+
98
+ // Features
99
+ log.message('');
100
+ log.message(pc.bold(' Features:'));
101
+ log.message(` ${config.search?.enabled ? pc.green('✓') : pc.dim('○')} Search`);
102
+ log.message(` ${config.i18n?.enabled ? pc.green('✓') : pc.dim('○')} i18n`);
103
+ log.message(` ${config.versioning?.enabled ? pc.green('✓') : pc.dim('○')} Versioning`);
104
+
105
+ // Portability check
106
+ log.message('');
107
+ const portable = isPortableConfig(config);
108
+ if (portable) {
109
+ log.message(` ${pc.green('✓')} ${pc.dim('Portable config (works with any template)')}`);
110
+ } else {
111
+ const usedExtensions = getExtensionKeys().filter(key => key in config);
112
+ log.message(` ${pc.yellow('!')} ${pc.dim(`Uses template extensions: ${usedExtensions.join(', ')}`)}`);
113
+ }
114
+
115
+ log.message('');
116
+ outro(pc.green('Validation complete!'));
117
+ } catch (error) {
118
+ log.error(pc.red(error.message));
119
+ if (error.stack && !options.quiet) {
120
+ log.error(pc.gray(error.stack));
121
+ }
122
+ process.exit(1);
123
+ }
124
+ }