dolly-mls-cli 1.0.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/README.md +109 -0
- package/bin/cli.js +38 -0
- package/lib/create.js +49 -0
- package/lib/download.js +200 -0
- package/lib/replace.js +204 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# mls-cli
|
|
2
|
+
|
|
3
|
+
一个命令行脚手架工具,用于从远程 Git 仓库拉取模板并生成新项目结构。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- 从 GitHub/GitLab 等远程仓库下载项目模板
|
|
8
|
+
- 支持模板变量替换
|
|
9
|
+
- 交互式项目配置引导
|
|
10
|
+
- 快速初始化项目目录结构
|
|
11
|
+
|
|
12
|
+
## 环境要求
|
|
13
|
+
|
|
14
|
+
- Node.js >= 14.0.0
|
|
15
|
+
- npm 或 yarn
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
### 全局安装(推荐)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g mls-cli
|
|
23
|
+
或
|
|
24
|
+
npm install -g mls-cli --registry=https://packages.aliyun.com/688f46a99eda9d4e3ee46b62/npm/npm-registry/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 本地使用
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx mls-cli <project-name>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 源码运行说明
|
|
34
|
+
|
|
35
|
+
### 1. 克隆仓库
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://codeup.aliyun.com/688f46a99eda9d4e3ee46b62/mls-cli.git
|
|
39
|
+
cd mls-cli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. 安装依赖
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. 本地运行
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 使用 node 直接运行
|
|
52
|
+
node bin/cli.js <project-name>
|
|
53
|
+
|
|
54
|
+
# 或者创建软链接进行测试
|
|
55
|
+
npm link
|
|
56
|
+
mls <project-name>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 4. 运行测试
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm test
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 使用示例
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 创建新项目
|
|
69
|
+
mls my-project https://codeup.aliyun.com/688f46a99eda9d4e3ee46b62/mls-template.git
|
|
70
|
+
|
|
71
|
+
- 上面地址为可用脚手架模板地址
|
|
72
|
+
|
|
73
|
+
# 按照提示选择模板和配置
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 项目结构
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
mls-cli/
|
|
80
|
+
├── bin/
|
|
81
|
+
│ └── cli.js # 命令行入口
|
|
82
|
+
├── lib/
|
|
83
|
+
│ ├── create.js # 项目创建逻辑
|
|
84
|
+
│ ├── download.js # 模板下载逻辑
|
|
85
|
+
│ └── replace.js # 变量替换逻辑
|
|
86
|
+
├── test/
|
|
87
|
+
│ └── test.js # 测试文件
|
|
88
|
+
├── package.json # 项目配置
|
|
89
|
+
└── README.md # 说明文档
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 依赖说明
|
|
93
|
+
|
|
94
|
+
- **commander**: 命令行参数解析
|
|
95
|
+
- **download-git-repo**: Git 仓库下载
|
|
96
|
+
- **chalk**: 命令行颜色输出
|
|
97
|
+
- **ora**: 加载动画
|
|
98
|
+
- **fs-extra**: 文件系统扩展
|
|
99
|
+
- **glob**: 文件匹配
|
|
100
|
+
|
|
101
|
+
## 开发计划
|
|
102
|
+
|
|
103
|
+
- [ ] 支持更多模板源
|
|
104
|
+
- [ ] 添加配置文件支持
|
|
105
|
+
- [ ] 优化交互体验
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
ISC
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const create = require('../lib/create');
|
|
6
|
+
const pkg = require('../package.json');
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('mls')
|
|
10
|
+
.version(pkg.version)
|
|
11
|
+
.description('A CLI scaffold tool for creating projects from remote templates');
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command('create <projectName> <url>')
|
|
15
|
+
.description('Create a new project from a remote template')
|
|
16
|
+
.option('-p, --placeholder <placeholder>', 'Custom placeholder pattern to replace', '{{PROJECT_NAME}}')
|
|
17
|
+
.action(async (projectName, url, options) => {
|
|
18
|
+
console.log(chalk.blue(`\nCreating project: ${projectName}`));
|
|
19
|
+
console.log(chalk.gray(`Template URL: ${url}\n`));
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await create(projectName, url, options);
|
|
23
|
+
console.log(chalk.green(`\nProject ${projectName} created successfully!`));
|
|
24
|
+
console.log(chalk.cyan(`\nNext steps:`));
|
|
25
|
+
console.log(chalk.white(` cd ${projectName}`));
|
|
26
|
+
console.log(chalk.white(` npm install`));
|
|
27
|
+
console.log(chalk.white(` npm start\n`));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
program.parse(process.argv);
|
|
35
|
+
|
|
36
|
+
if (!process.argv.slice(2).length) {
|
|
37
|
+
program.outputHelp();
|
|
38
|
+
}
|
package/lib/create.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const download = require('./download');
|
|
6
|
+
const replace = require('./replace');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a new project from a remote template
|
|
10
|
+
* @param {string} projectName - The name of the project to create
|
|
11
|
+
* @param {string} url - The URL of the template (git repo or zip file)
|
|
12
|
+
* @param {object} options - Options including placeholder pattern
|
|
13
|
+
*/
|
|
14
|
+
async function create(projectName, url, options = {}) {
|
|
15
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
16
|
+
const placeholder = options.placeholder || '{{PROJECT_NAME}}';
|
|
17
|
+
|
|
18
|
+
// Check if target directory already exists
|
|
19
|
+
if (fs.existsSync(targetDir)) {
|
|
20
|
+
throw new Error(`Directory "${projectName}" already exists. Please choose a different name or remove the existing directory.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Create target directory
|
|
24
|
+
fs.ensureDirSync(targetDir);
|
|
25
|
+
|
|
26
|
+
const downloadSpinner = ora('Downloading template...').start();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Download template
|
|
30
|
+
await download(url, targetDir);
|
|
31
|
+
downloadSpinner.succeed('Template downloaded successfully');
|
|
32
|
+
|
|
33
|
+
// Replace placeholders
|
|
34
|
+
const replaceSpinner = ora('Replacing placeholders...').start();
|
|
35
|
+
const replacedCount = await replace(targetDir, placeholder, projectName);
|
|
36
|
+
replaceSpinner.succeed(`Replaced ${replacedCount} occurrences in template files`);
|
|
37
|
+
|
|
38
|
+
return targetDir;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
downloadSpinner.fail('Failed to download template');
|
|
41
|
+
// Clean up on failure
|
|
42
|
+
if (fs.existsSync(targetDir)) {
|
|
43
|
+
fs.removeSync(targetDir);
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = create;
|
package/lib/download.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const { URL } = require('url');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Download template from URL
|
|
10
|
+
* Supports: git repositories, zip files, tar.gz files
|
|
11
|
+
* @param {string} url - The URL to download from
|
|
12
|
+
* @param {string} targetDir - The target directory
|
|
13
|
+
*/
|
|
14
|
+
async function download(url, targetDir) {
|
|
15
|
+
// Determine download type based on URL
|
|
16
|
+
if (isGitUrl(url)) {
|
|
17
|
+
return downloadFromGit(url, targetDir);
|
|
18
|
+
} else if (url.endsWith('.zip') || url.endsWith('.tar.gz') || url.endsWith('.tgz')) {
|
|
19
|
+
return downloadArchive(url, targetDir);
|
|
20
|
+
} else {
|
|
21
|
+
// Try git clone first, fallback to archive download
|
|
22
|
+
try {
|
|
23
|
+
return await downloadFromGit(url, targetDir);
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error('Unable to download template. Please provide a valid git URL or archive URL (.zip, .tar.gz)');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if URL is a git repository URL
|
|
32
|
+
*/
|
|
33
|
+
function isGitUrl(url) {
|
|
34
|
+
return url.endsWith('.git') ||
|
|
35
|
+
url.includes('github.com') ||
|
|
36
|
+
url.includes('gitlab.com') ||
|
|
37
|
+
url.includes('gitee.com') ||
|
|
38
|
+
url.includes('bitbucket.org');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Download from git repository
|
|
43
|
+
*/
|
|
44
|
+
async function downloadFromGit(url, targetDir) {
|
|
45
|
+
// Normalize git URL
|
|
46
|
+
let gitUrl = url;
|
|
47
|
+
|
|
48
|
+
// Handle GitHub shorthand (user/repo)
|
|
49
|
+
if (/^[\w-]+\/[\w-]+$/.test(url)) {
|
|
50
|
+
gitUrl = `https://github.com/${url}.git`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Ensure .git extension for proper cloning
|
|
54
|
+
if (!gitUrl.endsWith('.git') && !gitUrl.includes('?')) {
|
|
55
|
+
gitUrl = gitUrl + '.git';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Clone to temp directory first, then move contents
|
|
60
|
+
const tempDir = targetDir + '_temp';
|
|
61
|
+
execSync(`git clone --depth 1 "${gitUrl}" "${tempDir}"`, {
|
|
62
|
+
stdio: 'pipe'
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Remove .git folder
|
|
66
|
+
fs.removeSync(path.join(tempDir, '.git'));
|
|
67
|
+
|
|
68
|
+
// Move contents to target directory
|
|
69
|
+
const files = fs.readdirSync(tempDir);
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
fs.moveSync(path.join(tempDir, file), path.join(targetDir, file));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Clean up temp directory
|
|
75
|
+
fs.removeSync(tempDir);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`Git clone failed: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Download archive file (zip, tar.gz)
|
|
83
|
+
*/
|
|
84
|
+
async function downloadArchive(url, targetDir) {
|
|
85
|
+
const tempFile = path.join(targetDir, 'temp_archive');
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Download file
|
|
89
|
+
await downloadFile(url, tempFile);
|
|
90
|
+
|
|
91
|
+
// Extract based on file type
|
|
92
|
+
if (url.endsWith('.zip')) {
|
|
93
|
+
await extractZip(tempFile, targetDir);
|
|
94
|
+
} else {
|
|
95
|
+
await extractTarGz(tempFile, targetDir);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clean up temp file
|
|
99
|
+
fs.removeSync(tempFile);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`Archive download failed: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Download a file from URL
|
|
107
|
+
*/
|
|
108
|
+
function downloadFile(url, dest) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const parsedUrl = new URL(url);
|
|
111
|
+
const protocol = parsedUrl.protocol === 'https:' ? https : http;
|
|
112
|
+
|
|
113
|
+
const file = fs.createWriteStream(dest);
|
|
114
|
+
|
|
115
|
+
const request = protocol.get(url, (response) => {
|
|
116
|
+
// Handle redirects
|
|
117
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
118
|
+
file.close();
|
|
119
|
+
fs.unlinkSync(dest);
|
|
120
|
+
return downloadFile(response.headers.location, dest).then(resolve).catch(reject);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (response.statusCode !== 200) {
|
|
124
|
+
file.close();
|
|
125
|
+
fs.unlinkSync(dest);
|
|
126
|
+
return reject(new Error(`HTTP ${response.statusCode}`));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
response.pipe(file);
|
|
130
|
+
|
|
131
|
+
file.on('finish', () => {
|
|
132
|
+
file.close();
|
|
133
|
+
resolve();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
request.on('error', (err) => {
|
|
138
|
+
fs.unlink(dest, () => {});
|
|
139
|
+
reject(err);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
file.on('error', (err) => {
|
|
143
|
+
fs.unlink(dest, () => {});
|
|
144
|
+
reject(err);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract zip file
|
|
151
|
+
*/
|
|
152
|
+
async function extractZip(zipPath, targetDir) {
|
|
153
|
+
// Use built-in unzip on Unix or PowerShell on Windows
|
|
154
|
+
const isWindows = process.platform === 'win32';
|
|
155
|
+
|
|
156
|
+
if (isWindows) {
|
|
157
|
+
execSync(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${targetDir}' -Force"`, {
|
|
158
|
+
stdio: 'pipe'
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
execSync(`unzip -o "${zipPath}" -d "${targetDir}"`, {
|
|
162
|
+
stdio: 'pipe'
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// If extracted to a single subdirectory, move contents up
|
|
167
|
+
await flattenSingleSubdir(targetDir);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extract tar.gz file
|
|
172
|
+
*/
|
|
173
|
+
async function extractTarGz(tarPath, targetDir) {
|
|
174
|
+
execSync(`tar -xzf "${tarPath}" -C "${targetDir}"`, {
|
|
175
|
+
stdio: 'pipe'
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// If extracted to a single subdirectory, move contents up
|
|
179
|
+
await flattenSingleSubdir(targetDir);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* If directory contains only one subdirectory, move its contents up
|
|
184
|
+
*/
|
|
185
|
+
async function flattenSingleSubdir(dir) {
|
|
186
|
+
const items = fs.readdirSync(dir).filter(item => !item.startsWith('temp_'));
|
|
187
|
+
|
|
188
|
+
if (items.length === 1) {
|
|
189
|
+
const subdir = path.join(dir, items[0]);
|
|
190
|
+
if (fs.statSync(subdir).isDirectory()) {
|
|
191
|
+
const files = fs.readdirSync(subdir);
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
fs.moveSync(path.join(subdir, file), path.join(dir, file), { overwrite: true });
|
|
194
|
+
}
|
|
195
|
+
fs.removeSync(subdir);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = download;
|
package/lib/replace.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { glob } = require('glob');
|
|
4
|
+
|
|
5
|
+
// File extensions to process for placeholder replacement
|
|
6
|
+
const TEXT_EXTENSIONS = [
|
|
7
|
+
'.json', '.yaml', '.yml', '.html', '.htm', '.xml',
|
|
8
|
+
'.md', 'Dockerfile', 'Makefile', 'Rakefile'
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
// Files to always check (regardless of extension)
|
|
12
|
+
const ALWAYS_CHECK_FILES = [
|
|
13
|
+
'package.json',
|
|
14
|
+
'package-lock.json',
|
|
15
|
+
'README.md',
|
|
16
|
+
'README',
|
|
17
|
+
'LICENSE',
|
|
18
|
+
'.gitignore',
|
|
19
|
+
'.npmrc',
|
|
20
|
+
'.nvmrc',
|
|
21
|
+
'Dockerfile',
|
|
22
|
+
'docker-compose.yml',
|
|
23
|
+
'docker-compose.yaml'
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Directories to skip
|
|
27
|
+
const SKIP_DIRS = [
|
|
28
|
+
'node_modules',
|
|
29
|
+
'.git',
|
|
30
|
+
'.svn',
|
|
31
|
+
'.hg',
|
|
32
|
+
'dist',
|
|
33
|
+
'build',
|
|
34
|
+
'.next',
|
|
35
|
+
'.nuxt',
|
|
36
|
+
'__pycache__',
|
|
37
|
+
'.pytest_cache',
|
|
38
|
+
'vendor',
|
|
39
|
+
'.idea',
|
|
40
|
+
'.vscode'
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replace placeholder in all text files within a directory
|
|
45
|
+
* @param {string} dir - The directory to process
|
|
46
|
+
* @param {string} placeholder - The placeholder pattern to replace
|
|
47
|
+
* @param {string} replacement - The replacement value (project name)
|
|
48
|
+
* @returns {number} - Number of replacements made
|
|
49
|
+
*/
|
|
50
|
+
async function replace(dir, placeholder, replacement) {
|
|
51
|
+
let totalReplacements = 0;
|
|
52
|
+
|
|
53
|
+
// Get all files recursively
|
|
54
|
+
const files = await glob('**/*', {
|
|
55
|
+
cwd: dir,
|
|
56
|
+
nodir: true,
|
|
57
|
+
dot: true,
|
|
58
|
+
ignore: SKIP_DIRS.map(d => `**/${d}/**`)
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const filePath = path.join(dir, file);
|
|
63
|
+
const fileName = path.basename(file);
|
|
64
|
+
const ext = path.extname(file).toLowerCase();
|
|
65
|
+
|
|
66
|
+
// Check if file should be processed
|
|
67
|
+
const shouldProcess = TEXT_EXTENSIONS.includes(ext) ||
|
|
68
|
+
TEXT_EXTENSIONS.includes(fileName) ||
|
|
69
|
+
ALWAYS_CHECK_FILES.includes(fileName) ||
|
|
70
|
+
!ext; // Files without extension
|
|
71
|
+
|
|
72
|
+
if (shouldProcess) {
|
|
73
|
+
try {
|
|
74
|
+
const count = await replaceInFile(filePath, placeholder, replacement);
|
|
75
|
+
totalReplacements += count;
|
|
76
|
+
} catch {
|
|
77
|
+
// Skip files that can't be read (binary files, etc.)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Also handle file/directory renaming if placeholder is in the name
|
|
83
|
+
await renameWithPlaceholder(dir, placeholder, replacement);
|
|
84
|
+
|
|
85
|
+
return totalReplacements;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Replace placeholder in a single file
|
|
90
|
+
* @param {string} filePath - Path to the file
|
|
91
|
+
* @param {string} placeholder - The placeholder pattern
|
|
92
|
+
* @param {string} replacement - The replacement value
|
|
93
|
+
* @returns {number} - Number of replacements made
|
|
94
|
+
*/
|
|
95
|
+
async function replaceInFile(filePath, placeholder, replacement) {
|
|
96
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
97
|
+
|
|
98
|
+
// Check if file is binary
|
|
99
|
+
if (isBinary(content)) {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create regex for the placeholder (escape special characters)
|
|
104
|
+
const escapedPlaceholder = escapeRegExp(placeholder);
|
|
105
|
+
const regex = new RegExp(escapedPlaceholder, 'g');
|
|
106
|
+
|
|
107
|
+
// Count matches
|
|
108
|
+
const matches = content.match(regex);
|
|
109
|
+
const count = matches ? matches.length : 0;
|
|
110
|
+
|
|
111
|
+
if (count > 0) {
|
|
112
|
+
const newContent = content.replace(regex, replacement);
|
|
113
|
+
await fs.writeFile(filePath, newContent, 'utf8');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return count;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Rename files and directories containing the placeholder using recursive approach
|
|
121
|
+
* This ensures nested directories are handled correctly
|
|
122
|
+
*/
|
|
123
|
+
async function renameWithPlaceholder(dir, placeholder, replacement) {
|
|
124
|
+
await renameRecursive(dir, placeholder, replacement);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Recursively rename files and directories
|
|
129
|
+
* Process order: files first, then recurse into subdirs, finally rename current dir items
|
|
130
|
+
*/
|
|
131
|
+
async function renameRecursive(dir, placeholder, replacement) {
|
|
132
|
+
if (!fs.existsSync(dir)) return;
|
|
133
|
+
|
|
134
|
+
const items = fs.readdirSync(dir);
|
|
135
|
+
const files = [];
|
|
136
|
+
const dirs = [];
|
|
137
|
+
|
|
138
|
+
// Separate files and directories
|
|
139
|
+
for (const item of items) {
|
|
140
|
+
if (SKIP_DIRS.includes(item)) continue;
|
|
141
|
+
|
|
142
|
+
const itemPath = path.join(dir, item);
|
|
143
|
+
try {
|
|
144
|
+
if (fs.statSync(itemPath).isDirectory()) {
|
|
145
|
+
dirs.push(item);
|
|
146
|
+
} else {
|
|
147
|
+
files.push(item);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Skip items that can't be accessed
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 1. Rename files in current directory
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
if (file.includes(placeholder)) {
|
|
157
|
+
const filePath = path.join(dir, file);
|
|
158
|
+
const newName = file.replace(new RegExp(escapeRegExp(placeholder), 'g'), replacement);
|
|
159
|
+
const newPath = path.join(dir, newName);
|
|
160
|
+
await fs.move(filePath, newPath, { overwrite: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 2. Recurse into subdirectories (process children before renaming parent)
|
|
165
|
+
for (const subdir of dirs) {
|
|
166
|
+
const subdirPath = path.join(dir, subdir);
|
|
167
|
+
await renameRecursive(subdirPath, placeholder, replacement);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Rename subdirectories in current directory (after their contents are processed)
|
|
171
|
+
// Re-read directory as some items may have been renamed in recursion
|
|
172
|
+
const updatedItems = fs.readdirSync(dir);
|
|
173
|
+
for (const item of updatedItems) {
|
|
174
|
+
if (SKIP_DIRS.includes(item)) continue;
|
|
175
|
+
|
|
176
|
+
const itemPath = path.join(dir, item);
|
|
177
|
+
try {
|
|
178
|
+
if (fs.statSync(itemPath).isDirectory() && item.includes(placeholder)) {
|
|
179
|
+
const newName = item.replace(new RegExp(escapeRegExp(placeholder), 'g'), replacement);
|
|
180
|
+
const newPath = path.join(dir, newName);
|
|
181
|
+
await fs.move(itemPath, newPath, { overwrite: true });
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Skip items that can't be accessed
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if content appears to be binary
|
|
191
|
+
*/
|
|
192
|
+
function isBinary(content) {
|
|
193
|
+
// Check for null bytes which indicate binary content
|
|
194
|
+
return content.includes('\0');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Escape special regex characters in a string
|
|
199
|
+
*/
|
|
200
|
+
function escapeRegExp(string) {
|
|
201
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = replace;
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dolly-mls-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CLI scaffold tool for creating projects from remote templates",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mls": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node test/test.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"scaffold",
|
|
15
|
+
"template",
|
|
16
|
+
"generator"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"chalk": "^4.1.2",
|
|
22
|
+
"commander": "^12.0.0",
|
|
23
|
+
"download-git-repo": "^3.0.2",
|
|
24
|
+
"fs-extra": "^11.2.0",
|
|
25
|
+
"glob": "^13.0.6",
|
|
26
|
+
"ora": "^5.4.1"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=14.0.0"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin",
|
|
33
|
+
"lib"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|