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.
Files changed (2) hide show
  1. package/bin/index.js +329 -94
  2. 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
- async function run() {
10
- console.log(chalk.magenta.bold('\n🚀 Axhub Make - 项目初始化\n'));
9
+ function normalizeRelPath(p) {
10
+ return p.split(path.sep).join('/');
11
+ }
11
12
 
12
- // -----------------------------
13
- // 1. 参数解析
14
- // -----------------------------
15
- const args = process.argv.slice(2);
16
- let templateGit = '';
13
+ function escapeRegExp(s) {
14
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
+ }
17
16
 
18
- for (let i = 0; i < args.length; i++) {
19
- if (args[i] === '-t' || args[i] === '--template') {
20
- templateGit = args[i + 1];
21
- break;
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
- // 2. 交互
27
- // -----------------------------
28
- const questions = [
29
- {
30
- type: 'input',
31
- name: 'projectName',
32
- message: '请输入项目名称:',
33
- default: 'my-app'
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
- if (!templateGit) {
38
- questions.push({
39
- type: 'input',
40
- name: 'customTemplateGit',
41
- message: '请输入 Git 仓库地址:',
42
- default: 'https://github.com/lintendo/Axhub-Make.git'
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
- const answers = await inquirer.prompt(questions);
101
+ return files;
102
+ }
47
103
 
48
- const projectName = answers.projectName.trim();
49
- templateGit = templateGit || answers.customTemplateGit;
50
- const targetDir = path.resolve(process.cwd(), projectName);
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
- // 3. 目录检查
54
- // -----------------------------
55
- if (fs.existsSync(targetDir)) {
56
- const { overwrite } = await inquirer.prompt([
57
- {
58
- type: 'confirm',
59
- name: 'overwrite',
60
- message: `目录 ${projectName} 已存在,是否覆盖?`,
61
- default: false
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 (!overwrite) {
66
- console.log(chalk.yellow('已取消'));
67
- return;
191
+ if (destExists && rules.defaultOverwrite === false) {
192
+ skipped.push(relPosix);
193
+ continue;
68
194
  }
69
195
 
70
- await fs.remove(targetDir);
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
- // 4. git clone(安全版,支持空格/中文路径)
290
+ // 模板下载(统一下载到临时目录)
75
291
  // -----------------------------
76
- console.log(chalk.blue('\n📡 正在下载模板...\n'));
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', templateGit, targetDir],
297
+ ['clone', '--depth', '1', opts.template, tmpDir],
81
298
  { stdio: 'inherit' }
82
299
  );
83
300
 
84
- // 移除 .git
85
- await fs.remove(path.join(targetDir, '.git'));
301
+ await fs.remove(path.join(tmpDir, '.git'));
86
302
 
87
- // -----------------------------
88
- // 5. 自动修改 package.json
89
- // -----------------------------
90
- const pkgPath = path.join(targetDir, 'package.json');
91
- if (fs.existsSync(pkgPath)) {
92
- const pkg = await fs.readJson(pkgPath);
93
- pkg.name = projectName;
94
- await fs.writeJson(pkgPath, pkg, { spaces: 2 });
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
- // 6. 安装依赖
99
- // -----------------------------
100
- const pkgManager = getPackageManager();
317
+ const result = await applyUpdateFromTemplate(tmpDir, targetDir, rules, conflictMode);
318
+ await fs.remove(tmpDir);
101
319
 
102
- console.log(
103
- chalk.blue(`\n📦 正在使用 ${pkgManager} 安装依赖(请稍等)...\n`)
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
- execSync(`${pkgManager} install`, {
107
- cwd: targetDir,
108
- stdio: 'inherit'
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
- // 7. 直接启动(dev 优先,fallback start)
347
+ // 安装依赖
113
348
  // -----------------------------
114
- console.log(chalk.green('\n🚀 依赖安装完成,正在启动项目...\n'));
349
+ const pm = opts.pm || detectPackageManager();
115
350
 
116
- try {
117
- execSync(`${pkgManager} run dev`, {
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
- } catch (e) {
357
+ }
358
+
359
+ // -----------------------------
360
+ // 启动项目
361
+ // -----------------------------
362
+ if (opts.start) {
363
+ console.log(chalk.green('\n🚀 启动项目...\n'));
122
364
  try {
123
- execSync(`${pkgManager} run start`, {
124
- cwd: targetDir,
125
- stdio: 'inherit'
126
- });
127
- } catch (_) {
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 getPackageManager() {
137
- try {
138
- execSync('pnpm -v', { stdio: 'ignore' });
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.2",
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
  }