@litodocs/cli 0.5.2 → 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.
- package/README.md +325 -124
- package/lito-manifest.schema.json +118 -0
- package/package.json +1 -1
- package/src/cli.js +49 -1
- package/src/commands/build.js +26 -17
- package/src/commands/dev.js +17 -12
- package/src/commands/doctor.js +311 -0
- package/src/commands/info.js +192 -0
- package/src/commands/init.js +284 -0
- package/src/commands/preview.js +98 -0
- package/src/commands/validate.js +124 -0
- package/src/core/config-sync.js +17 -1
- package/src/core/config-validator.js +229 -0
- package/src/core/config.js +62 -18
- package/src/core/framework-runner.js +208 -0
- package/src/core/landing-sync.js +708 -0
- package/src/core/output.js +10 -4
- package/src/core/sync.js +141 -15
- package/src/core/template-registry.js +8 -5
- package/src/schema/core-schema.json +262 -0
- package/src/schema/template-manifest.json +106 -0
|
@@ -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
|
+
}
|