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.
Files changed (2) hide show
  1. package/bin/index.js +263 -32
  2. 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
- if (fs.existsSync(targetDir)) {
63
- const files = await fs.readdir(targetDir);
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
- if (!isCurrentDir && opts.force) {
76
- await fs.remove(targetDir);
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('\n📡 下载模板...\n'));
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
- if (isCurrentDir) {
98
- await fs.copy(tmpDir, targetDir, {
99
- overwrite: true,
100
- errorOnExist: false
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
- const pkgPath = path.join(targetDir, 'package.json');
109
- if (fs.existsSync(pkgPath)) {
110
- const pkg = await fs.readJson(pkgPath);
111
- pkg.name = projectName;
112
- await fs.writeJson(pkgPath, pkg, { spaces: 2 });
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",
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
  }