@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.
@@ -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
+ }