axhub-make 1.0.3 → 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 +263 -32
- package/package.json +5 -2
package/bin/index.js
CHANGED
|
@@ -6,23 +6,225 @@ const os = require('os');
|
|
|
6
6
|
const { execFileSync, execSync } = require('child_process');
|
|
7
7
|
const chalk = require('chalk');
|
|
8
8
|
|
|
9
|
+
function normalizeRelPath(p) {
|
|
10
|
+
return p.split(path.sep).join('/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function escapeRegExp(s) {
|
|
14
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
}
|
|
16
|
+
|
|
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;
|
|
31
|
+
}
|
|
32
|
+
if (ch === '?') {
|
|
33
|
+
re += '[^/]';
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
re += escapeRegExp(ch);
|
|
37
|
+
}
|
|
38
|
+
re += '$';
|
|
39
|
+
return new RegExp(re);
|
|
40
|
+
}
|
|
41
|
+
|
|
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')
|
|
76
|
+
];
|
|
77
|
+
const results = await Promise.all(checks.map((p) => fs.pathExists(p)));
|
|
78
|
+
return results.every(Boolean);
|
|
79
|
+
}
|
|
80
|
+
|
|
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
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return files;
|
|
102
|
+
}
|
|
103
|
+
|
|
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
|
+
}
|
|
113
|
+
|
|
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;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (matchesAny(relPosix, rules.neverOverwrite) && destExists) {
|
|
187
|
+
skipped.push(relPosix);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (destExists && rules.defaultOverwrite === false) {
|
|
192
|
+
skipped.push(relPosix);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
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
|
+
|
|
9
204
|
// -----------------------------
|
|
10
205
|
// 参数解析(无依赖,稳定)
|
|
11
206
|
// -----------------------------
|
|
12
207
|
function parseArgs(argv) {
|
|
13
208
|
const args = argv.slice(2);
|
|
14
209
|
const opts = {
|
|
210
|
+
pre: false,
|
|
15
211
|
dir: '.',
|
|
16
212
|
template: 'https://github.com/lintendo/Axhub-Make.git',
|
|
17
213
|
install: true,
|
|
18
214
|
start: true,
|
|
19
215
|
force: false,
|
|
20
|
-
pm: null
|
|
216
|
+
pm: null,
|
|
217
|
+
conflict: 'keep'
|
|
21
218
|
};
|
|
22
219
|
|
|
23
220
|
for (let i = 0; i < args.length; i++) {
|
|
24
221
|
const a = args[i];
|
|
25
222
|
|
|
223
|
+
if ((a === 'pre' || a === 'preinstall' || a === 'preupdate') && opts.dir === '.' && !opts.pre) {
|
|
224
|
+
opts.pre = true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
26
228
|
if (!a.startsWith('-') && opts.dir === '.') {
|
|
27
229
|
opts.dir = a;
|
|
28
230
|
continue;
|
|
@@ -35,8 +237,13 @@ function parseArgs(argv) {
|
|
|
35
237
|
|
|
36
238
|
if (a === '--no-install') opts.install = false;
|
|
37
239
|
if (a === '--no-start') opts.start = false;
|
|
240
|
+
if (a === '--pre') opts.pre = true;
|
|
38
241
|
if (a === '--force') opts.force = true;
|
|
39
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
|
+
}
|
|
40
247
|
}
|
|
41
248
|
|
|
42
249
|
return opts;
|
|
@@ -57,34 +264,33 @@ async function run() {
|
|
|
57
264
|
const projectName = path.basename(targetDir);
|
|
58
265
|
|
|
59
266
|
// -----------------------------
|
|
60
|
-
//
|
|
267
|
+
// 安装/更新模式识别
|
|
61
268
|
// -----------------------------
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (files.length > 0 && !opts.force) {
|
|
66
|
-
console.log(
|
|
67
|
-
chalk.red(
|
|
68
|
-
'❌ 当前目录非空。\n' +
|
|
69
|
-
'如确认覆盖,请使用 --force'
|
|
70
|
-
)
|
|
71
|
-
);
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
269
|
+
const targetExists = fs.existsSync(targetDir);
|
|
270
|
+
const isUpdate = targetExists ? await isAxhubMakeProject(targetDir) : false;
|
|
74
271
|
|
|
75
|
-
|
|
76
|
-
|
|
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);
|
|
77
286
|
}
|
|
78
287
|
}
|
|
79
288
|
|
|
80
289
|
// -----------------------------
|
|
81
|
-
//
|
|
290
|
+
// 模板下载(统一下载到临时目录)
|
|
82
291
|
// -----------------------------
|
|
83
|
-
console.log(chalk.blue(
|
|
84
|
-
|
|
85
|
-
const tmpDir = isCurrentDir
|
|
86
|
-
? path.join(os.tmpdir(), `axhub-make-${Date.now()}`)
|
|
87
|
-
: targetDir;
|
|
292
|
+
console.log(chalk.blue(`\n📡 下载模板(${isUpdate ? '更新' : '安装'})...\n`));
|
|
293
|
+
const tmpDir = path.join(os.tmpdir(), `axhub-make-${Date.now()}`);
|
|
88
294
|
|
|
89
295
|
execFileSync(
|
|
90
296
|
'git',
|
|
@@ -94,22 +300,47 @@ async function run() {
|
|
|
94
300
|
|
|
95
301
|
await fs.remove(path.join(tmpDir, '.git'));
|
|
96
302
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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);
|
|
102
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;
|
|
103
315
|
}
|
|
104
316
|
|
|
317
|
+
const result = await applyUpdateFromTemplate(tmpDir, targetDir, rules, conflictMode);
|
|
318
|
+
await fs.remove(tmpDir);
|
|
319
|
+
|
|
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} 个`));
|
|
333
|
+
|
|
105
334
|
// -----------------------------
|
|
106
335
|
// package.json 处理
|
|
107
336
|
// -----------------------------
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
}
|
|
113
344
|
}
|
|
114
345
|
|
|
115
346
|
// -----------------------------
|
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
|
}
|