@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.
@@ -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
+ }
@@ -1,9 +1,15 @@
1
1
  import pkg from "fs-extra";
2
- const { readFile, writeFile, ensureDir } = pkg;
2
+ const { readFile, writeFile, ensureDir, pathExists } = pkg;
3
3
  import { join } from "path";
4
4
  import { generateThemeStyles } from "./colors.js";
5
5
 
6
- export async function generateConfig(projectDir, options) {
6
+ /**
7
+ * Generate configuration for the project
8
+ * @param {string} projectDir - The scaffolded project directory
9
+ * @param {object} options - CLI options
10
+ * @param {object|null} frameworkConfig - Framework configuration (optional)
11
+ */
12
+ export async function generateConfig(projectDir, options, frameworkConfig = null) {
7
13
  const {
8
14
  baseUrl,
9
15
  name,
@@ -68,27 +74,65 @@ export async function generateConfig(projectDir, options) {
68
74
  await writeFile(join(stylesDir, "generated-theme.css"), "/* No generated theme styles */", "utf-8");
69
75
  }
70
76
 
71
- // Update astro.config.mjs with base URL and site URL
72
- if ((baseUrl && baseUrl !== "/") || config.metadata?.url) {
77
+ // Update astro.config.mjs with base URL and site URL (only for Astro)
78
+ const frameworkName = frameworkConfig?.name || 'astro';
79
+
80
+ if (frameworkName === 'astro' && ((baseUrl && baseUrl !== "/") || config.metadata?.url)) {
73
81
  const astroConfigPath = join(projectDir, "astro.config.mjs");
74
- let content = await readFile(astroConfigPath, "utf-8");
82
+ if (await pathExists(astroConfigPath)) {
83
+ let content = await readFile(astroConfigPath, "utf-8");
84
+
85
+ let injection = "export default defineConfig({\n";
86
+
87
+ if (baseUrl && baseUrl !== "/") {
88
+ injection += ` base: '${baseUrl}',\n`;
89
+ }
90
+
91
+ if (config.metadata?.url) {
92
+ injection += ` site: '${config.metadata.url}',\n`;
93
+ }
94
+
95
+ // Add base/site option to defineConfig
96
+ content = content.replace(
97
+ "export default defineConfig({",
98
+ injection
99
+ );
75
100
 
76
- let injection = "export default defineConfig({\n";
77
-
78
- if (baseUrl && baseUrl !== "/") {
79
- injection += ` base: '${baseUrl}',\n`;
101
+ await writeFile(astroConfigPath, content, "utf-8");
80
102
  }
81
-
82
- if (config.metadata?.url) {
83
- injection += ` site: '${config.metadata.url}',\n`;
103
+ }
104
+
105
+ // Update vite.config.js for React/Vue frameworks
106
+ if (['react', 'vue'].includes(frameworkName) && baseUrl && baseUrl !== "/") {
107
+ const viteConfigPath = join(projectDir, "vite.config.js");
108
+ if (await pathExists(viteConfigPath)) {
109
+ let content = await readFile(viteConfigPath, "utf-8");
110
+
111
+ // Add base option to defineConfig
112
+ if (content.includes("defineConfig({")) {
113
+ content = content.replace(
114
+ "defineConfig({",
115
+ `defineConfig({\n base: '${baseUrl}',`
116
+ );
117
+ await writeFile(viteConfigPath, content, "utf-8");
118
+ }
84
119
  }
120
+ }
85
121
 
86
- // Add base/site option to defineConfig
87
- content = content.replace(
88
- "export default defineConfig({",
89
- injection
90
- );
122
+ // Update next.config.js for Next.js
123
+ if (frameworkName === 'next' && baseUrl && baseUrl !== "/") {
124
+ const nextConfigPath = join(projectDir, "next.config.js");
125
+ if (await pathExists(nextConfigPath)) {
126
+ let content = await readFile(nextConfigPath, "utf-8");
91
127
 
92
- await writeFile(astroConfigPath, content, "utf-8");
128
+ // Add basePath to Next.js config
129
+ if (!content.includes("basePath")) {
130
+ content = content.replace(
131
+ "module.exports = {",
132
+ `module.exports = {\n basePath: '${baseUrl}',`
133
+ );
134
+ await writeFile(nextConfigPath, content, "utf-8");
135
+ }
136
+ }
93
137
  }
94
138
  }