@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.
- package/README.md +325 -124
- package/package.json +7 -2
- package/src/cli.js +50 -2
- 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.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/core/update-check.js +11 -1
package/src/commands/build.js
CHANGED
|
@@ -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
|
|
33
|
-
s.start('Setting up
|
|
32
|
+
// Step 1: Scaffold temporary project
|
|
33
|
+
s.start('Setting up project...');
|
|
34
34
|
const projectDir = await scaffoldProject(templatePath);
|
|
35
|
-
s.stop('
|
|
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(
|
|
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
|
|
65
|
-
s.start(
|
|
66
|
-
await
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
package/src/commands/dev.js
CHANGED
|
@@ -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
|
|
33
|
-
s.start('Setting up
|
|
32
|
+
// Step 1: Scaffold temporary project
|
|
33
|
+
s.start('Setting up project...');
|
|
34
34
|
const projectDir = await scaffoldProject(templatePath);
|
|
35
|
-
s.stop('
|
|
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(
|
|
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
|
|
119
|
-
note(`Starting
|
|
120
|
-
await
|
|
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
|
+
}
|