@wmj-code/clone_npm 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 +24 -0
- package/ai-models.html +394 -0
- package/clear_yl_npm_i/345/272/237/345/274/203.js +106 -0
- package/clone_check_branch/345/272/237/345/274/203.js +162 -0
- package/clone_npm.js +646 -0
- package/config-editor.html +741 -0
- package/config.json +362 -0
- package/config/345/272/237/345/274/203.js +135 -0
- package/main.css +374 -0
- package/package.json +19 -0
- package/server.js +236 -0
- package/server.pid +1 -0
- package//344/275/277/347/224/250/346/226/271/346/263/225.png +0 -0
- package//345/256/211/350/243/205/344/273/245/345/217/212/350/257/264/346/230/216.txt +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# clone_npm
|
|
2
|
+
1、专属于前端开发者的快捷工具
|
|
3
|
+
2、一个自定义命令行工具,打通clone和npm install的流程
|
|
4
|
+
3、环境要求
|
|
5
|
+
- Node.js >= 14.0.0
|
|
6
|
+
4、适用场景
|
|
7
|
+
- 负责多项目,集中处理多个系统代码
|
|
8
|
+
- 快速打开项目
|
|
9
|
+
- 快速切换项目
|
|
10
|
+
- 快速重新安装项目依赖
|
|
11
|
+
等等
|
|
12
|
+
# 安装
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g clone_npm
|
|
15
|
+
```
|
|
16
|
+
# 使用
|
|
17
|
+
```bash
|
|
18
|
+
cn
|
|
19
|
+
```
|
|
20
|
+
执行后会显示使用方法以及UI界面
|
|
21
|
+
|
|
22
|
+
# 注意事项
|
|
23
|
+
- powerShell请在管理员权限下执行命令
|
|
24
|
+
- 下载npm依赖时,请确保您本地的项目未运行
|
package/ai-models.html
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>AI 模型列表</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
16
|
+
background: #f5f5f5;
|
|
17
|
+
padding: 20px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.api-info {
|
|
21
|
+
background: #fff;
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
padding: 16px 20px;
|
|
24
|
+
margin-bottom: 20px;
|
|
25
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, .1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.api-info .row {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
margin-bottom: 8px;
|
|
32
|
+
gap: 10px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.api-info .row:last-child {
|
|
36
|
+
margin-bottom: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.api-info label {
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
min-width: 80px;
|
|
42
|
+
color: #333;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.api-info input {
|
|
46
|
+
flex: 1;
|
|
47
|
+
padding: 6px 10px;
|
|
48
|
+
border: 1px solid #ddd;
|
|
49
|
+
border-radius: 4px;
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.api-info button {
|
|
54
|
+
padding: 6px 16px;
|
|
55
|
+
background: #4CAF50;
|
|
56
|
+
color: #fff;
|
|
57
|
+
border: none;
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.api-info button:hover {
|
|
63
|
+
background: #45a049;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.api-info .btn-back {
|
|
67
|
+
background: #666;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.api-info .btn-back:hover {
|
|
71
|
+
background: #555;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.api-tabs {
|
|
75
|
+
display: flex;
|
|
76
|
+
margin-bottom: 16px;
|
|
77
|
+
border-bottom: 1px solid #ddd;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.api-tab {
|
|
81
|
+
padding: 8px 16px;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
border-bottom: 2px solid transparent;
|
|
84
|
+
font-size: 14px;
|
|
85
|
+
font-weight: 500;
|
|
86
|
+
color: #666;
|
|
87
|
+
transition: all .2s;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.api-tab:hover {
|
|
91
|
+
color: #4CAF50;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.api-tab.active {
|
|
95
|
+
color: #4CAF50;
|
|
96
|
+
border-bottom-color: #4CAF50;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
.tabs {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
gap: 6px;
|
|
105
|
+
margin-bottom: 16px;
|
|
106
|
+
background: #fff;
|
|
107
|
+
padding: 12px;
|
|
108
|
+
border-radius: 8px;
|
|
109
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, .1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.tab {
|
|
113
|
+
padding: 6px 14px;
|
|
114
|
+
border: 1px solid #ddd;
|
|
115
|
+
border-radius: 20px;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font-size: 13px;
|
|
118
|
+
transition: all .2s;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.tab:hover {
|
|
122
|
+
border-color: #4CAF50;
|
|
123
|
+
color: #4CAF50;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.tab.active {
|
|
127
|
+
background: #4CAF50;
|
|
128
|
+
color: #fff;
|
|
129
|
+
border-color: #4CAF50;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.tab .count {
|
|
133
|
+
font-size: 11px;
|
|
134
|
+
opacity: .7;
|
|
135
|
+
margin-left: 4px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.models-grid {
|
|
139
|
+
display: grid;
|
|
140
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
141
|
+
gap: 10px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.model-card {
|
|
145
|
+
background: #fff;
|
|
146
|
+
border-radius: 6px;
|
|
147
|
+
padding: 12px 16px;
|
|
148
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, .08);
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
transition: all .2s;
|
|
151
|
+
border: 1px solid transparent;
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: space-between;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.model-card:hover {
|
|
158
|
+
border-color: #4CAF50;
|
|
159
|
+
box-shadow: 0 2px 8px rgba(76, 175, 80, .15);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.model-card .name {
|
|
163
|
+
font-size: 14px;
|
|
164
|
+
color: #333;
|
|
165
|
+
word-break: break-all;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.model-card .copy-icon {
|
|
169
|
+
color: #999;
|
|
170
|
+
font-size: 12px;
|
|
171
|
+
flex-shrink: 0;
|
|
172
|
+
margin-left: 8px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.model-card.copied {
|
|
176
|
+
border-color: #4CAF50;
|
|
177
|
+
background: #f0fff0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.toast {
|
|
181
|
+
position: fixed;
|
|
182
|
+
top: 20px;
|
|
183
|
+
left: 50%;
|
|
184
|
+
transform: translateX(-50%);
|
|
185
|
+
background: #333;
|
|
186
|
+
color: #fff;
|
|
187
|
+
padding: 8px 20px;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
font-size: 14px;
|
|
190
|
+
display: none;
|
|
191
|
+
z-index: 999;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.loading {
|
|
195
|
+
text-align: center;
|
|
196
|
+
padding: 40px;
|
|
197
|
+
color: #999;
|
|
198
|
+
font-size: 16px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.error {
|
|
202
|
+
text-align: center;
|
|
203
|
+
padding: 40px;
|
|
204
|
+
color: #e53935;
|
|
205
|
+
font-size: 14px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.stats {
|
|
209
|
+
color: #666;
|
|
210
|
+
font-size: 13px;
|
|
211
|
+
margin-bottom: 12px;
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
214
|
+
</head>
|
|
215
|
+
|
|
216
|
+
<body>
|
|
217
|
+
<div class="api-info">
|
|
218
|
+
<div class="api-tabs">
|
|
219
|
+
<div class="api-tab active" data-index="0">API 1</div>
|
|
220
|
+
<div class="api-tab" data-index="1">API 2</div>
|
|
221
|
+
<div class="api-tab" data-index="2">API 3</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="row">
|
|
224
|
+
<label>API URL:</label>
|
|
225
|
+
<input type="text" id="apiUrl" value="http://192.168.82.252:8318/v1" placeholder="https://api.openai.com/v1">
|
|
226
|
+
</div>
|
|
227
|
+
<div class="row">
|
|
228
|
+
<label>API Key:</label>
|
|
229
|
+
<input type="text" id="apiKey" value="sk-01-33012860-73a4-465b-be66-73c3b618eca3" placeholder="sk-xxx">
|
|
230
|
+
<button id="fetchBtn" onclick="fetchModels()">查询模型</button>
|
|
231
|
+
<button class="btn-back" onclick="window.location.href='/config-editor.html'">返回配置</button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
<div class="tabs" id="tabs"></div>
|
|
237
|
+
<div class="stats" id="stats"></div>
|
|
238
|
+
<div id="content">
|
|
239
|
+
<div class="loading">请点击「查询模型」获取可用模型列表</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="toast" id="toast"></div>
|
|
242
|
+
|
|
243
|
+
<script>
|
|
244
|
+
// API 配置数据
|
|
245
|
+
const apiConfigs = [
|
|
246
|
+
{
|
|
247
|
+
url: 'http://192.168.82.252:8318/v1',
|
|
248
|
+
key: 'sk-01-33012860-73a4-465b-be66-73c3b618eca3'
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
url: 'http://192.168.82.252:8319/v1',
|
|
252
|
+
key: 'sk-02-21c522d2-4dce-4af2-aa24-11f79d8c4a2f'
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
url: 'http://192.168.82.252:8320/v1',
|
|
256
|
+
key: 'sk-03-5b4bc65b-69c6-42c6-9395-daf862cf5584'
|
|
257
|
+
}
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
let allModels = [];
|
|
261
|
+
let groupedModels = {};
|
|
262
|
+
let currentTab = '全部';
|
|
263
|
+
|
|
264
|
+
// 初始化 API 标签页
|
|
265
|
+
function initApiTabs() {
|
|
266
|
+
const apiTabs = document.querySelectorAll('.api-tab');
|
|
267
|
+
apiTabs.forEach(tab => {
|
|
268
|
+
tab.addEventListener('click', () => {
|
|
269
|
+
const index = parseInt(tab.dataset.index);
|
|
270
|
+
// 更新标签页状态
|
|
271
|
+
apiTabs.forEach(t => t.classList.remove('active'));
|
|
272
|
+
tab.classList.add('active');
|
|
273
|
+
// 更新 API URL 和 Key
|
|
274
|
+
document.getElementById('apiUrl').value = apiConfigs[index].url;
|
|
275
|
+
document.getElementById('apiKey').value = apiConfigs[index].key;
|
|
276
|
+
// 自动查询模型
|
|
277
|
+
fetchModels();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 页面加载完成后初始化
|
|
283
|
+
window.onload = function() {
|
|
284
|
+
initApiTabs();
|
|
285
|
+
fetchModels();
|
|
286
|
+
};
|
|
287
|
+
function showToast(text) {
|
|
288
|
+
const t = document.getElementById('toast');
|
|
289
|
+
t.textContent = text;
|
|
290
|
+
t.style.display = 'block';
|
|
291
|
+
setTimeout(() => t.style.display = 'none', 1500);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function copyText(text, cardEl) {
|
|
295
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
296
|
+
showToast(`已复制: ${text}`);
|
|
297
|
+
cardEl.classList.add('copied');
|
|
298
|
+
setTimeout(() => cardEl.classList.remove('copied'), 1000);
|
|
299
|
+
}).catch(() => {
|
|
300
|
+
const ta = document.createElement('textarea');
|
|
301
|
+
ta.value = text; document.body.appendChild(ta);
|
|
302
|
+
ta.select(); document.execCommand('copy');
|
|
303
|
+
document.body.removeChild(ta);
|
|
304
|
+
showToast(`已复制: ${text}`);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function getGroup(modelId) {
|
|
309
|
+
const parts = modelId.split('-');
|
|
310
|
+
return parts[0] || '其他';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function groupModels(models) {
|
|
314
|
+
const groups = {};
|
|
315
|
+
models.forEach(id => {
|
|
316
|
+
const g = getGroup(id);
|
|
317
|
+
if (!groups[g]) groups[g] = [];
|
|
318
|
+
groups[g].push(id);
|
|
319
|
+
});
|
|
320
|
+
// 每组内排序
|
|
321
|
+
Object.keys(groups).forEach(k => groups[k].sort());
|
|
322
|
+
return groups;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function renderTabs() {
|
|
326
|
+
const tabsEl = document.getElementById('tabs');
|
|
327
|
+
const keys = Object.keys(groupedModels).sort();
|
|
328
|
+
let html = `<div class="tab ${currentTab === '全部' ? 'active' : ''}" onclick="switchTab('全部')">全部<span class="count">(${allModels.length})</span></div>`;
|
|
329
|
+
keys.forEach(k => {
|
|
330
|
+
const count = groupedModels[k].length;
|
|
331
|
+
html += `<div class="tab ${currentTab === k ? 'active' : ''}" onclick="switchTab('${k}')">${k}<span class="count">(${count})</span></div>`;
|
|
332
|
+
});
|
|
333
|
+
tabsEl.innerHTML = html;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function renderModels() {
|
|
337
|
+
const contentEl = document.getElementById('content');
|
|
338
|
+
const statsEl = document.getElementById('stats');
|
|
339
|
+
let models;
|
|
340
|
+
if (currentTab === '全部') {
|
|
341
|
+
models = [...allModels].sort();
|
|
342
|
+
} else {
|
|
343
|
+
models = groupedModels[currentTab] || [];
|
|
344
|
+
}
|
|
345
|
+
statsEl.textContent = `当前显示 ${models.length} 个模型`;
|
|
346
|
+
if (models.length === 0) {
|
|
347
|
+
contentEl.innerHTML = '<div class="loading">该分组下没有模型</div>';
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
contentEl.innerHTML = '<div class="models-grid">' + models.map(id =>
|
|
351
|
+
`<div class="model-card" onclick="copyText('${id}', this)">
|
|
352
|
+
<span class="name">${id}</span>
|
|
353
|
+
<span class="copy-icon">📋</span>
|
|
354
|
+
</div>`
|
|
355
|
+
).join('') + '</div>';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function switchTab(tab) {
|
|
359
|
+
currentTab = tab;
|
|
360
|
+
renderTabs();
|
|
361
|
+
renderModels();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function fetchModels() {
|
|
365
|
+
const apiUrl = document.getElementById('apiUrl').value.trim();
|
|
366
|
+
const apiKey = document.getElementById('apiKey').value.trim();
|
|
367
|
+
if (!apiUrl || !apiKey) { showToast('请填写 API URL 和 Key'); return; }
|
|
368
|
+
|
|
369
|
+
const contentEl = document.getElementById('content');
|
|
370
|
+
contentEl.innerHTML = '<div class="loading">正在查询模型列表...</div>';
|
|
371
|
+
document.getElementById('tabs').innerHTML = '';
|
|
372
|
+
document.getElementById('stats').textContent = '';
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const res = await fetch(`${apiUrl}/models`, {
|
|
376
|
+
method: 'GET',
|
|
377
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
378
|
+
});
|
|
379
|
+
const json = await res.json();
|
|
380
|
+
if (json.error) { throw new Error(json.error.message); }
|
|
381
|
+
|
|
382
|
+
allModels = json.data.map(item => item.id);
|
|
383
|
+
groupedModels = groupModels(allModels);
|
|
384
|
+
currentTab = '全部';
|
|
385
|
+
renderTabs();
|
|
386
|
+
renderModels();
|
|
387
|
+
} catch (e) {
|
|
388
|
+
contentEl.innerHTML = `<div class="error">查询失败: ${e.message}</div>`;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
</script>
|
|
392
|
+
</body>
|
|
393
|
+
|
|
394
|
+
</html>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// ===================== 脚本说明 =====================
|
|
6
|
+
// 本脚本用于批量清理多个Node.js项目的依赖文件(node_modules和package-lock.json),
|
|
7
|
+
// 并重新安装依赖。适用于需要统一更新依赖或解决依赖冲突的场景。
|
|
8
|
+
// 使用前请确保已安装Node.js和npm,并根据实际项目路径修改配置项。
|
|
9
|
+
// =====================================================
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 休眠函数(可选,用于日志输出更清晰,可删除)
|
|
14
|
+
* @param {number} ms 休眠毫秒数
|
|
15
|
+
*/
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 删除文件/目录
|
|
22
|
+
* @param {string} targetPath 要删除的文件/目录路径
|
|
23
|
+
*/
|
|
24
|
+
function deleteTarget(targetPath) {
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(targetPath)) {
|
|
27
|
+
// 判断是目录还是文件
|
|
28
|
+
const stats = fs.statSync(targetPath);
|
|
29
|
+
if (stats.isDirectory()) {
|
|
30
|
+
// 删除目录(递归删除所有内容)
|
|
31
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
32
|
+
console.log(`✅ 成功删除目录:${targetPath}`);
|
|
33
|
+
} else {
|
|
34
|
+
// 删除文件
|
|
35
|
+
fs.unlinkSync(targetPath);
|
|
36
|
+
console.log(`✅ 成功删除文件:${targetPath}`);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
console.log(`ℹ️ 目标不存在,无需删除:${targetPath}`);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`❌ 删除失败:${targetPath},错误信息:${error.message}`);
|
|
43
|
+
// 若删除失败,抛出错误终止当前项目处理,避免影响后续项目
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 处理单个项目的完整流程
|
|
50
|
+
* @param {string} projectPath 项目路径
|
|
51
|
+
*/
|
|
52
|
+
async function handleSingleProject(projectPath) {
|
|
53
|
+
try {
|
|
54
|
+
// 1. 格式化项目路径(解决不同系统路径分隔符问题)
|
|
55
|
+
const normalizedProjectPath = path.normalize(projectPath);
|
|
56
|
+
console.log(`\n=================================================`);
|
|
57
|
+
console.log(`开始处理项目:${normalizedProjectPath}`);
|
|
58
|
+
console.log(`=================================================`);
|
|
59
|
+
|
|
60
|
+
// 2. 定义要删除的目标文件/目录路径
|
|
61
|
+
const nodeModulesPath = path.join(normalizedProjectPath, 'node_modules');
|
|
62
|
+
const packageLockPath = path.join(normalizedProjectPath, 'package-lock.json');
|
|
63
|
+
|
|
64
|
+
// 3. 顺序删除 node_modules 和 package-lock.json
|
|
65
|
+
console.log(`\n--- 开始删除依赖文件/目录 ---`);
|
|
66
|
+
deleteTarget(nodeModulesPath); // 先删node_modules
|
|
67
|
+
deleteTarget(packageLockPath); // 再删package-lock.json
|
|
68
|
+
|
|
69
|
+
// 4. 清除npm依赖缓存(--force 强制清除)
|
|
70
|
+
console.log(`\n--- 开始清除npm依赖缓存 ---`);
|
|
71
|
+
execSync('npm cache clean --force', {
|
|
72
|
+
cwd: normalizedProjectPath, // 执行目录:当前项目根目录
|
|
73
|
+
stdio: 'inherit' // 输出日志到控制台
|
|
74
|
+
});
|
|
75
|
+
console.log(`✅ npm依赖缓存清除成功`);
|
|
76
|
+
|
|
77
|
+
// 5. 执行npm install安装依赖
|
|
78
|
+
console.log(`\n--- 开始执行npm install ---`);
|
|
79
|
+
execSync('npm i', {
|
|
80
|
+
cwd: normalizedProjectPath,
|
|
81
|
+
stdio: 'inherit'
|
|
82
|
+
});
|
|
83
|
+
console.log(`✅ 项目 ${normalizedProjectPath} 依赖安装完成`);
|
|
84
|
+
|
|
85
|
+
// 可选:休眠1秒,让日志输出更清晰(可根据需要调整或删除)
|
|
86
|
+
await sleep(1000);
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`\n❌ 项目 ${projectPath} 处理失败,跳过该项目继续处理下一个`, error.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 批量处理所有项目(按顺序执行)
|
|
95
|
+
*/
|
|
96
|
+
async function batchHandleProjects(PROJECT) {
|
|
97
|
+
console.log(`🚀 开始批量处理 ${PROJECT.length} 个项目...`);
|
|
98
|
+
for (const project of PROJECT) {
|
|
99
|
+
const projectPath = path.join(project.savePath, project.codePath);
|
|
100
|
+
await handleSingleProject(projectPath); // 按顺序处理,上一个项目完成后再处理下一个
|
|
101
|
+
}
|
|
102
|
+
console.log(`\n🎉 所有项目处理完毕!`);
|
|
103
|
+
}
|
|
104
|
+
module.exports = {
|
|
105
|
+
batchHandleProjects
|
|
106
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// ==================== 脚本说明 =====================
|
|
6
|
+
// 本脚本用于批量克隆GitLab上的多个项目,并切换到指定分支。
|
|
7
|
+
// 支持为每个项目单独指定存放路径,或使用默认路径(脚本所在目录)。
|
|
8
|
+
// 使用前请确保已安装Git,并根据实际需求修改配置项。
|
|
9
|
+
// =====================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 批量克隆GitLab项目并切换指定分支(支持指定存放路径)
|
|
13
|
+
* @param {Array} projectConfigList 项目配置数组
|
|
14
|
+
* 格式:[{ gitUrl: 'GitLab项目地址', branch: '目标分支名', savePath?: '自定义存放路径' }, ...]
|
|
15
|
+
*/
|
|
16
|
+
async function batchCloneAndSwitchBranch(projectConfigList) {
|
|
17
|
+
if (!Array.isArray(projectConfigList) || projectConfigList.length === 0) {
|
|
18
|
+
console.error('❌ 错误:请传入有效的项目配置数组(包含gitUrl和branch)');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`🚀 开始批量处理 ${projectConfigList.length} 个GitLab项目...\n`);
|
|
23
|
+
|
|
24
|
+
// 遍历每个项目,按顺序执行克隆和切换分支
|
|
25
|
+
for (let index = 0; index < projectConfigList.length; index++) {
|
|
26
|
+
const { gitUrl, branch, savePath } = projectConfigList[index];
|
|
27
|
+
const projectIndex = index + 1;
|
|
28
|
+
|
|
29
|
+
// 校验核心配置项是否完整
|
|
30
|
+
if (!gitUrl || !branch) {
|
|
31
|
+
console.error(`❌ 第 ${projectIndex} 个项目配置不完整,缺少gitUrl或branch,跳过该项目`);
|
|
32
|
+
console.log(`----------------------------------------------------\n`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
console.log(`====================================================`);
|
|
38
|
+
console.log(`正在处理第 ${projectIndex} 个项目`);
|
|
39
|
+
console.log(`Git地址:${gitUrl}`);
|
|
40
|
+
console.log(`目标分支:${branch}`);
|
|
41
|
+
console.log(`指定存放路径:${savePath || '默认(脚本所在目录)'}`);
|
|
42
|
+
console.log(`====================================================`);
|
|
43
|
+
|
|
44
|
+
// 1. 提取项目名称(从git地址中解析,支持.git后缀和无后缀)
|
|
45
|
+
const projectName = getProjectNameFromGitUrl(gitUrl);
|
|
46
|
+
console.log(`\n🔍 解析出项目名称:${projectName}`);
|
|
47
|
+
|
|
48
|
+
// 2. 确定最终的项目存放目录(优先使用单个项目自定义路径,无则使用默认路径)
|
|
49
|
+
let finalProjectDir;
|
|
50
|
+
if (savePath) {
|
|
51
|
+
// 格式化自定义存放路径(兼容Windows/Mac/Linux路径分隔符)
|
|
52
|
+
const normalizedSavePath = path.normalize(savePath);
|
|
53
|
+
// 确保自定义存放目录存在,不存在则自动创建
|
|
54
|
+
if (!fs.existsSync(normalizedSavePath)) {
|
|
55
|
+
fs.mkdirSync(normalizedSavePath, { recursive: true }); // recursive: true 支持创建多级目录
|
|
56
|
+
console.log(`ℹ️ 自定义存放目录不存在,已自动创建:${normalizedSavePath}`);
|
|
57
|
+
}
|
|
58
|
+
// 拼接「自定义存放路径 + 项目名称」作为最终项目目录
|
|
59
|
+
finalProjectDir = path.join(normalizedSavePath, projectName);
|
|
60
|
+
} else {
|
|
61
|
+
// 默认路径:脚本所在目录 + 项目名称
|
|
62
|
+
finalProjectDir = path.resolve(process.cwd(), projectName);
|
|
63
|
+
}
|
|
64
|
+
console.log(`\n📂 项目最终存放目录:${finalProjectDir}`);
|
|
65
|
+
|
|
66
|
+
// 3. 检查项目是否已存在,避免重复克隆
|
|
67
|
+
if (fs.existsSync(finalProjectDir)) {
|
|
68
|
+
console.log(`ℹ️ 项目目录 ${projectName} 已存在,跳过克隆,直接切换分支`);
|
|
69
|
+
// 切换到项目目录,执行分支切换
|
|
70
|
+
switchBranch(finalProjectDir, branch);
|
|
71
|
+
} else {
|
|
72
|
+
// 4. 克隆GitLab项目到指定目录
|
|
73
|
+
console.log(`\n📥 开始克隆项目到指定目录...`);
|
|
74
|
+
// git clone 语法:git clone <git地址> <目标存放路径/项目名>
|
|
75
|
+
execSync(`git clone ${gitUrl} "${finalProjectDir}"`, {
|
|
76
|
+
stdio: 'inherit', // 实时输出克隆日志到控制台
|
|
77
|
+
encoding: 'utf-8'
|
|
78
|
+
});
|
|
79
|
+
console.log(`✅ 项目 ${projectName} 克隆成功`);
|
|
80
|
+
|
|
81
|
+
// 5. 切换到指定分支
|
|
82
|
+
switchBranch(finalProjectDir, branch);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`\n✅ 第 ${projectIndex} 个项目处理完成!`);
|
|
86
|
+
console.log(`----------------------------------------------------\n`);
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`\n❌ 第 ${projectIndex} 个项目处理失败:${error.message}`);
|
|
90
|
+
console.log(`----------------------------------------------------\n`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`🎉 所有项目批量处理完毕!`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 从Git地址中提取项目名称
|
|
100
|
+
* @param {string} gitUrl GitLab项目地址(支持http/https/ssh格式)
|
|
101
|
+
* @returns {string} 项目名称
|
|
102
|
+
*/
|
|
103
|
+
function getProjectNameFromGitUrl(gitUrl) {
|
|
104
|
+
// 处理规则:截取最后一个斜杠后的内容,去除.git后缀(如果有)
|
|
105
|
+
const lastSlashIndex = gitUrl.lastIndexOf('/');
|
|
106
|
+
let projectName = gitUrl.slice(lastSlashIndex + 1);
|
|
107
|
+
// 去除.git后缀
|
|
108
|
+
if (projectName.endsWith('.git')) {
|
|
109
|
+
projectName = projectName.slice(0, -4);
|
|
110
|
+
}
|
|
111
|
+
return projectName;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 切换项目到指定分支
|
|
116
|
+
* @param {string} projectDir 项目目录路径
|
|
117
|
+
* @param {string} targetBranch 目标分支名
|
|
118
|
+
*/
|
|
119
|
+
function switchBranch(projectDir, targetBranch) {
|
|
120
|
+
console.log(`\n🔀 开始切换到 ${targetBranch} 分支...`);
|
|
121
|
+
|
|
122
|
+
// 1. 进入项目目录
|
|
123
|
+
process.chdir(projectDir);
|
|
124
|
+
|
|
125
|
+
// 2. 拉取远程所有分支信息(确保本地有目标分支的缓存)
|
|
126
|
+
console.log(`ℹ️ 拉取远程分支最新信息...`);
|
|
127
|
+
execSync(`git fetch origin`, {
|
|
128
|
+
stdio: 'inherit',
|
|
129
|
+
encoding: 'utf-8'
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 3. 尝试切换分支(优先使用git switch,兼容git checkout)
|
|
133
|
+
try {
|
|
134
|
+
// 先检查本地是否存在目标分支
|
|
135
|
+
const localBranches = execSync(`git branch`, { encoding: 'utf-8' });
|
|
136
|
+
if (localBranches.includes(targetBranch)) {
|
|
137
|
+
// 本地已存在分支,直接切换
|
|
138
|
+
execSync(`git switch ${targetBranch}`, { stdio: 'inherit' });
|
|
139
|
+
} else {
|
|
140
|
+
// 本地不存在,拉取远程分支并切换(一步到位)
|
|
141
|
+
execSync(`git switch -c ${targetBranch} origin/${targetBranch}`, { stdio: 'inherit' });
|
|
142
|
+
}
|
|
143
|
+
console.log(`✅ 成功切换到 ${targetBranch} 分支`);
|
|
144
|
+
} catch (switchError) {
|
|
145
|
+
// 兼容旧版本Git(无git switch命令),使用git checkout
|
|
146
|
+
console.log(`ℹ️ git switch命令不支持,使用git checkout兼容...`);
|
|
147
|
+
try {
|
|
148
|
+
const localBranches = execSync(`git branch`, { encoding: 'utf-8' });
|
|
149
|
+
if (localBranches.includes(targetBranch)) {
|
|
150
|
+
execSync(`git checkout ${targetBranch}`, { stdio: 'inherit' });
|
|
151
|
+
} else {
|
|
152
|
+
execSync(`git checkout -b ${targetBranch} origin/${targetBranch}`, { stdio: 'inherit' });
|
|
153
|
+
}
|
|
154
|
+
console.log(`✅ 成功切换到 ${targetBranch} 分支`);
|
|
155
|
+
} catch (checkoutError) {
|
|
156
|
+
throw new Error(`切换分支失败:${checkoutError.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
module.exports = {
|
|
161
|
+
batchCloneAndSwitchBranch
|
|
162
|
+
}
|