@litodocs/cli 1.3.3 → 1.4.1

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.3",
3
+ "version": "1.4.1",
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
@@ -108,15 +108,43 @@ export async function generateConfig(projectDir, options, frameworkConfig = null
108
108
  if (await pathExists(astroConfigPath)) {
109
109
  let content = await readFile(astroConfigPath, "utf-8");
110
110
 
111
- // Add import after the last existing import line
112
- const importLine = `import astroLlmsTxt from '@4hse/astro-llms-txt';`;
113
- if (!content.includes(importLine)) {
111
+ // Add imports after the last existing import line
112
+ const importLines = [
113
+ `import _astroLlmsTxt from '@4hse/astro-llms-txt';`,
114
+ `import { fileURLToPath as _llmsFileURLToPath } from 'url';`,
115
+ ];
116
+ // Wrapper: fix Windows dir.pathname (/C:/... → C:/...) before passing to plugin
117
+ const wrapperFn = `function astroLlmsTxt(opts) {
118
+ const inner = _astroLlmsTxt(opts);
119
+ const origHook = inner.hooks['astro:build:done'];
120
+ inner.hooks['astro:build:done'] = async (args) => {
121
+ if (args.dir && args.dir.pathname) {
122
+ const fixed = _llmsFileURLToPath(args.dir);
123
+ args = { ...args, dir: { ...args.dir, pathname: fixed } };
124
+ }
125
+ return origHook(args);
126
+ };
127
+ return inner;
128
+ }`;
129
+ for (const importLine of importLines) {
130
+ if (!content.includes(importLine)) {
131
+ const lines = content.split('\n');
132
+ let lastImportIdx = 0;
133
+ for (let i = 0; i < lines.length; i++) {
134
+ if (lines[i].startsWith('import ')) lastImportIdx = i;
135
+ }
136
+ lines.splice(lastImportIdx + 1, 0, importLine);
137
+ content = lines.join('\n');
138
+ }
139
+ }
140
+ // Inject wrapper function after imports
141
+ if (!content.includes('function astroLlmsTxt(')) {
114
142
  const lines = content.split('\n');
115
143
  let lastImportIdx = 0;
116
144
  for (let i = 0; i < lines.length; i++) {
117
145
  if (lines[i].startsWith('import ')) lastImportIdx = i;
118
146
  }
119
- lines.splice(lastImportIdx + 1, 0, importLine);
147
+ lines.splice(lastImportIdx + 1, 0, '', wrapperFn);
120
148
  content = lines.join('\n');
121
149
  }
122
150
 
@@ -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
  }