axhub-make 1.0.2 → 1.0.4
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/bin/index.js +329 -94
- package/package.json +5 -2
package/bin/index.js
CHANGED
|
@@ -2,148 +2,383 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
5
6
|
const { execFileSync, execSync } = require('child_process');
|
|
6
|
-
const inquirer = require('inquirer');
|
|
7
7
|
const chalk = require('chalk');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
function normalizeRelPath(p) {
|
|
10
|
+
return p.split(path.sep).join('/');
|
|
11
|
+
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const args = process.argv.slice(2);
|
|
16
|
-
let templateGit = '';
|
|
13
|
+
function escapeRegExp(s) {
|
|
14
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
}
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
function globToRegExp(pattern) {
|
|
18
|
+
const normalized = pattern.split(path.sep).join('/');
|
|
19
|
+
let re = '^';
|
|
20
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
21
|
+
const ch = normalized[i];
|
|
22
|
+
const next = normalized[i + 1];
|
|
23
|
+
if (ch === '*' && next === '*') {
|
|
24
|
+
re += '.*';
|
|
25
|
+
i++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (ch === '*') {
|
|
29
|
+
re += '[^/]*';
|
|
30
|
+
continue;
|
|
22
31
|
}
|
|
32
|
+
if (ch === '?') {
|
|
33
|
+
re += '[^/]';
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
re += escapeRegExp(ch);
|
|
23
37
|
}
|
|
38
|
+
re += '$';
|
|
39
|
+
return new RegExp(re);
|
|
40
|
+
}
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
function matchesAny(relPath, patterns) {
|
|
43
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
44
|
+
const p = relPath.startsWith('./') ? relPath.slice(2) : relPath;
|
|
45
|
+
return patterns.some((pattern) => globToRegExp(pattern).test(p));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isValidConflictMode(v) {
|
|
49
|
+
return v === 'keep' || v === 'overwrite';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readUpdateRules(tmpDir) {
|
|
53
|
+
const rulesPath = path.join(tmpDir, 'scaffold.update.json');
|
|
54
|
+
if (!(await fs.pathExists(rulesPath))) {
|
|
55
|
+
return {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
neverOverwrite: [],
|
|
58
|
+
conflictCheck: [],
|
|
59
|
+
defaultOverwrite: true
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const rules = await fs.readJson(rulesPath);
|
|
63
|
+
return {
|
|
64
|
+
schemaVersion: typeof rules.schemaVersion === 'number' ? rules.schemaVersion : 1,
|
|
65
|
+
neverOverwrite: Array.isArray(rules.neverOverwrite) ? rules.neverOverwrite : [],
|
|
66
|
+
conflictCheck: Array.isArray(rules.conflictCheck) ? rules.conflictCheck : [],
|
|
67
|
+
defaultOverwrite: typeof rules.defaultOverwrite === 'boolean' ? rules.defaultOverwrite : true
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function isAxhubMakeProject(dir) {
|
|
72
|
+
const checks = [
|
|
73
|
+
path.join(dir, 'vite.config.ts'),
|
|
74
|
+
path.join(dir, 'entries.json'),
|
|
75
|
+
path.join(dir, 'src', 'common', 'axhub-types.ts')
|
|
35
76
|
];
|
|
77
|
+
const results = await Promise.all(checks.map((p) => fs.pathExists(p)));
|
|
78
|
+
return results.every(Boolean);
|
|
79
|
+
}
|
|
36
80
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
});
|
|
81
|
+
async function listFilesRecursive(rootDir) {
|
|
82
|
+
const files = [];
|
|
83
|
+
const stack = [rootDir];
|
|
84
|
+
|
|
85
|
+
while (stack.length > 0) {
|
|
86
|
+
const current = stack.pop();
|
|
87
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
90
|
+
const fullPath = path.join(current, entry.name);
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
stack.push(fullPath);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (entry.isFile()) {
|
|
96
|
+
files.push(fullPath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
44
99
|
}
|
|
45
100
|
|
|
46
|
-
|
|
101
|
+
return files;
|
|
102
|
+
}
|
|
47
103
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
104
|
+
async function filesEqual(a, b) {
|
|
105
|
+
try {
|
|
106
|
+
const [abuf, bbuf] = await Promise.all([fs.readFile(a), fs.readFile(b)]);
|
|
107
|
+
if (abuf.length !== bbuf.length) return false;
|
|
108
|
+
return abuf.equals(bbuf);
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
51
113
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
114
|
+
async function planUpdateFromTemplate(tmpDir, targetDir, rules, conflictMode) {
|
|
115
|
+
const templateFiles = await listFilesRecursive(tmpDir);
|
|
116
|
+
const copied = [];
|
|
117
|
+
const skipped = [];
|
|
118
|
+
const conflicts = [];
|
|
119
|
+
const wouldOverwriteConflicts = [];
|
|
120
|
+
|
|
121
|
+
for (const srcPath of templateFiles) {
|
|
122
|
+
const relPath = path.relative(tmpDir, srcPath);
|
|
123
|
+
if (!relPath || relPath.startsWith('..')) continue;
|
|
124
|
+
const relPosix = normalizeRelPath(relPath);
|
|
125
|
+
|
|
126
|
+
const destPath = path.join(targetDir, relPath);
|
|
127
|
+
const destExists = await fs.pathExists(destPath);
|
|
128
|
+
|
|
129
|
+
if (matchesAny(relPosix, rules.conflictCheck) && destExists) {
|
|
130
|
+
const same = await filesEqual(destPath, srcPath);
|
|
131
|
+
if (!same) {
|
|
132
|
+
conflicts.push(relPosix);
|
|
133
|
+
if (conflictMode === 'overwrite') {
|
|
134
|
+
wouldOverwriteConflicts.push(relPosix);
|
|
135
|
+
copied.push(relPosix);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (matchesAny(relPosix, rules.neverOverwrite) && destExists) {
|
|
142
|
+
skipped.push(relPosix);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (destExists && rules.defaultOverwrite === false) {
|
|
147
|
+
skipped.push(relPosix);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
copied.push(relPosix);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { copied, skipped, conflicts, wouldOverwriteConflicts };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function applyUpdateFromTemplate(tmpDir, targetDir, rules, conflictMode) {
|
|
158
|
+
const templateFiles = await listFilesRecursive(tmpDir);
|
|
159
|
+
const copied = [];
|
|
160
|
+
const skipped = [];
|
|
161
|
+
const conflicts = [];
|
|
162
|
+
const overwrittenConflicts = [];
|
|
163
|
+
|
|
164
|
+
for (const srcPath of templateFiles) {
|
|
165
|
+
const relPath = path.relative(tmpDir, srcPath);
|
|
166
|
+
if (!relPath || relPath.startsWith('..')) continue;
|
|
167
|
+
const relPosix = normalizeRelPath(relPath);
|
|
168
|
+
|
|
169
|
+
const destPath = path.join(targetDir, relPath);
|
|
170
|
+
const destExists = await fs.pathExists(destPath);
|
|
171
|
+
|
|
172
|
+
if (matchesAny(relPosix, rules.conflictCheck) && destExists) {
|
|
173
|
+
const same = await filesEqual(destPath, srcPath);
|
|
174
|
+
if (!same) {
|
|
175
|
+
conflicts.push(relPosix);
|
|
176
|
+
if (conflictMode !== 'overwrite') continue;
|
|
177
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
178
|
+
await fs.copyFile(srcPath, destPath);
|
|
179
|
+
overwrittenConflicts.push(relPosix);
|
|
180
|
+
copied.push(relPosix);
|
|
181
|
+
continue;
|
|
62
182
|
}
|
|
63
|
-
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (matchesAny(relPosix, rules.neverOverwrite) && destExists) {
|
|
187
|
+
skipped.push(relPosix);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
64
190
|
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
191
|
+
if (destExists && rules.defaultOverwrite === false) {
|
|
192
|
+
skipped.push(relPosix);
|
|
193
|
+
continue;
|
|
68
194
|
}
|
|
69
195
|
|
|
70
|
-
await fs.
|
|
196
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
197
|
+
await fs.copyFile(srcPath, destPath);
|
|
198
|
+
copied.push(relPosix);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { copied, skipped, conflicts, overwrittenConflicts };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// -----------------------------
|
|
205
|
+
// 参数解析(无依赖,稳定)
|
|
206
|
+
// -----------------------------
|
|
207
|
+
function parseArgs(argv) {
|
|
208
|
+
const args = argv.slice(2);
|
|
209
|
+
const opts = {
|
|
210
|
+
pre: false,
|
|
211
|
+
dir: '.',
|
|
212
|
+
template: 'https://github.com/lintendo/Axhub-Make.git',
|
|
213
|
+
install: true,
|
|
214
|
+
start: true,
|
|
215
|
+
force: false,
|
|
216
|
+
pm: null,
|
|
217
|
+
conflict: 'keep'
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < args.length; i++) {
|
|
221
|
+
const a = args[i];
|
|
222
|
+
|
|
223
|
+
if ((a === 'pre' || a === 'preinstall' || a === 'preupdate') && opts.dir === '.' && !opts.pre) {
|
|
224
|
+
opts.pre = true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!a.startsWith('-') && opts.dir === '.') {
|
|
229
|
+
opts.dir = a;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (a === '-t' || a === '--template') {
|
|
234
|
+
opts.template = args[++i];
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (a === '--no-install') opts.install = false;
|
|
239
|
+
if (a === '--no-start') opts.start = false;
|
|
240
|
+
if (a === '--pre') opts.pre = true;
|
|
241
|
+
if (a === '--force') opts.force = true;
|
|
242
|
+
if (a === '--pm') opts.pm = args[++i];
|
|
243
|
+
if (a === '--conflict') {
|
|
244
|
+
const v = args[++i];
|
|
245
|
+
if (isValidConflictMode(v)) opts.conflict = v;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return opts;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function run() {
|
|
253
|
+
console.log(chalk.magenta.bold('\n🚀 Axhub Make\n'));
|
|
254
|
+
|
|
255
|
+
const opts = parseArgs(process.argv);
|
|
256
|
+
|
|
257
|
+
const isCurrentDir =
|
|
258
|
+
opts.dir === '.' || opts.dir === './' || opts.dir === '';
|
|
259
|
+
|
|
260
|
+
const targetDir = isCurrentDir
|
|
261
|
+
? process.cwd()
|
|
262
|
+
: path.resolve(process.cwd(), opts.dir);
|
|
263
|
+
|
|
264
|
+
const projectName = path.basename(targetDir);
|
|
265
|
+
|
|
266
|
+
// -----------------------------
|
|
267
|
+
// 安装/更新模式识别
|
|
268
|
+
// -----------------------------
|
|
269
|
+
const targetExists = fs.existsSync(targetDir);
|
|
270
|
+
const isUpdate = targetExists ? await isAxhubMakeProject(targetDir) : false;
|
|
271
|
+
|
|
272
|
+
if (!isUpdate) {
|
|
273
|
+
if (targetExists) {
|
|
274
|
+
const files = await fs.readdir(targetDir);
|
|
275
|
+
if (files.length > 0) {
|
|
276
|
+
console.log(
|
|
277
|
+
chalk.red(
|
|
278
|
+
'❌ 当前目录非空,且不是 Axhub Make 项目目录,无法安装。\n' +
|
|
279
|
+
'请在空目录中运行命令,或在已有 Axhub Make 项目目录中运行以执行更新。'
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
if (!opts.pre) await fs.ensureDir(targetDir);
|
|
286
|
+
}
|
|
71
287
|
}
|
|
72
288
|
|
|
73
289
|
// -----------------------------
|
|
74
|
-
//
|
|
290
|
+
// 模板下载(统一下载到临时目录)
|
|
75
291
|
// -----------------------------
|
|
76
|
-
console.log(chalk.blue(
|
|
292
|
+
console.log(chalk.blue(`\n📡 下载模板(${isUpdate ? '更新' : '安装'})...\n`));
|
|
293
|
+
const tmpDir = path.join(os.tmpdir(), `axhub-make-${Date.now()}`);
|
|
77
294
|
|
|
78
295
|
execFileSync(
|
|
79
296
|
'git',
|
|
80
|
-
['clone', '--depth', '1',
|
|
297
|
+
['clone', '--depth', '1', opts.template, tmpDir],
|
|
81
298
|
{ stdio: 'inherit' }
|
|
82
299
|
);
|
|
83
300
|
|
|
84
|
-
|
|
85
|
-
await fs.remove(path.join(targetDir, '.git'));
|
|
301
|
+
await fs.remove(path.join(tmpDir, '.git'));
|
|
86
302
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
303
|
+
const rules = await readUpdateRules(tmpDir);
|
|
304
|
+
const conflictMode = isValidConflictMode(opts.conflict) ? opts.conflict : 'keep';
|
|
305
|
+
|
|
306
|
+
if (opts.pre) {
|
|
307
|
+
const plan = await planUpdateFromTemplate(tmpDir, targetDir, rules, conflictMode);
|
|
308
|
+
await fs.remove(tmpDir);
|
|
309
|
+
console.log(JSON.stringify({
|
|
310
|
+
mode: isUpdate ? 'update' : 'install',
|
|
311
|
+
conflictMode,
|
|
312
|
+
conflicts: plan.conflicts
|
|
313
|
+
}, null, 2));
|
|
314
|
+
return;
|
|
95
315
|
}
|
|
96
316
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// -----------------------------
|
|
100
|
-
const pkgManager = getPackageManager();
|
|
317
|
+
const result = await applyUpdateFromTemplate(tmpDir, targetDir, rules, conflictMode);
|
|
318
|
+
await fs.remove(tmpDir);
|
|
101
319
|
|
|
102
|
-
|
|
103
|
-
chalk.
|
|
104
|
-
|
|
320
|
+
if (result.conflicts.length > 0 && conflictMode !== 'overwrite') {
|
|
321
|
+
console.log(chalk.yellow('\n⚠️ 发现冲突文件(已保留本地文件):'));
|
|
322
|
+
result.conflicts.forEach((p) => console.log(chalk.yellow(`- ${p}`)));
|
|
323
|
+
}
|
|
324
|
+
if (result.overwrittenConflicts.length > 0 && conflictMode === 'overwrite') {
|
|
325
|
+
console.log(chalk.yellow('\n⚠️ 发现冲突文件(已按配置覆盖):'));
|
|
326
|
+
result.overwrittenConflicts.forEach((p) => console.log(chalk.yellow(`- ${p}`)));
|
|
327
|
+
}
|
|
328
|
+
if (result.skipped.length > 0) {
|
|
329
|
+
console.log(chalk.gray('\n⏭️ 跳过覆盖(已保留本地文件):'));
|
|
330
|
+
result.skipped.forEach((p) => console.log(chalk.gray(`- ${p}`)));
|
|
331
|
+
}
|
|
332
|
+
console.log(chalk.green(`\n✅ 已写入/更新文件:${result.copied.length} 个`));
|
|
105
333
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
334
|
+
// -----------------------------
|
|
335
|
+
// package.json 处理
|
|
336
|
+
// -----------------------------
|
|
337
|
+
if (!isUpdate) {
|
|
338
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
339
|
+
if (fs.existsSync(pkgPath)) {
|
|
340
|
+
const pkg = await fs.readJson(pkgPath);
|
|
341
|
+
pkg.name = projectName;
|
|
342
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
110
345
|
|
|
111
346
|
// -----------------------------
|
|
112
|
-
//
|
|
347
|
+
// 安装依赖
|
|
113
348
|
// -----------------------------
|
|
114
|
-
|
|
349
|
+
const pm = opts.pm || detectPackageManager();
|
|
115
350
|
|
|
116
|
-
|
|
117
|
-
|
|
351
|
+
if (opts.install) {
|
|
352
|
+
console.log(chalk.blue(`\n📦 安装依赖(${pm})...\n`));
|
|
353
|
+
execSync(`${pm} install`, {
|
|
118
354
|
cwd: targetDir,
|
|
119
355
|
stdio: 'inherit'
|
|
120
356
|
});
|
|
121
|
-
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// -----------------------------
|
|
360
|
+
// 启动项目
|
|
361
|
+
// -----------------------------
|
|
362
|
+
if (opts.start) {
|
|
363
|
+
console.log(chalk.green('\n🚀 启动项目...\n'));
|
|
122
364
|
try {
|
|
123
|
-
execSync(`${
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Ctrl+C 中断属于正常行为
|
|
365
|
+
execSync(`${pm} run dev`, { cwd: targetDir, stdio: 'inherit' });
|
|
366
|
+
} catch {
|
|
367
|
+
try {
|
|
368
|
+
execSync(`${pm} run start`, { cwd: targetDir, stdio: 'inherit' });
|
|
369
|
+
} catch {}
|
|
129
370
|
}
|
|
130
371
|
}
|
|
372
|
+
|
|
373
|
+
console.log(chalk.green('\n✅ 完成'));
|
|
131
374
|
}
|
|
132
375
|
|
|
133
376
|
// -----------------------------
|
|
134
377
|
// 包管理器检测
|
|
135
378
|
// -----------------------------
|
|
136
|
-
function
|
|
137
|
-
try {
|
|
138
|
-
|
|
139
|
-
return 'pnpm';
|
|
140
|
-
} catch {}
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
execSync('yarn -v', { stdio: 'ignore' });
|
|
144
|
-
return 'yarn';
|
|
145
|
-
} catch {}
|
|
146
|
-
|
|
379
|
+
function detectPackageManager() {
|
|
380
|
+
try { execSync('pnpm -v', { stdio: 'ignore' }); return 'pnpm'; } catch {}
|
|
381
|
+
try { execSync('yarn -v', { stdio: 'ignore' }); return 'yarn'; } catch {}
|
|
147
382
|
return 'npm';
|
|
148
383
|
}
|
|
149
384
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "axhub-make",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Axhub Make scaffolding tool",
|
|
5
5
|
"bin": {
|
|
6
6
|
"axhub-make": "./bin/index.js"
|
|
@@ -15,5 +15,8 @@
|
|
|
15
15
|
"inquirer": "^8.2.0"
|
|
16
16
|
},
|
|
17
17
|
"author": "Lintendo",
|
|
18
|
-
"license": "ISC"
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"execa": "^9.6.1"
|
|
21
|
+
}
|
|
19
22
|
}
|