@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,89 @@
1
+ import { existsSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { intro, outro, spinner, log, note, isCancel, cancel } from '@clack/prompts';
4
+ import pc from 'picocolors';
5
+ import fs from 'fs-extra';
6
+ import { scaffoldProject, cleanupProject } from '../core/scaffold.js';
7
+ import { syncDocs } from '../core/sync.js';
8
+ import { generateConfig } from '../core/config.js';
9
+ import { syncDocsConfig } from '../core/config-sync.js';
10
+ import { getTemplatePath } from '../core/template-fetcher.js';
11
+
12
+ export async function ejectCommand(options) {
13
+ try {
14
+ // Validate input path
15
+ const inputPath = resolve(options.input);
16
+ if (!existsSync(inputPath)) {
17
+ log.error(`Input path does not exist: ${pc.cyan(inputPath)}`);
18
+ process.exit(1);
19
+ }
20
+
21
+ const outputPath = resolve(options.output);
22
+ if (existsSync(outputPath)) {
23
+ log.error(`Output path already exists: ${pc.cyan(outputPath)}`);
24
+ log.warning('Please provide a path to a non-existent directory to avoid overwriting files.');
25
+ process.exit(1);
26
+ }
27
+
28
+ console.clear();
29
+ intro(pc.inverse(pc.cyan(' Lito - Eject ')));
30
+
31
+ const s = spinner();
32
+
33
+ // Step 0: Resolve template
34
+ s.start('Resolving template...');
35
+ const templatePath = await getTemplatePath(options.template, options.refresh);
36
+ s.stop(templatePath ? `Using template: ${pc.cyan(templatePath)}` : 'Using bundled template');
37
+
38
+ // Step 1: Scaffold temporary Astro project
39
+ s.start('Scaffolding Astro project...');
40
+ const projectDir = await scaffoldProject(templatePath);
41
+ s.stop('Astro project scaffolded');
42
+
43
+ // Step 2: Sync docs to Astro
44
+ s.start('Syncing documentation files...');
45
+ await syncDocs(inputPath, projectDir);
46
+ s.stop('Documentation synced');
47
+
48
+ // Step 3: Sync docs config (auto-generate navigation)
49
+ s.start('Generating navigation...');
50
+ const userConfigPath = resolve(options.input, 'docs-config.json');
51
+ await syncDocsConfig(projectDir, inputPath, userConfigPath);
52
+ s.stop('Navigation generated');
53
+
54
+ // Step 4: Generate config
55
+ s.start('Generating Astro configuration...');
56
+ await generateConfig(projectDir, options);
57
+ s.stop('Configuration generated');
58
+
59
+ // Step 5: Copy entire project to output
60
+ s.start('Exporting project source code...');
61
+ await fs.copy(projectDir, outputPath);
62
+ s.stop(`Project exported to ${pc.cyan(outputPath)}`);
63
+
64
+ // Clean up temp directory
65
+ s.start('Cleaning up...');
66
+ await cleanupProject();
67
+ s.stop('Cleanup complete');
68
+
69
+ // Step 6: Final instructions
70
+ const { getInstallInstruction, getRunInstruction } = await import('../core/package-manager.js');
71
+ const installCmd = await getInstallInstruction();
72
+ const devCmd = await getRunInstruction('dev');
73
+
74
+ note(`${pc.cyan(`cd ${options.output}`)}\n${pc.cyan(installCmd)}\n${pc.cyan(devCmd)}`, 'To get started');
75
+
76
+ outro(pc.green('Project ejected successfully!'));
77
+
78
+ } catch (error) {
79
+ if (isCancel(error)) {
80
+ cancel('Operation cancelled.');
81
+ process.exit(0);
82
+ }
83
+ log.error(pc.red('Eject failed: ' + error.message));
84
+ if (error.stack) {
85
+ log.error(pc.gray(error.stack));
86
+ }
87
+ process.exit(1);
88
+ }
89
+ }
@@ -0,0 +1,80 @@
1
+ import { intro, outro, spinner, log, note } from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { listCachedTemplates, clearTemplateCache } from '../core/template-fetcher.js';
4
+ import { getRegistryNames, getRegistryInfo } from '../core/template-registry.js';
5
+
6
+ /**
7
+ * List available templates
8
+ */
9
+ export async function templateListCommand() {
10
+ intro(pc.inverse(pc.cyan(' Lito - Templates ')));
11
+
12
+ // Show registry templates
13
+ const names = getRegistryNames();
14
+
15
+ log.info(pc.bold('Available Templates:'));
16
+ console.log('');
17
+
18
+ for (const name of names) {
19
+ const info = getRegistryInfo(name);
20
+ console.log(` ${pc.blue('●')} ${pc.bold(name)} ${pc.dim(`→ ${info.source}`)}`);
21
+ }
22
+
23
+ console.log('');
24
+ log.info(pc.dim('You can also use GitHub URLs directly:'));
25
+ console.log(pc.dim(' lito dev -i . --template github:owner/repo'));
26
+ console.log(pc.dim(' lito dev -i . --template github:owner/repo#v1.0.0'));
27
+ console.log('');
28
+
29
+ // Show cached templates
30
+ const cached = await listCachedTemplates();
31
+ if (cached.length > 0) {
32
+ log.info(pc.bold('Cached Templates:'));
33
+ console.log('');
34
+ for (const t of cached) {
35
+ const age = Math.round((Date.now() - t.cachedAt) / (1000 * 60));
36
+ console.log(` ${pc.yellow('●')} ${t.owner}/${t.repo}#${t.ref} ${pc.dim(`(cached ${age}m ago)`)}`);
37
+ }
38
+ console.log('');
39
+ }
40
+
41
+ outro(pc.dim('Use --template to select a template'));
42
+ }
43
+
44
+ /**
45
+ * Manage template cache
46
+ */
47
+ export async function templateCacheCommand(options) {
48
+ intro(pc.inverse(pc.cyan(' Lito - Template Cache ')));
49
+
50
+ if (options.clear) {
51
+ const s = spinner();
52
+ s.start('Clearing template cache...');
53
+ await clearTemplateCache();
54
+ s.stop('Template cache cleared');
55
+ outro(pc.green('Done!'));
56
+ return;
57
+ }
58
+
59
+ // Show cache info
60
+ const cached = await listCachedTemplates();
61
+
62
+ if (cached.length === 0) {
63
+ log.info('No templates cached.');
64
+ outro('');
65
+ return;
66
+ }
67
+
68
+ log.info(pc.bold(`${cached.length} template(s) cached:`));
69
+ console.log('');
70
+
71
+ for (const t of cached) {
72
+ const age = Math.round((Date.now() - t.cachedAt) / (1000 * 60 * 60));
73
+ console.log(` ${pc.cyan(t.owner)}/${pc.cyan(t.repo)}#${pc.yellow(t.ref)}`);
74
+ console.log(` ${pc.dim(`Cached ${age}h ago`)}`);
75
+ }
76
+
77
+ console.log('');
78
+ note('lito template cache --clear', 'To clear cache');
79
+ outro('');
80
+ }
@@ -0,0 +1,9 @@
1
+ import { runBinary } from './package-manager.js';
2
+
3
+ export async function runAstroBuild(projectDir) {
4
+ await runBinary(projectDir, 'astro', ['build']);
5
+ }
6
+
7
+ export async function runAstroDev(projectDir, port = '4321') {
8
+ await runBinary(projectDir, 'astro', ['dev', '--port', port]);
9
+ }
@@ -0,0 +1,142 @@
1
+
2
+
3
+ function hexToRgb(hex) {
4
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
5
+ if (!result) return { r: 0, g: 0, b: 0 };
6
+ return {
7
+ r: parseInt(result[1], 16) / 255,
8
+ g: parseInt(result[2], 16) / 255,
9
+ b: parseInt(result[3], 16) / 255,
10
+ };
11
+ }
12
+
13
+ function linearRgbToOklab(rgb) {
14
+ const toLinear = (c) =>
15
+ c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
16
+ const r = toLinear(rgb.r);
17
+ const g = toLinear(rgb.g);
18
+ const b = toLinear(rgb.b);
19
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
20
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
21
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
22
+ return {
23
+ L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
24
+ a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
25
+ b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
26
+ };
27
+ }
28
+
29
+ function oklabToOklch(lab) {
30
+ const c = Math.sqrt(lab.a * lab.a + lab.b * lab.b);
31
+ let h = (Math.atan2(lab.b, lab.a) * 180) / Math.PI;
32
+ if (h < 0) h += 360;
33
+ return { l: lab.L, c: c, h: h };
34
+ }
35
+
36
+ function hexToOklch(hex) {
37
+ return oklabToOklch(linearRgbToOklab(hexToRgb(hex)));
38
+ }
39
+
40
+ function formatOklch(oklch, alpha) {
41
+ const l = oklch.l.toFixed(3);
42
+ const c = oklch.c.toFixed(3);
43
+ const h = Math.round(oklch.h);
44
+ return alpha !== undefined
45
+ ? `oklch(${l} ${c} ${h} / ${alpha})`
46
+ : `oklch(${l} ${c} ${h})`;
47
+ }
48
+
49
+ function generatePalette(hex, prefix = 'primary') {
50
+ const base = hexToOklch(hex);
51
+ const shades = {
52
+ '50': 0.98, '100': 0.95, '200': 0.90, '300': 0.82, '400': 0.70,
53
+ '500': 0.55, '600': 0.48, '700': 0.40, '800': 0.32, '900': 0.24, '950': 0.18,
54
+ };
55
+ const palette = {};
56
+ for (const [shade, lightness] of Object.entries(shades)) {
57
+ let chroma = base.c;
58
+ if (lightness > 0.85 || lightness < 0.25) chroma = base.c * 0.3;
59
+ else if (lightness > 0.75 || lightness < 0.35) chroma = base.c * 0.6;
60
+ else if (lightness > 0.65 || lightness < 0.45) chroma = base.c * 0.85;
61
+ palette[`--${prefix}-${shade}`] = formatOklch({ l: lightness, c: Math.min(chroma, 0.15), h: base.h });
62
+ }
63
+ return palette;
64
+ }
65
+
66
+ function generateDarkPalette(hex, prefix = 'primary') {
67
+ const base = hexToOklch(hex);
68
+ const shades = {
69
+ '50': 0.20, '100': 0.25, '200': 0.32, '300': 0.42, '400': 0.55,
70
+ '500': 0.65, '600': 0.72, '700': 0.80, '800': 0.88, '900': 0.94, '950': 0.97,
71
+ };
72
+ const palette = {};
73
+ for (const [shade, lightness] of Object.entries(shades)) {
74
+ let chroma = base.c;
75
+ if (lightness > 0.85 || lightness < 0.25) chroma = base.c * 0.3;
76
+ else if (lightness > 0.75 || lightness < 0.35) chroma = base.c * 0.6;
77
+ else if (lightness > 0.65 || lightness < 0.45) chroma = base.c * 0.85;
78
+ palette[`--${prefix}-${shade}`] = formatOklch({ l: lightness, c: Math.min(chroma, 0.15), h: base.h });
79
+ }
80
+ return palette;
81
+ }
82
+
83
+ function generateGrayPalette(tintHex) {
84
+ const tint = tintHex ? hexToOklch(tintHex) : { l: 0, c: 0, h: 0 };
85
+ const hue = tint.h;
86
+ const lightShades = {
87
+ '50': { l: 0.99, c: 0.002 }, '100': { l: 0.97, c: 0.003 }, '200': { l: 0.93, c: 0.005 },
88
+ '300': { l: 0.87, c: 0.008 }, '400': { l: 0.70, c: 0.010 }, '500': { l: 0.55, c: 0.012 },
89
+ '600': { l: 0.45, c: 0.010 }, '700': { l: 0.35, c: 0.008 }, '800': { l: 0.25, c: 0.005 },
90
+ '900': { l: 0.18, c: 0.003 }, '950': { l: 0.12, c: 0.002 },
91
+ };
92
+ const darkShades = {
93
+ '50': { l: 0.13, c: 0.003 }, '100': { l: 0.17, c: 0.005 }, '200': { l: 0.22, c: 0.008 },
94
+ '300': { l: 0.30, c: 0.010 }, '400': { l: 0.42, c: 0.012 }, '500': { l: 0.55, c: 0.010 },
95
+ '600': { l: 0.65, c: 0.008 }, '700': { l: 0.75, c: 0.005 }, '800': { l: 0.85, c: 0.003 },
96
+ '900': { l: 0.92, c: 0.002 }, '950': { l: 0.97, c: 0.001 },
97
+ };
98
+ const light = {};
99
+ const dark = {};
100
+ for (const [shade, values] of Object.entries(lightShades)) {
101
+ light[`--gray-${shade}`] = formatOklch({ l: values.l, c: values.c, h: hue });
102
+ }
103
+ for (const [shade, values] of Object.entries(darkShades)) {
104
+ dark[`--gray-${shade}`] = formatOklch({ l: values.l, c: values.c, h: hue });
105
+ }
106
+ return { light, dark };
107
+ }
108
+
109
+ export function generateThemeStyles(colors) {
110
+ if (!colors || !colors.primary) return '';
111
+ const primaryPalette = generatePalette(colors.primary, 'primary');
112
+ const primaryDarkPalette = generateDarkPalette(colors.primary, 'primary');
113
+ const grayPalettes = generateGrayPalette(colors.primary);
114
+
115
+ let lightVars = Object.entries(primaryPalette).map(([k, v]) => `${k}: ${v};`).join('\n ');
116
+ lightVars += '\n ' + Object.entries(grayPalettes.light).map(([k, v]) => `${k}: ${v};`).join('\n ');
117
+
118
+ let darkVars = Object.entries(primaryDarkPalette).map(([k, v]) => `${k}: ${v};`).join('\n ');
119
+ darkVars += '\n ' + Object.entries(grayPalettes.dark).map(([k, v]) => `${k}: ${v};`).join('\n ');
120
+
121
+ const baseOklch = hexToOklch(colors.primary);
122
+ const shadowVars = `
123
+ --shadow-primary-sm: 0 2px 8px -2px ${formatOklch({ l: 0.55, c: 0.13, h: baseOklch.h }, 0.20)};
124
+ --shadow-primary: 0 4px 16px -4px ${formatOklch({ l: 0.55, c: 0.13, h: baseOklch.h }, 0.25)};
125
+ --shadow-primary-lg: 0 12px 32px -8px ${formatOklch({ l: 0.55, c: 0.13, h: baseOklch.h }, 0.30)};`;
126
+ const darkShadowVars = `
127
+ --shadow-primary-sm: 0 2px 8px -2px ${formatOklch({ l: 0.65, c: 0.13, h: baseOklch.h }, 0.30)};
128
+ --shadow-primary: 0 4px 16px -4px ${formatOklch({ l: 0.65, c: 0.13, h: baseOklch.h }, 0.40)};
129
+ --shadow-primary-lg: 0 12px 32px -8px ${formatOklch({ l: 0.65, c: 0.13, h: baseOklch.h }, 0.50)};`;
130
+
131
+ return `
132
+ /* Generated Theme Styles */
133
+ :root {
134
+ ${lightVars}
135
+ ${shadowVars}
136
+ }
137
+
138
+ .dark {
139
+ ${darkVars}
140
+ ${darkShadowVars}
141
+ }`;
142
+ }
@@ -0,0 +1,117 @@
1
+ import pkg from 'fs-extra';
2
+ const { readFile, writeFile, pathExists } = pkg;
3
+ import { join } from 'path';
4
+ import { readdir } from 'fs/promises';
5
+
6
+ /**
7
+ * Auto-generates navigation structure from docs folder
8
+ */
9
+ export async function generateNavigationFromDocs(docsPath) {
10
+ const sidebar = [];
11
+
12
+ try {
13
+ const entries = await readdir(docsPath, { withFileTypes: true });
14
+
15
+ // Group files by directory
16
+ const groups = new Map();
17
+
18
+ for (const entry of entries) {
19
+ if (entry.isDirectory()) {
20
+ const dirName = entry.name;
21
+ const dirPath = join(docsPath, dirName);
22
+ const files = await readdir(dirPath);
23
+
24
+ const items = files
25
+ .filter(f => f.endsWith('.md') || f.endsWith('.mdx'))
26
+ .map(f => ({
27
+ label: formatLabel(f.replace(/\.(md|mdx)$/, '')),
28
+ slug: `${dirName}/${f.replace(/\.(md|mdx)$/, '')}`,
29
+ }))
30
+ .sort((a, b) => a.label.localeCompare(b.label));
31
+
32
+ if (items.length > 0) {
33
+ groups.set(dirName, {
34
+ label: formatLabel(dirName),
35
+ items,
36
+ });
37
+ }
38
+ }
39
+ }
40
+
41
+ // Add root-level files
42
+ const rootFiles = entries
43
+ .filter(e => e.isFile() && (e.name.endsWith('.md') || e.name.endsWith('.mdx')) && e.name !== 'index.md')
44
+ .map(e => ({
45
+ label: formatLabel(e.name.replace(/\.(md|mdx)$/, '')),
46
+ slug: e.name.replace(/\.(md|mdx)$/, ''),
47
+ }))
48
+ .sort((a, b) => a.label.localeCompare(b.label));
49
+
50
+ if (rootFiles.length > 0) {
51
+ sidebar.push({
52
+ label: 'Getting Started',
53
+ icon: 'lucide:rocket',
54
+ items: rootFiles,
55
+ });
56
+ }
57
+
58
+ // Add directory groups sorted alphabetically
59
+ const sortedGroups = Array.from(groups.values()).sort((a, b) => a.label.localeCompare(b.label));
60
+ for (const group of sortedGroups) {
61
+ sidebar.push(group);
62
+ }
63
+
64
+ } catch (error) {
65
+ console.warn('Could not auto-generate navigation:', error.message);
66
+ }
67
+
68
+ return sidebar;
69
+ }
70
+
71
+ /**
72
+ * Merges user config with auto-generated navigation
73
+ */
74
+ export async function syncDocsConfig(projectDir, docsPath, userConfigPath) {
75
+ const configPath = join(projectDir, 'docs-config.json');
76
+
77
+ // Read base config from template
78
+ let config = JSON.parse(await readFile(configPath, 'utf-8'));
79
+
80
+ // If user has a custom config, merge it
81
+ if (userConfigPath && await pathExists(userConfigPath)) {
82
+ const userConfig = JSON.parse(await readFile(userConfigPath, 'utf-8'));
83
+ config = deepMerge(config, userConfig);
84
+ }
85
+
86
+ // Auto-generate navigation if not provided
87
+ if (!config.navigation?.sidebar || config.navigation.sidebar.length === 0) {
88
+ config.navigation.sidebar = await generateNavigationFromDocs(docsPath);
89
+ }
90
+
91
+ // Write merged config
92
+ await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
93
+ }
94
+
95
+ function formatLabel(str) {
96
+ return str
97
+ .split('-')
98
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
99
+ .join(' ');
100
+ }
101
+
102
+ function deepMerge(target, source) {
103
+ const output = { ...target };
104
+
105
+ for (const key in source) {
106
+ // Arrays should be replaced, not merged
107
+ if (Array.isArray(source[key])) {
108
+ output[key] = source[key];
109
+ } else if (source[key] instanceof Object && key in target && !Array.isArray(target[key])) {
110
+ output[key] = deepMerge(target[key], source[key]);
111
+ } else {
112
+ output[key] = source[key];
113
+ }
114
+ }
115
+
116
+ return output;
117
+ }
@@ -0,0 +1,94 @@
1
+ import pkg from "fs-extra";
2
+ const { readFile, writeFile, ensureDir } = pkg;
3
+ import { join } from "path";
4
+ import { generateThemeStyles } from "./colors.js";
5
+
6
+ export async function generateConfig(projectDir, options) {
7
+ const {
8
+ baseUrl,
9
+ name,
10
+ description,
11
+ primaryColor,
12
+ accentColor,
13
+ favicon,
14
+ logo,
15
+ } = options;
16
+
17
+ // Update docs-config.json with metadata and theme options
18
+ const configPath = join(projectDir, "docs-config.json");
19
+ let config = JSON.parse(await readFile(configPath, "utf-8"));
20
+
21
+ // Update metadata
22
+ if (name) {
23
+ config.metadata = config.metadata || {};
24
+ config.metadata.name = name;
25
+ }
26
+
27
+ if (description) {
28
+ config.metadata = config.metadata || {};
29
+ config.metadata.description = description;
30
+ }
31
+
32
+ // Update theme colors
33
+ if (primaryColor || accentColor) {
34
+ config.theme = config.theme || {};
35
+ config.branding = config.branding || {};
36
+ config.branding.colors = config.branding.colors || {};
37
+
38
+ if (primaryColor) {
39
+ config.theme.primaryColor = primaryColor;
40
+ config.branding.colors.primary = primaryColor;
41
+ }
42
+ if (accentColor) {
43
+ config.theme.accentColor = accentColor;
44
+ config.branding.colors.accent = accentColor;
45
+ }
46
+ }
47
+
48
+ // Update branding
49
+ if (favicon || logo) {
50
+ config.branding = config.branding || {};
51
+ if (favicon) config.branding.favicon = favicon;
52
+ if (logo) config.branding.logo = config.branding.logo || {};
53
+ if (logo) config.branding.logo.src = logo;
54
+ }
55
+
56
+ await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
57
+
58
+ // Generate theme styles
59
+ if (config.branding?.colors?.primary) {
60
+ const themeStyles = generateThemeStyles(config.branding.colors);
61
+ const stylesDir = join(projectDir, "src", "styles");
62
+ await ensureDir(stylesDir);
63
+ await writeFile(join(stylesDir, "generated-theme.css"), themeStyles, "utf-8");
64
+ } else {
65
+ // Write empty file to avoid import error
66
+ const stylesDir = join(projectDir, "src", "styles");
67
+ await ensureDir(stylesDir);
68
+ await writeFile(join(stylesDir, "generated-theme.css"), "/* No generated theme styles */", "utf-8");
69
+ }
70
+
71
+ // Update astro.config.mjs with base URL and site URL
72
+ if ((baseUrl && baseUrl !== "/") || config.metadata?.url) {
73
+ const astroConfigPath = join(projectDir, "astro.config.mjs");
74
+ let content = await readFile(astroConfigPath, "utf-8");
75
+
76
+ let injection = "export default defineConfig({\n";
77
+
78
+ if (baseUrl && baseUrl !== "/") {
79
+ injection += ` base: '${baseUrl}',\n`;
80
+ }
81
+
82
+ if (config.metadata?.url) {
83
+ injection += ` site: '${config.metadata.url}',\n`;
84
+ }
85
+
86
+ // Add base/site option to defineConfig
87
+ content = content.replace(
88
+ "export default defineConfig({",
89
+ injection
90
+ );
91
+
92
+ await writeFile(astroConfigPath, content, "utf-8");
93
+ }
94
+ }
@@ -0,0 +1,15 @@
1
+ import pkg from 'fs-extra';
2
+ const { copy, ensureDir } = pkg;
3
+ import { join } from 'path';
4
+
5
+ export async function copyOutput(projectDir, outputPath) {
6
+ const distPath = join(projectDir, 'dist');
7
+
8
+ // Ensure output directory exists
9
+ await ensureDir(outputPath);
10
+
11
+ // Copy built files to output
12
+ await copy(distPath, outputPath, {
13
+ overwrite: true,
14
+ });
15
+ }
@@ -0,0 +1,96 @@
1
+ import { execa } from 'execa';
2
+
3
+ let detectedManager = null;
4
+
5
+ export async function getPackageManager() {
6
+ if (detectedManager) return detectedManager;
7
+
8
+ // Check for Bun first (fastest) - keeping preference for Bun as per user rules if available
9
+ try {
10
+ await execa('bun', ['--version']);
11
+ detectedManager = 'bun';
12
+ return 'bun';
13
+ } catch { }
14
+
15
+ // Check for pnpm
16
+ try {
17
+ await execa('pnpm', ['--version']);
18
+ detectedManager = 'pnpm';
19
+ return 'pnpm';
20
+ } catch { }
21
+
22
+ // Check for yarn
23
+ try {
24
+ await execa('yarn', ['--version']);
25
+ detectedManager = 'yarn';
26
+ return 'yarn';
27
+ } catch { }
28
+
29
+ // Default to npm
30
+ detectedManager = 'npm';
31
+ return 'npm';
32
+ }
33
+
34
+ export async function installDependencies(projectDir, { silent = true } = {}) {
35
+ const manager = await getPackageManager();
36
+
37
+ await execa(manager, ['install'], {
38
+ cwd: projectDir,
39
+ stdio: silent ? 'pipe' : 'inherit',
40
+ env: { ...process.env, CI: 'true' }, // Force non-interactive mode
41
+ });
42
+ }
43
+
44
+ export async function installPackage(projectDir, packageName, { dev = false, silent = true } = {}) {
45
+ const manager = await getPackageManager();
46
+ const args = manager === 'npm' ? ['install'] : ['add'];
47
+
48
+ if (dev) {
49
+ args.push('-D');
50
+ }
51
+
52
+ args.push(packageName);
53
+
54
+ await execa(manager, args, {
55
+ cwd: projectDir,
56
+ stdio: silent ? 'pipe' : 'inherit',
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Runs a binary using the package manager (e.g. 'astro').
62
+ */
63
+ export async function runBinary(projectDir, binary, args = []) {
64
+ const manager = await getPackageManager();
65
+
66
+ let cmd = manager;
67
+ let cmdArgs = [];
68
+
69
+ if (manager === 'npm') {
70
+ cmd = 'npx';
71
+ cmdArgs = [binary, ...args];
72
+ } else {
73
+ // bun astro, pnpm astro, yarn astro
74
+ // yarn might need 'yarn run' or just 'yarn' if bin is exposed? 'yarn astro' works.
75
+ cmdArgs = [binary, ...args];
76
+ }
77
+
78
+ await execa(cmd, cmdArgs, {
79
+ cwd: projectDir,
80
+ stdio: 'inherit',
81
+ preferLocal: true,
82
+ });
83
+ }
84
+
85
+ export async function getRunInstruction(script) {
86
+ const manager = await getPackageManager();
87
+ if (manager === 'npm') {
88
+ return `npm run ${script}`;
89
+ }
90
+ return `${manager} run ${script}`;
91
+ }
92
+
93
+ export async function getInstallInstruction() {
94
+ const manager = await getPackageManager();
95
+ return `${manager} install`;
96
+ }