@litodocs/cli 1.3.2 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litodocs/cli",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Beautiful documentation sites from Markdown. Fast, simple, and open-source.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@ export async function buildCommand(options) {
31
31
 
32
32
  // Step 1: Scaffold temporary project
33
33
  s.start('Setting up project...');
34
- const projectDir = await scaffoldProject(templatePath);
34
+ const projectDir = await scaffoldProject(templatePath, inputPath);
35
35
  s.stop('Project scaffolded');
36
36
 
37
37
  // Step 1.5: Detect framework
@@ -88,7 +88,7 @@ export async function buildCommand(options) {
88
88
 
89
89
  // Cleanup temp directory
90
90
  s.start('Cleaning up...');
91
- await cleanupProject();
91
+ await cleanupProject(projectDir);
92
92
  s.stop('Cleanup complete');
93
93
 
94
94
  outro(pc.green('Build completed successfully!'));
@@ -100,7 +100,7 @@ export async function buildCommand(options) {
100
100
 
101
101
  // Attempt to cleanup even on error
102
102
  try {
103
- await cleanupProject();
103
+ await cleanupProject(projectDir);
104
104
  } catch (e) {
105
105
  // failed to cleanup
106
106
  }
@@ -31,7 +31,7 @@ export async function devCommand(options) {
31
31
 
32
32
  // Step 1: Scaffold temporary project
33
33
  s.start('Setting up project...');
34
- const projectDir = await scaffoldProject(templatePath);
34
+ const projectDir = await scaffoldProject(templatePath, inputPath);
35
35
  s.stop('Project scaffolded');
36
36
 
37
37
  // Step 1.5: Detect framework
@@ -42,7 +42,7 @@ export async function devCommand(options) {
42
42
  // Register cleanup handlers
43
43
  const cleanup = async () => {
44
44
  s.start('Cleaning up...');
45
- await cleanupProject();
45
+ await cleanupProject(projectDir);
46
46
  s.stop('Cleanup complete');
47
47
  process.exit(0);
48
48
  };
@@ -37,7 +37,7 @@ export async function ejectCommand(options) {
37
37
 
38
38
  // Step 1: Scaffold temporary Astro project
39
39
  s.start('Scaffolding Astro project...');
40
- const projectDir = await scaffoldProject(templatePath);
40
+ const projectDir = await scaffoldProject(templatePath, inputPath);
41
41
  s.stop('Astro project scaffolded');
42
42
 
43
43
  // Step 2: Sync docs to Astro
@@ -63,7 +63,7 @@ export async function ejectCommand(options) {
63
63
 
64
64
  // Clean up temp directory
65
65
  s.start('Cleaning up...');
66
- await cleanupProject();
66
+ await cleanupProject(projectDir);
67
67
  s.stop('Cleanup complete');
68
68
 
69
69
  // Step 6: Final instructions
@@ -1,29 +1,110 @@
1
1
  import pkg from 'fs-extra';
2
2
  const { ensureDir, emptyDir, copy, remove } = pkg;
3
3
  import { homedir } from 'os';
4
- import { join, basename } from 'path';
4
+ import { join, basename, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { dirname } from 'path';
7
+ import { platform } from 'os';
8
+ import { execSync } from 'child_process';
9
+ import { createHash } from 'crypto';
7
10
 
8
11
  const __filename = fileURLToPath(import.meta.url);
9
12
  const __dirname = dirname(__filename);
10
13
 
11
- // Use a directory under the user's home to avoid resolution issues
12
- // with bundlers (e.g. Turbopack) that fail to resolve node_modules from /tmp
13
- const LITO_DIR = join(homedir(), '.lito', 'dev-project');
14
+ // Base directory for all dev projects
15
+ const LITO_PROJECTS_DIR = join(homedir(), '.lito', 'dev-projects');
14
16
 
15
- export async function scaffoldProject(customTemplatePath = null) {
16
- // Ensure the directory exists (creates if it doesn't)
17
- await ensureDir(LITO_DIR);
17
+ // Legacy single dev-project path (for migration cleanup)
18
+ const LEGACY_DIR = join(homedir(), '.lito', 'dev-project');
18
19
 
19
- // Empty the directory to ensure a clean state
20
- await emptyDir(LITO_DIR);
20
+ /**
21
+ * Derive a short, unique directory name from an input path.
22
+ * e.g. /home/user/my-docs → "my-docs-a1b2c3"
23
+ */
24
+ function getProjectSlug(inputPath) {
25
+ const resolved = resolve(inputPath);
26
+ const hash = createHash('md5').update(resolved).digest('hex').slice(0, 8);
27
+ const name = basename(resolved).replace(/[^a-zA-Z0-9_-]/g, '_');
28
+ return `${name}-${hash}`;
29
+ }
30
+
31
+ /**
32
+ * Get the project directory for a given input path.
33
+ */
34
+ export function getProjectDir(inputPath) {
35
+ const slug = getProjectSlug(inputPath);
36
+ return join(LITO_PROJECTS_DIR, slug);
37
+ }
21
38
 
22
- const tempDir = LITO_DIR;
39
+ /**
40
+ * Kill stale processes spawned from a specific project directory (Windows).
41
+ * On Windows, .bin/*.exe files stay locked if a previous dev server
42
+ * wasn't shut down cleanly. This finds and kills those processes.
43
+ */
44
+ function killStaleProcesses(projectDir) {
45
+ if (platform() !== 'win32') return;
46
+
47
+ try {
48
+ // Escape backslashes for WMIC LIKE pattern
49
+ const escapedPath = projectDir.replace(/\\/g, '\\\\');
50
+ const cmd = `wmic process where "ExecutablePath like '%${escapedPath}%'" get ProcessId /format:list 2>nul`;
51
+ const output = execSync(cmd, { encoding: 'utf-8', timeout: 5000 });
52
+ const pids = output.match(/ProcessId=(\d+)/g);
53
+ if (pids) {
54
+ for (const match of pids) {
55
+ const pid = match.split('=')[1];
56
+ try {
57
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore', timeout: 3000 });
58
+ } catch { /* process may already be gone */ }
59
+ }
60
+ // Brief wait for OS to release file handles
61
+ execSync('timeout /t 1 /nobreak >nul 2>&1', { timeout: 3000 });
62
+ }
63
+ } catch { /* wmic/taskkill not available or no matching processes */ }
64
+ }
65
+
66
+ /**
67
+ * Safely empty a directory.
68
+ * Retries with stale process cleanup on EPERM (Windows file lock).
69
+ */
70
+ async function safeEmptyDir(dir) {
71
+ try {
72
+ await emptyDir(dir);
73
+ } catch (err) {
74
+ if (err.code === 'EPERM' || err.code === 'EBUSY') {
75
+ killStaleProcesses(dir);
76
+
77
+ // Retry — remove entirely then recreate
78
+ try {
79
+ await remove(dir);
80
+ } catch { /* ignore */ }
81
+ await ensureDir(dir);
82
+ } else {
83
+ throw err;
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Clean up the legacy single dev-project directory if it exists.
90
+ */
91
+ async function cleanupLegacyDir() {
92
+ try {
93
+ await remove(LEGACY_DIR);
94
+ } catch { /* ignore — may not exist or may be locked */ }
95
+ }
96
+
97
+ export async function scaffoldProject(customTemplatePath = null, inputPath = null) {
98
+ const projectDir = inputPath ? getProjectDir(inputPath) : join(LITO_PROJECTS_DIR, '_default');
99
+
100
+ await ensureDir(projectDir);
101
+ await safeEmptyDir(projectDir);
102
+
103
+ // Clean up legacy directory from older CLI versions
104
+ await cleanupLegacyDir();
23
105
 
24
- // Use custom template path if provided, otherwise use bundled template
25
106
  const templatePath = customTemplatePath || join(__dirname, '../template');
26
- await copy(templatePath, tempDir, {
107
+ await copy(templatePath, projectDir, {
27
108
  filter: (src) => {
28
109
  const name = basename(src);
29
110
 
@@ -41,14 +122,21 @@ export async function scaffoldProject(customTemplatePath = null) {
41
122
  }
42
123
  });
43
124
 
44
- return tempDir;
125
+ return projectDir;
45
126
  }
46
127
 
47
- // Cleanup function to remove the temp directory on exit
48
- export async function cleanupProject() {
128
+ // Cleanup function to remove a specific project directory on exit
129
+ export async function cleanupProject(projectDir) {
130
+ if (!projectDir) return;
131
+
49
132
  try {
50
- await remove(LITO_DIR);
133
+ await remove(projectDir);
51
134
  } catch (error) {
52
- // Ignore errors during cleanup (directory might not exist)
135
+ if (error.code === 'EPERM' || error.code === 'EBUSY') {
136
+ killStaleProcesses(projectDir);
137
+ try {
138
+ await remove(projectDir);
139
+ } catch { /* best-effort cleanup */ }
140
+ }
53
141
  }
54
142
  }
@@ -123,11 +123,15 @@ export async function fetchGitHubTemplate(owner, repo, ref) {
123
123
 
124
124
  // Use tar to extract (available on all Unix systems and modern Windows)
125
125
  const { execa } = await import('execa');
126
- await execa('tar', [
127
- '-xzf', tempTarPath,
128
- '-C', cachePath,
129
- '--strip-components=1'
130
- ]);
126
+ const isWin = process.platform === 'win32';
127
+ const tarArgs = [
128
+ '-xzf', isWin ? tempTarPath.replace(/\\/g, '/') : tempTarPath,
129
+ '-C', isWin ? cachePath.replace(/\\/g, '/') : cachePath,
130
+ '--strip-components=1',
131
+ // On Windows, GNU tar misinterprets drive letters (C:) as remote hosts
132
+ ...(isWin ? ['--force-local'] : [])
133
+ ];
134
+ await execa('tar', tarArgs);
131
135
 
132
136
  // Cleanup temp tarball
133
137
  await remove(tempTarPath);