@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 +1 -1
- package/src/commands/build.js +3 -3
- package/src/commands/dev.js +2 -2
- package/src/commands/eject.js +2 -2
- package/src/core/scaffold.js +105 -17
- package/src/core/template-fetcher.js +9 -5
package/package.json
CHANGED
package/src/commands/build.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/dev.js
CHANGED
|
@@ -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
|
};
|
package/src/commands/eject.js
CHANGED
|
@@ -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
|
package/src/core/scaffold.js
CHANGED
|
@@ -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
|
-
//
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
125
|
+
return projectDir;
|
|
45
126
|
}
|
|
46
127
|
|
|
47
|
-
// Cleanup function to remove
|
|
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(
|
|
133
|
+
await remove(projectDir);
|
|
51
134
|
} catch (error) {
|
|
52
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
'-
|
|
129
|
-
'
|
|
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);
|