@litodocs/cli 0.5.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/LICENSE +21 -0
- package/README.md +260 -0
- package/bin/cli.js +8 -0
- package/package.json +50 -0
- package/src/cli.js +116 -0
- package/src/commands/build.js +105 -0
- package/src/commands/dev.js +133 -0
- package/src/commands/eject.js +89 -0
- package/src/commands/template.js +80 -0
- package/src/core/astro.js +9 -0
- package/src/core/colors.js +142 -0
- package/src/core/config-sync.js +117 -0
- package/src/core/config.js +94 -0
- package/src/core/output.js +15 -0
- package/src/core/package-manager.js +96 -0
- package/src/core/providers.js +78 -0
- package/src/core/scaffold.js +53 -0
- package/src/core/sync.js +364 -0
- package/src/core/template-fetcher.js +242 -0
- package/src/core/template-registry.js +50 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import pkg from 'fs-extra';
|
|
2
|
+
const { ensureDir, copy, remove, pathExists, readJson, writeJson } = pkg;
|
|
3
|
+
import { homedir, tmpdir } from 'os';
|
|
4
|
+
import { join, basename } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
import { createWriteStream } from 'fs';
|
|
8
|
+
import { pipeline } from 'stream/promises';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
// Cache directory for downloaded templates
|
|
14
|
+
const CACHE_DIR = join(homedir(), '.lito', 'templates');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a template specification string into a structured format
|
|
18
|
+
* Supports:
|
|
19
|
+
* - 'default' or null -> registry lookup
|
|
20
|
+
* - 'github:owner/repo' -> GitHub repo (default branch)
|
|
21
|
+
* - 'github:owner/repo#ref' -> GitHub repo with specific branch/tag
|
|
22
|
+
* - 'github:owner/repo/path/to/template' -> GitHub repo with subdirectory
|
|
23
|
+
* - 'github:owner/repo/path#ref' -> GitHub repo with subdirectory and ref
|
|
24
|
+
* - './path/to/template' or '/absolute/path' -> local path
|
|
25
|
+
* - 'template-name' -> lookup in registry
|
|
26
|
+
*/
|
|
27
|
+
export function parseThemeSpec(themeSpec) {
|
|
28
|
+
if (!themeSpec || themeSpec === 'default') {
|
|
29
|
+
return { type: 'registry', name: 'default' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// GitHub format: github:owner/repo[/path]#ref or github:owner/repo[/path]
|
|
33
|
+
if (themeSpec.startsWith('github:')) {
|
|
34
|
+
const repoSpec = themeSpec.slice(7);
|
|
35
|
+
const [pathPart, ref = 'main'] = repoSpec.split('#');
|
|
36
|
+
const parts = pathPart.split('/');
|
|
37
|
+
|
|
38
|
+
if (parts.length < 2) {
|
|
39
|
+
throw new Error(`Invalid GitHub template format: ${themeSpec}. Expected: github:owner/repo or github:owner/repo/path#ref`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const owner = parts[0];
|
|
43
|
+
const repo = parts[1];
|
|
44
|
+
const subPath = parts.length > 2 ? parts.slice(2).join('/') : null;
|
|
45
|
+
|
|
46
|
+
return { type: 'github', owner, repo, ref, subPath };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Local path (starts with . or /)
|
|
50
|
+
if (themeSpec.startsWith('.') || themeSpec.startsWith('/')) {
|
|
51
|
+
return { type: 'local', path: themeSpec };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Registry name (fallback)
|
|
55
|
+
return { type: 'registry', name: themeSpec };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get cache key for a GitHub template
|
|
60
|
+
*/
|
|
61
|
+
function getCacheKey(owner, repo, ref) {
|
|
62
|
+
return `${owner}-${repo}-${ref}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a cached template exists and return its path
|
|
67
|
+
*/
|
|
68
|
+
export async function getCachedTemplate(owner, repo, ref) {
|
|
69
|
+
const cacheKey = getCacheKey(owner, repo, ref);
|
|
70
|
+
const cachePath = join(CACHE_DIR, cacheKey);
|
|
71
|
+
|
|
72
|
+
if (await pathExists(cachePath)) {
|
|
73
|
+
// Check if cache metadata exists and is valid
|
|
74
|
+
const metaPath = join(cachePath, '.lito-cache.json');
|
|
75
|
+
if (await pathExists(metaPath)) {
|
|
76
|
+
const meta = await readJson(metaPath);
|
|
77
|
+
// Cache is valid for 24 hours by default
|
|
78
|
+
const maxAge = 24 * 60 * 60 * 1000; // 24 hours in ms
|
|
79
|
+
if (Date.now() - meta.cachedAt < maxAge) {
|
|
80
|
+
return cachePath;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Download a GitHub repository as a tarball and extract it
|
|
90
|
+
*/
|
|
91
|
+
export async function fetchGitHubTemplate(owner, repo, ref) {
|
|
92
|
+
const cacheKey = getCacheKey(owner, repo, ref);
|
|
93
|
+
const cachePath = join(CACHE_DIR, cacheKey);
|
|
94
|
+
const tempTarPath = join(tmpdir(), `lito-${cacheKey}.tar.gz`);
|
|
95
|
+
|
|
96
|
+
// Ensure cache directory exists
|
|
97
|
+
await ensureDir(CACHE_DIR);
|
|
98
|
+
|
|
99
|
+
// Download the tarball
|
|
100
|
+
const tarballUrl = `https://api.github.com/repos/${owner}/${repo}/tarball/${ref}`;
|
|
101
|
+
|
|
102
|
+
const response = await fetch(tarballUrl, {
|
|
103
|
+
headers: {
|
|
104
|
+
'Accept': 'application/vnd.github+json',
|
|
105
|
+
'User-Agent': 'lito-cli'
|
|
106
|
+
},
|
|
107
|
+
redirect: 'follow'
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
if (response.status === 404) {
|
|
112
|
+
throw new Error(`Template not found: github:${owner}/${repo}#${ref}`);
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Failed to fetch template: ${response.status} ${response.statusText}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Write tarball to temp file
|
|
118
|
+
const fileStream = createWriteStream(tempTarPath);
|
|
119
|
+
await pipeline(response.body, fileStream);
|
|
120
|
+
|
|
121
|
+
// Extract tarball to cache directory
|
|
122
|
+
await ensureDir(cachePath);
|
|
123
|
+
|
|
124
|
+
// Use tar to extract (available on all Unix systems and modern Windows)
|
|
125
|
+
const { execa } = await import('execa');
|
|
126
|
+
await execa('tar', [
|
|
127
|
+
'-xzf', tempTarPath,
|
|
128
|
+
'-C', cachePath,
|
|
129
|
+
'--strip-components=1'
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
// Cleanup temp tarball
|
|
133
|
+
await remove(tempTarPath);
|
|
134
|
+
|
|
135
|
+
// Write cache metadata
|
|
136
|
+
const metaPath = join(cachePath, '.lito-cache.json');
|
|
137
|
+
await writeJson(metaPath, {
|
|
138
|
+
owner,
|
|
139
|
+
repo,
|
|
140
|
+
ref,
|
|
141
|
+
cachedAt: Date.now()
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return cachePath;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get the bundled template path
|
|
149
|
+
*/
|
|
150
|
+
export function getBundledTemplatePath() {
|
|
151
|
+
return join(__dirname, '../template');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Main entry point: resolve template spec and return template path
|
|
156
|
+
* @param {string} themeSpec - Template specification
|
|
157
|
+
* @param {boolean} forceRefresh - If true, bypass cache and re-download
|
|
158
|
+
*/
|
|
159
|
+
export async function getTemplatePath(themeSpec, forceRefresh = false) {
|
|
160
|
+
const parsed = parseThemeSpec(themeSpec);
|
|
161
|
+
|
|
162
|
+
switch (parsed.type) {
|
|
163
|
+
|
|
164
|
+
case 'github': {
|
|
165
|
+
// Check cache first (unless forcing refresh)
|
|
166
|
+
let basePath = forceRefresh ? null : await getCachedTemplate(parsed.owner, parsed.repo, parsed.ref);
|
|
167
|
+
if (!basePath) {
|
|
168
|
+
// Fetch from GitHub
|
|
169
|
+
basePath = await fetchGitHubTemplate(parsed.owner, parsed.repo, parsed.ref);
|
|
170
|
+
}
|
|
171
|
+
// If subPath is specified, return the subdirectory
|
|
172
|
+
if (parsed.subPath) {
|
|
173
|
+
const fullPath = join(basePath, parsed.subPath);
|
|
174
|
+
if (!(await pathExists(fullPath))) {
|
|
175
|
+
throw new Error(`Template subdirectory not found: ${parsed.subPath} in github:${parsed.owner}/${parsed.repo}`);
|
|
176
|
+
}
|
|
177
|
+
return fullPath;
|
|
178
|
+
}
|
|
179
|
+
return basePath;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'local': {
|
|
183
|
+
const { resolve } = await import('path');
|
|
184
|
+
const localPath = resolve(parsed.path);
|
|
185
|
+
if (!(await pathExists(localPath))) {
|
|
186
|
+
throw new Error(`Local template not found: ${localPath}`);
|
|
187
|
+
}
|
|
188
|
+
return localPath;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'registry': {
|
|
192
|
+
// Import registry and resolve
|
|
193
|
+
const { resolveRegistryName } = await import('./template-registry.js');
|
|
194
|
+
const resolved = resolveRegistryName(parsed.name);
|
|
195
|
+
if (!resolved) {
|
|
196
|
+
throw new Error(`Unknown template: ${parsed.name}. Use 'lito template list' to see available templates.`);
|
|
197
|
+
}
|
|
198
|
+
// Recursively resolve the GitHub URL
|
|
199
|
+
return await getTemplatePath(resolved);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
default:
|
|
203
|
+
throw new Error(`Unknown theme type: ${parsed.type}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Clear the template cache
|
|
209
|
+
*/
|
|
210
|
+
export async function clearTemplateCache() {
|
|
211
|
+
if (await pathExists(CACHE_DIR)) {
|
|
212
|
+
await remove(CACHE_DIR);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* List cached templates
|
|
218
|
+
*/
|
|
219
|
+
export async function listCachedTemplates() {
|
|
220
|
+
if (!(await pathExists(CACHE_DIR))) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const { readdir } = await import('fs/promises');
|
|
225
|
+
const entries = await readdir(CACHE_DIR, { withFileTypes: true });
|
|
226
|
+
|
|
227
|
+
const templates = [];
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
if (entry.isDirectory()) {
|
|
230
|
+
const metaPath = join(CACHE_DIR, entry.name, '.lito-cache.json');
|
|
231
|
+
if (await pathExists(metaPath)) {
|
|
232
|
+
const meta = await readJson(metaPath);
|
|
233
|
+
templates.push({
|
|
234
|
+
name: entry.name,
|
|
235
|
+
...meta
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return templates;
|
|
242
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Registry - Maps shorthand names to GitHub template URLs
|
|
3
|
+
*
|
|
4
|
+
* Users can use these short names instead of full GitHub URLs:
|
|
5
|
+
* lito dev -i . --theme modern
|
|
6
|
+
*
|
|
7
|
+
* Instead of:
|
|
8
|
+
* lito dev -i . --theme github:devrohit06/lito-theme-modern
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const TEMPLATE_REGISTRY = {
|
|
12
|
+
// Default template (fetched from GitHub)
|
|
13
|
+
'default': 'github:Lito-docs/template',
|
|
14
|
+
|
|
15
|
+
// Official templates
|
|
16
|
+
// 'modern': 'github:devrohit06/lito-theme-modern',
|
|
17
|
+
// 'minimal': 'github:devrohit06/lito-theme-minimal',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a registry name to a GitHub URL or null for bundled
|
|
22
|
+
* Returns undefined if the name is not in the registry
|
|
23
|
+
*/
|
|
24
|
+
export function resolveRegistryName(name) {
|
|
25
|
+
if (name in TEMPLATE_REGISTRY) {
|
|
26
|
+
return TEMPLATE_REGISTRY[name];
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get all available template names from the registry
|
|
33
|
+
*/
|
|
34
|
+
export function getRegistryNames() {
|
|
35
|
+
return Object.keys(TEMPLATE_REGISTRY);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get template info from registry
|
|
40
|
+
*/
|
|
41
|
+
export function getRegistryInfo(name) {
|
|
42
|
+
if (name in TEMPLATE_REGISTRY) {
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
source: TEMPLATE_REGISTRY[name] || 'bundled',
|
|
46
|
+
isBundled: TEMPLATE_REGISTRY[name] === null
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|