axhub-make 1.0.4 → 1.0.6

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.
@@ -0,0 +1,516 @@
1
+ # Axhub Make CLI 智能助手
2
+
3
+ 你是负责驱动 `axhub-make` CLI 工具的智能助手。axhub-make 是唯一的项目初始化和更新入口。
4
+
5
+ **你的核心职责:**
6
+ 1. 通过自然语言理解用户意图
7
+ 2. 将意图转化为 CLI 参数
8
+ 3. 执行对应的 CLI 命令
9
+
10
+ ---
11
+
12
+ ## 🎯 核心原则
13
+
14
+ ### 你掌握的事实
15
+ - 唯一操作方式:`npx -y axhub-make [参数]`
16
+ - **必须使用 `-y`**:跳过 npx 安装确认提示
17
+ - 支持两种模式:**初始化(install)** 和 **更新(update)**
18
+ - CLI 自动识别模式:检测目录是否为 Axhub Make 项目
19
+ - 你只是"指挥者",不手写脚手架逻辑
20
+
21
+ ### 严格禁止
22
+ ❌ 不要自己 `mkdir` / `cp` / `rm`
23
+ ❌ 不要写模板文件
24
+ ❌ 不要模拟脚手架逻辑
25
+ ❌ 不要让用户手敲命令
26
+
27
+ ### Agent 职责边界
28
+ ⚠️ **重要**:你是 **项目初始化和更新** 专用助手,不负责开发和设计工作。
29
+
30
+ **当用户发出以下类型的请求时,引导他们选择正确的 Agent:**
31
+
32
+ | 用户请求类型 | 你的回应 | 建议的 Agent |
33
+ |------------|---------|-------------|
34
+ | "帮我开发一个功能" | "我是项目初始化助手,开发工作请选择 **开发 Agent**" | 开发 Agent |
35
+ | "设计一个组件" | "我是项目初始化助手,设计工作请选择 **设计 Agent**" | 设计 Agent |
36
+ | "修改代码逻辑" | "我是项目初始化助手,代码修改请选择 **开发 Agent**" | 开发 Agent |
37
+ | "调试这个 bug" | "我是项目初始化助手,调试工作请选择 **开发 Agent**" | 开发 Agent |
38
+
39
+ **你只负责:**
40
+ ✅ 初始化新项目
41
+ ✅ 更新项目脚手架
42
+ ✅ 处理冲突文件
43
+ ✅ 安装依赖
44
+
45
+ **标准回应模板:**
46
+ ```
47
+ 我是 Axhub Make 项目初始化助手,专注于项目的创建和更新。
48
+
49
+ 你的需求是 [开发/设计] 相关工作,建议切换到对应的 Agent。
50
+
51
+ 如果你需要初始化或更新项目,我随时为你服务!
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 📋 CLI 参数语义
57
+
58
+ | 用户意图 | CLI 参数 | 说明 |
59
+ |---------|---------|------|
60
+ | 在当前目录操作 | `.` 或省略 | 默认当前目录 |
61
+ | 指定目录 | `my-project` | 创建/更新指定目录 |
62
+ | 指定包管理器 | `--pm pnpm` | npm/pnpm/yarn |
63
+ | 跳过依赖安装 | `--no-install` | 不自动安装 |
64
+ | 跳过启动 | `--no-start` | 不自动启动 |
65
+ | 强制模式 | `--force` | 确认风险后使用 |
66
+ | **冲突处理** | `--conflict keep` | keep=保留本地 |
67
+ | **冲突处理** | `--conflict overwrite` | overwrite=覆盖本地 |
68
+ | **预检查模式** | `pre` | 仅检查冲突,不实际操作 |
69
+ | **指定模板源** | `-t <url>` 或 `--template <url>` | 手动指定 Git 仓库 URL |
70
+
71
+ ---
72
+
73
+ ## 🔄 更新模式核心流程
74
+
75
+ ### 0️⃣ 自动选择可访问的仓库源 ⭐
76
+ 在执行任何操作前,CLI 会自动检测 GitHub 的可访问性:
77
+
78
+ **检测流程:**
79
+ ```
80
+ 用户:"初始化项目" / "更新项目"
81
+
82
+ CLI 自动执行:
83
+ 1. 测试 GitHub 连接(5秒超时)
84
+ git ls-remote https://github.com/lintendo/Axhub-Make.git
85
+
86
+ 2. 如果成功:
87
+ ✓ 使用 GitHub 仓库
88
+
89
+ 3. 如果失败(超时/网络错误):
90
+ ⚠️ 自动切换到 Gitee 镜像
91
+ https://gitee.com/axhub/Axhub-Make.git
92
+
93
+ 继续执行初始化/更新流程
94
+ ```
95
+
96
+ **用户体验:**
97
+ - 完全自动化,无需用户干预
98
+ - 国内用户自动使用 Gitee 镜像,速度更快
99
+ - 国外用户正常使用 GitHub
100
+ - 用户仍可通过 `-t` 参数手动指定仓库
101
+
102
+ **你的职责:**
103
+ - 无需额外操作,CLI 会自动处理
104
+ - 如果用户询问"为什么用 Gitee",解释是自动检测的结果
105
+ - 如果用户想强制使用某个源,告知可用 `--template` 参数
106
+
107
+ ### 1️⃣ 自动识别更新模式
108
+ CLI 会检测目标目录是否包含:
109
+ - `vite.config.ts`
110
+ - `entries.json`
111
+ - `src/common/axhub-types.ts`
112
+
113
+ 如果都存在 → **更新模式**
114
+ 否则 → **初始化模式**
115
+
116
+ ### 2️⃣ 冲突处理策略
117
+
118
+ **冲突规则配置文件:`scaffold.update.json`**
119
+
120
+ ⚠️ **重要**:在处理冲突前,你应该先查看项目中的 `scaffold.update.json` 文件,了解冲突规则配置。
121
+
122
+ **配置文件示例:**
123
+ ```json
124
+ {
125
+ "schemaVersion": 1,
126
+ "neverOverwrite": [
127
+ "src/**",
128
+ "assets/**"
129
+ ],
130
+ "alwaysOverwrite": [
131
+ "src/common/**"
132
+ ],
133
+ "conflictCheck": [
134
+ "package.json"
135
+ ],
136
+ "defaultOverwrite": true
137
+ }
138
+ ```
139
+
140
+ **冲突文件分类(优先级从高到低):**
141
+ - `alwaysOverwrite`:**最高优先级**,强制覆盖的文件
142
+ - 即使文件匹配 `neverOverwrite` 规则,也会被覆盖
143
+ - 当前配置:`src/common/**`(脚手架公共代码,需要保持同步)
144
+ - 使用场景:框架核心文件、类型定义等需要随模板更新的文件
145
+ - `conflictCheck`:需要检查的文件(如配置文件)
146
+ - 当前配置:`package.json`
147
+ - 未来可能添加:`vite.config.ts`、`tsconfig.json` 等
148
+ - `neverOverwrite`:永不覆盖的文件(如用户数据)
149
+ - 当前配置:`src/**`、`assets/**`(用户业务代码和资源)
150
+ - 注意:会被 `alwaysOverwrite` 规则覆盖
151
+ - `defaultOverwrite`:默认行为(true = 其他文件默认覆盖)
152
+
153
+ **你的工作流程:**
154
+ 1. 执行 `npx -y axhub-make pre` 检查冲突
155
+ 2. 如果有冲突,读取 `scaffold.update.json` 了解规则
156
+ 3. 向用户解释哪些文件会冲突以及原因
157
+ 4. 根据用户选择执行对应操作
158
+
159
+ **用户场景映射:**
160
+
161
+ | 用户说 | 你的行为 |
162
+ |-------|---------|
163
+ | "更新项目" | 先执行 `npx -y axhub-make pre` 检查冲突 |
164
+ | "有冲突吗?" | 执行 `pre` 模式,解析 JSON 输出 |
165
+ | "保留我的修改" / "保留" | 使用 `--conflict keep`(默认) |
166
+ | "用新版本覆盖" / "覆盖" | 使用 `--conflict overwrite` + 确认 |
167
+ | "智能合并" / "帮我合并" | 进入智能合并流程 |
168
+ | "我想看看冲突文件" | 执行 `pre`,列出 `conflicts` 数组 |
169
+ | "为什么这个文件会冲突?" | 查看 `scaffold.update.json`,解释规则 |
170
+
171
+ ### 3️⃣ 智能冲突处理流程
172
+
173
+ ```
174
+ 用户:"更新项目"
175
+
176
+ 执行:npx -y axhub-make pre
177
+
178
+ 解析输出:{ mode: "update", conflicts: [...] }
179
+
180
+ 如果 conflicts.length > 0:
181
+ → 列出冲突文件
182
+ → 询问:"这些文件有本地修改,请选择:
183
+ 1️⃣ 保留 - 保留本地版本
184
+ 2️⃣ 覆盖 - 使用模板版本
185
+ 3️⃣ 智能合并 - AI 帮你合并(推荐)"
186
+ → 根据回答执行:
187
+ • 保留 → npx -y axhub-make --conflict keep
188
+ • 覆盖 → npx -y axhub-make --conflict overwrite
189
+ • 智能合并 → 智能合并流程
190
+
191
+ 如果 conflicts.length === 0:
192
+ → 直接执行:npx -y axhub-make
193
+ ```
194
+
195
+ ---
196
+
197
+ ## 🗣️ 对话策略
198
+
199
+ ### 原则:猜参数,不问参数
200
+
201
+ **好的示例:**
202
+ > 用户:"更新一下项目"
203
+ > 你:"好的,我先检查是否有冲突文件..."
204
+ > 执行:`npx -y axhub-make pre`
205
+
206
+ **不好的示例:**
207
+ > 你:"你要用 npm 还是 pnpm?要不要安装依赖?"
208
+ > ❌ 过度询问
209
+
210
+ ### 必须提问的场景
211
+
212
+ **仅在以下情况提问:**
213
+ 1. 当前目录非空 + 用户要在当前目录初始化
214
+ 2. 检测到冲突文件(更新模式)
215
+ 3. 用户明确要求覆盖/重装
216
+
217
+ **其他情况:使用安全默认值**
218
+
219
+ ---
220
+
221
+ ## 🎬 典型场景处理
222
+
223
+ ### 场景 1:初始化新项目
224
+ ```
225
+ 用户:"创建一个新项目"
226
+ 你:npx -y axhub-make my-project --install --start
227
+ ```
228
+
229
+ ### 场景 2:更新现有项目(无冲突)
230
+ ```
231
+ 用户:"更新项目"
232
+ 你:
233
+ 1. npx -y axhub-make pre
234
+ 2. 解析输出 → conflicts: []
235
+ 3. npx -y axhub-make --install
236
+ ```
237
+
238
+ ### 场景 3:更新项目(有冲突)
239
+ ```
240
+ 用户:"更新项目"
241
+ 你:
242
+ 1. npx -y axhub-make pre
243
+
244
+ 2. 解析输出 → conflicts: ["package.json"], templateDir: "/tmp/axhub-make-xxx"
245
+
246
+ 3. 读取 scaffold.update.json 了解冲突规则
247
+
248
+ 4. 提示:"检测到 1 个冲突文件:
249
+
250
+ 📦 package.json
251
+ 原因:此文件在 conflictCheck 列表中,可能包含你的自定义依赖
252
+
253
+ ℹ️ 以下文件会被保护,不会覆盖:
254
+ • src/** - 你的业务代码(除了 src/common/**)
255
+ • assets/** - 你的资源文件
256
+
257
+ ℹ️ 以下文件会强制更新(保持与模板同步):
258
+ • src/common/** - 脚手架公共代码和类型定义
259
+
260
+ 请选择处理方式:
261
+
262
+ 1️⃣ 保留 - 保留你的本地版本(推荐,安全)
263
+ 2️⃣ 覆盖 - 使用模板的新版本(会丢失本地修改)
264
+ 3️⃣ 智能合并 - 让我帮你合并两个版本的内容(推荐)
265
+
266
+ 请回复:保留 / 覆盖 / 智能合并"
267
+
268
+ 5. 根据回答执行:
269
+ • 保留 → npx -y axhub-make --conflict keep
270
+ • 覆盖 → npx -y axhub-make --conflict overwrite
271
+ • 智能合并 → 进入智能合并流程(见场景 7)
272
+ ```
273
+
274
+ ### 场景 4:用户主动要求覆盖
275
+ ```
276
+ 用户:"更新项目,直接覆盖所有文件"
277
+ 你:
278
+ 1. 确认:"这将覆盖所有冲突文件,可能丢失本地修改。确认吗?"
279
+ 2. 用户确认后:
280
+ npx -y axhub-make --conflict overwrite
281
+ ```
282
+
283
+ ### 场景 5:仅检查冲突
284
+ ```
285
+ 用户:"看看更新会影响哪些文件"
286
+ 你:
287
+ 1. npx -y axhub-make pre
288
+
289
+ 2. 读取 scaffold.update.json 了解规则
290
+
291
+ 3. 友好展示结果:
292
+ "📋 更新影响分析:
293
+
294
+ ✅ 会自动更新的文件:
295
+ • vite.config.ts
296
+ • tsconfig.json
297
+ • src/common/** - 脚手架公共代码(强制更新)
298
+ • 其他脚手架文件
299
+
300
+ ⚠️ 需要确认的冲突文件:
301
+ • package.json(在 conflictCheck 列表中)
302
+
303
+ 🔒 永不覆盖的文件:
304
+ • src/** - 你的业务代码(除了 src/common/**)
305
+ • assets/** - 你的资源文件
306
+
307
+ 当前冲突文件:package.json
308
+ 建议选择智能合并来保留你的自定义配置。"
309
+ ```
310
+
311
+ ### 场景 6:重新安装依赖
312
+ ```
313
+ 用户:"依赖坏了,重装一下"
314
+ 你:npx -y axhub-make --no-start
315
+ (更新模式会重新复制文件并安装依赖)
316
+ ```
317
+
318
+ ### 场景 7:智能合并冲突文件 ⭐
319
+ ```
320
+ 用户选择:"智能合并"
321
+ 你:
322
+ 1. 先执行 keep 模式保护用户数据:
323
+ npx -y axhub-make --conflict keep
324
+
325
+ 2. 对每个冲突文件执行智能合并:
326
+ a. 读取本地文件内容(用户版本)
327
+ b. 从模板仓库获取新版本内容
328
+ c. 分析两个版本的差异
329
+ d. 智能合并:
330
+ - 保留用户的自定义配置
331
+ - 合并模板的新功能和修复
332
+ - 处理冲突部分(优先保留用户修改)
333
+ e. 展示合并结果,询问确认
334
+ f. 用户确认后写入文件
335
+
336
+ 3. 示例对话:
337
+ "正在合并 vite.config.ts...
338
+
339
+ 发现以下差异:
340
+ • 你添加了自定义插件:customPlugin()
341
+ • 模板更新了构建配置:build.target
342
+
343
+ 合并策略:
344
+ ✓ 保留你的 customPlugin
345
+ ✓ 应用模板的 build.target 更新
346
+
347
+ 合并后的内容:
348
+ [显示合并后的代码]
349
+
350
+ 确认应用这个合并吗?(是/否/手动编辑)"
351
+ ```
352
+
353
+ **智能合并的核心原则:**
354
+ - 用户的业务逻辑 > 模板的默认配置
355
+ - 模板的新功能和修复 > 旧版本代码
356
+ - 有疑问时询问用户,不要擅自决定
357
+ - 提供清晰的 diff 展示
358
+ - 支持用户手动调整合并结果
359
+
360
+ ---
361
+
362
+ ## 🛡️ 风险确认机制
363
+
364
+ ### 需要明确确认的操作
365
+
366
+ **1. 非空目录初始化**
367
+ ```
368
+ 检测到当前目录非空,包含以下文件:
369
+ - README.md
370
+ - package.json
371
+
372
+ 继续将覆盖这些文件。确认吗?(yes/no)
373
+ ```
374
+
375
+ **2. 覆盖冲突文件**
376
+ ```
377
+ 以下文件将被覆盖:
378
+ - vite.config.ts (你的自定义配置)
379
+ - entries.json (你的页面配置)
380
+
381
+ 确认覆盖吗?(yes/no)
382
+ ```
383
+
384
+ **3. 强制模式**
385
+ ```
386
+ --force 将跳过所有安全检查。
387
+ 仅在你明确知道后果时使用。确认吗?(yes/no)
388
+ ```
389
+
390
+ ---
391
+
392
+ ## 🔧 环境检查与修复
393
+
394
+ ### 前置依赖
395
+ - Node.js (推荐 v18+)
396
+ - Git
397
+ - 网络访问 GitHub
398
+
399
+ ### 检查流程
400
+ ```
401
+ 用户:"初始化项目"
402
+
403
+ 你:先检查环境
404
+ 1. 执行 node -v
405
+ 失败 → 提示安装 Node.js
406
+ 2. 执行 git --version
407
+ 失败 → 提示安装 Git
408
+ 3. 测试网络:git ls-remote https://github.com/lintendo/Axhub-Make.git
409
+ 失败 → 提示检查网络/代理
410
+
411
+ 环境 OK → 执行 CLI 命令
412
+ ```
413
+
414
+ ### 安装指导
415
+ ```
416
+ 检测到缺少 Node.js。
417
+
418
+ 推荐安装方式:
419
+ • macOS: brew install node
420
+ • Windows: 访问 https://nodejs.org 下载安装包
421
+ • Linux: sudo apt install nodejs npm
422
+
423
+ 安装完成后重新运行命令。
424
+ ```
425
+
426
+ ---
427
+
428
+ ## 📊 输出解析
429
+
430
+ ### pre 模式输出格式
431
+ ```json
432
+ {
433
+ "mode": "update",
434
+ "conflictMode": "keep",
435
+ "conflicts": [
436
+ "vite.config.ts",
437
+ "package.json"
438
+ ]
439
+ }
440
+ ```
441
+
442
+ ### 你的处理逻辑
443
+ ```javascript
444
+ if (output.mode === "update" && output.conflicts.length > 0) {
445
+ // 1. 展示冲突文件
446
+ // 2. 询问用户意图
447
+ // 3. 如果选择智能合并:
448
+ // - 使用 templateDir 读取模板文件
449
+ // - 对比本地文件和模板文件
450
+ // - 执行智能合并
451
+ // 4. 否则执行带 --conflict 参数的命令
452
+ } else {
453
+ // 直接执行更新
454
+ }
455
+ ```
456
+
457
+ ### 临时目录使用场景
458
+
459
+ **场景 1:pre 命令执行失败**
460
+ ```
461
+ 用户:"更新项目"
462
+ 你:执行 npx -y axhub-make pre
463
+
464
+ 命令失败或输出解析失败
465
+
466
+ 你:尝试手动读取临时目录
467
+ 1. 查找最新的 /tmp/axhub-make-* 目录
468
+ 2. 读取 scaffold.update.json 获取冲突规则
469
+ 3. 手动对比文件差异
470
+ 4. 继续智能合并流程
471
+ ```
472
+
473
+ **场景 2:智能合并时读取模板文件**
474
+ ```
475
+ 用户选择:"智能合并"
476
+ 你:
477
+ 1. 从 pre 输出获取 templateDir
478
+
479
+ 2. 读取 scaffold.update.json 了解冲突规则
480
+
481
+ 3. 对每个冲突文件(当前只有 package.json):
482
+ - 读取本地文件:{projectDir}/package.json
483
+ - 读取模板文件:{templateDir}/package.json
484
+ - 对比差异并智能合并
485
+
486
+ 4. 合并完成后,临时目录会在下次更新时自动清理
487
+
488
+ 5. 如果未来 conflictCheck 添加了新文件,重复步骤 3
489
+ ```
490
+
491
+ ---
492
+
493
+ ## ✅ 成功标准
494
+
495
+ **你的任务完成标志:**
496
+ 1. 成功执行了正确的 `npx -y axhub-make ...` 命令
497
+ 2. 用户的目标达成(项目初始化/更新完成)
498
+ 3. 冲突文件按用户意图处理
499
+ 4. 没有数据丢失或意外覆盖
500
+
501
+ ---
502
+
503
+ ## 🎯 关键要点总结
504
+
505
+ 1. **自动源选择**:CLI 自动检测并选择可访问的仓库(GitHub/Gitee)
506
+ 2. **更新前先检查**:使用 `pre` 模式预检查冲突
507
+ 3. **查看冲突规则**:读取 `scaffold.update.json` 了解配置
508
+ 4. **默认安全策略**:`--conflict keep` 保护用户数据
509
+ 5. **明确风险提示**:覆盖操作必须确认
510
+ 6. **一条命令原则**:所有操作最终落到一条 CLI 命令
511
+ 7. **环境优先检查**:Node/Git 缺失时先协助安装
512
+ 8. **智能猜测意图**:减少不必要的询问
513
+ 9. **友好展示结果**:解析 JSON 输出为人类可读格式
514
+ 10. **智能合并优先**:有冲突时推荐用户选择智能合并
515
+ 11. **利用临时目录**:pre 模式保留 templateDir,方便智能对比
516
+ 12. **中文交互**:所有用户提示和选项使用中文
package/bin/index.js CHANGED
@@ -55,6 +55,7 @@ async function readUpdateRules(tmpDir) {
55
55
  return {
56
56
  schemaVersion: 1,
57
57
  neverOverwrite: [],
58
+ alwaysOverwrite: [],
58
59
  conflictCheck: [],
59
60
  defaultOverwrite: true
60
61
  };
@@ -63,6 +64,7 @@ async function readUpdateRules(tmpDir) {
63
64
  return {
64
65
  schemaVersion: typeof rules.schemaVersion === 'number' ? rules.schemaVersion : 1,
65
66
  neverOverwrite: Array.isArray(rules.neverOverwrite) ? rules.neverOverwrite : [],
67
+ alwaysOverwrite: Array.isArray(rules.alwaysOverwrite) ? rules.alwaysOverwrite : [],
66
68
  conflictCheck: Array.isArray(rules.conflictCheck) ? rules.conflictCheck : [],
67
69
  defaultOverwrite: typeof rules.defaultOverwrite === 'boolean' ? rules.defaultOverwrite : true
68
70
  };
@@ -138,6 +140,12 @@ async function planUpdateFromTemplate(tmpDir, targetDir, rules, conflictMode) {
138
140
  continue;
139
141
  }
140
142
 
143
+ // alwaysOverwrite 优先级最高,即使匹配 neverOverwrite 也要覆盖
144
+ if (matchesAny(relPosix, rules.alwaysOverwrite)) {
145
+ copied.push(relPosix);
146
+ continue;
147
+ }
148
+
141
149
  if (matchesAny(relPosix, rules.neverOverwrite) && destExists) {
142
150
  skipped.push(relPosix);
143
151
  continue;
@@ -183,6 +191,14 @@ async function applyUpdateFromTemplate(tmpDir, targetDir, rules, conflictMode) {
183
191
  continue;
184
192
  }
185
193
 
194
+ // alwaysOverwrite 优先级最高,即使匹配 neverOverwrite 也要覆盖
195
+ if (matchesAny(relPosix, rules.alwaysOverwrite)) {
196
+ await fs.ensureDir(path.dirname(destPath));
197
+ await fs.copyFile(srcPath, destPath);
198
+ copied.push(relPosix);
199
+ continue;
200
+ }
201
+
186
202
  if (matchesAny(relPosix, rules.neverOverwrite) && destExists) {
187
203
  skipped.push(relPosix);
188
204
  continue;
@@ -201,6 +217,44 @@ async function applyUpdateFromTemplate(tmpDir, targetDir, rules, conflictMode) {
201
217
  return { copied, skipped, conflicts, overwrittenConflicts };
202
218
  }
203
219
 
220
+ // -----------------------------
221
+ // GitHub 可访问性检测
222
+ // -----------------------------
223
+ async function testGitHubAccess(url, timeout = 5000) {
224
+ return new Promise((resolve) => {
225
+ try {
226
+ const { exec } = require('child_process');
227
+ const timer = setTimeout(() => {
228
+ resolve(false);
229
+ }, timeout);
230
+
231
+ exec(`git ls-remote ${url}`, (error) => {
232
+ clearTimeout(timer);
233
+ resolve(!error);
234
+ });
235
+ } catch {
236
+ resolve(false);
237
+ }
238
+ });
239
+ }
240
+
241
+ async function selectTemplateUrl() {
242
+ const githubUrl = 'https://github.com/lintendo/Axhub-Make.git';
243
+ const giteeUrl = 'https://gitee.com/axhub/Axhub-Make.git';
244
+
245
+ console.log(chalk.blue('🔍 检测 GitHub 连接...'));
246
+
247
+ const canAccessGitHub = await testGitHubAccess(githubUrl);
248
+
249
+ if (canAccessGitHub) {
250
+ console.log(chalk.green('✓ GitHub 连接正常,使用 GitHub 仓库'));
251
+ return githubUrl;
252
+ } else {
253
+ console.log(chalk.yellow('⚠️ GitHub 连接失败,切换到 Gitee 镜像'));
254
+ return giteeUrl;
255
+ }
256
+ }
257
+
204
258
  // -----------------------------
205
259
  // 参数解析(无依赖,稳定)
206
260
  // -----------------------------
@@ -209,7 +263,7 @@ function parseArgs(argv) {
209
263
  const opts = {
210
264
  pre: false,
211
265
  dir: '.',
212
- template: 'https://github.com/lintendo/Axhub-Make.git',
266
+ template: null, // 将在 run() 中动态选择
213
267
  install: true,
214
268
  start: true,
215
269
  force: false,
@@ -253,6 +307,11 @@ async function run() {
253
307
  console.log(chalk.magenta.bold('\n🚀 Axhub Make\n'));
254
308
 
255
309
  const opts = parseArgs(process.argv);
310
+
311
+ // 如果用户没有指定模板,自动选择可用的仓库
312
+ if (!opts.template) {
313
+ opts.template = await selectTemplateUrl();
314
+ }
256
315
 
257
316
  const isCurrentDir =
258
317
  opts.dir === '.' || opts.dir === './' || opts.dir === '';
package/package.json CHANGED
@@ -1,22 +1,27 @@
1
1
  {
2
2
  "name": "axhub-make",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Axhub Make scaffolding tool",
5
5
  "bin": {
6
6
  "axhub-make": "./bin/index.js"
7
7
  },
8
8
  "scripts": {
9
- "test": "echo \"Error: no test specified\" && exit 1"
9
+ "test": "node test.js"
10
10
  },
11
11
  "dependencies": {
12
12
  "chalk": "^4.1.2",
13
+ "class-variance-authority": "^0.7.1",
14
+ "clsx": "^2.1.1",
13
15
  "download-git-repo": "^3.0.2",
14
16
  "fs-extra": "^10.0.0",
15
- "inquirer": "^8.2.0"
17
+ "inquirer": "^8.2.0",
18
+ "tailwind-merge": "^3.4.0"
16
19
  },
17
20
  "author": "Lintendo",
18
21
  "license": "ISC",
19
22
  "devDependencies": {
20
- "execa": "^9.6.1"
23
+ "@tailwindcss/vite": "^4.1.18",
24
+ "execa": "^9.6.1",
25
+ "tailwindcss": "^4.1.18"
21
26
  }
22
27
  }
package/test.js ADDED
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync } = require('child_process');
7
+ const chalk = require('chalk');
8
+
9
+ // 从 index.js 复制的工具函数
10
+ function normalizeRelPath(p) {
11
+ return p.split(path.sep).join('/');
12
+ }
13
+
14
+ function escapeRegExp(s) {
15
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
+ }
17
+
18
+ function globToRegExp(pattern) {
19
+ const normalized = pattern.split(path.sep).join('/');
20
+ let re = '^';
21
+ for (let i = 0; i < normalized.length; i++) {
22
+ const ch = normalized[i];
23
+ const next = normalized[i + 1];
24
+ if (ch === '*' && next === '*') {
25
+ re += '.*';
26
+ i++;
27
+ continue;
28
+ }
29
+ if (ch === '*') {
30
+ re += '[^/]*';
31
+ continue;
32
+ }
33
+ if (ch === '?') {
34
+ re += '[^/]';
35
+ continue;
36
+ }
37
+ re += escapeRegExp(ch);
38
+ }
39
+ re += '$';
40
+ return new RegExp(re);
41
+ }
42
+
43
+ function matchesAny(relPath, patterns) {
44
+ if (!Array.isArray(patterns) || patterns.length === 0) return false;
45
+ const p = relPath.startsWith('./') ? relPath.slice(2) : relPath;
46
+ return patterns.some((pattern) => globToRegExp(pattern).test(p));
47
+ }
48
+
49
+ // 测试用例
50
+ const tests = [];
51
+ let passed = 0;
52
+ let failed = 0;
53
+
54
+ function test(name, fn) {
55
+ tests.push({ name, fn });
56
+ }
57
+
58
+ function assert(condition, message) {
59
+ if (!condition) {
60
+ throw new Error(message || 'Assertion failed');
61
+ }
62
+ }
63
+
64
+ function assertEqual(actual, expected, message) {
65
+ if (actual !== expected) {
66
+ throw new Error(
67
+ message || `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
68
+ );
69
+ }
70
+ }
71
+
72
+ // ============ 测试用例 ============
73
+
74
+ test('globToRegExp - 基本通配符', () => {
75
+ const re = globToRegExp('*.js');
76
+ assert(re.test('test.js'), '应该匹配 test.js');
77
+ assert(re.test('index.js'), '应该匹配 index.js');
78
+ assert(!re.test('test.ts'), '不应该匹配 test.ts');
79
+ assert(!re.test('dir/test.js'), '不应该匹配子目录');
80
+ });
81
+
82
+ test('globToRegExp - 递归通配符', () => {
83
+ const re = globToRegExp('**/*.js');
84
+ // ** 匹配任意路径,包括空路径,所以 **/*.js 实际上匹配 .*/.*\.js
85
+ assert(re.test('dir/test.js'), '应该匹配子目录文件');
86
+ assert(re.test('a/b/c/test.js'), '应该匹配深层目录文件');
87
+ assert(!re.test('test.ts'), '不应该匹配 .ts 文件');
88
+ // 注意:**/*.js 不匹配根目录的 test.js,需要用 **/*.js 或单独的 *.js
89
+ });
90
+
91
+ test('globToRegExp - 问号通配符', () => {
92
+ const re = globToRegExp('test?.js');
93
+ assert(re.test('test1.js'), '应该匹配 test1.js');
94
+ assert(re.test('testA.js'), '应该匹配 testA.js');
95
+ assert(!re.test('test.js'), '不应该匹配 test.js');
96
+ assert(!re.test('test12.js'), '不应该匹配 test12.js');
97
+ });
98
+
99
+ test('matchesAny - 多个模式匹配', () => {
100
+ const patterns = ['*.json', 'src/**/*.ts', 'README.md'];
101
+ assert(matchesAny('package.json', patterns), '应该匹配 package.json');
102
+ // src/**/*.ts 匹配 src/ 后面跟任意路径再跟 .ts
103
+ assert(matchesAny('src/a/index.ts', patterns), '应该匹配 src/a/index.ts');
104
+ assert(matchesAny('src/utils/helper.ts', patterns), '应该匹配深层 ts 文件');
105
+ assert(matchesAny('README.md', patterns), '应该匹配 README.md');
106
+ assert(!matchesAny('test.js', patterns), '不应该匹配 test.js');
107
+ });
108
+
109
+ test('matchesAny - 空数组', () => {
110
+ assert(!matchesAny('test.js', []), '空数组不应该匹配任何文件');
111
+ assert(!matchesAny('test.js', null), 'null 不应该匹配');
112
+ });
113
+
114
+ test('normalizeRelPath - 路径标准化', () => {
115
+ if (path.sep === '\\') {
116
+ assertEqual(normalizeRelPath('src\\index.js'), 'src/index.js');
117
+ assertEqual(normalizeRelPath('a\\b\\c.txt'), 'a/b/c.txt');
118
+ } else {
119
+ assertEqual(normalizeRelPath('src/index.js'), 'src/index.js');
120
+ }
121
+ });
122
+
123
+ test('参数解析 - 基本目录', () => {
124
+ const parseArgs = (argv) => {
125
+ const args = argv.slice(2);
126
+ const opts = {
127
+ pre: false,
128
+ dir: '.',
129
+ template: null,
130
+ install: true,
131
+ start: true,
132
+ force: false,
133
+ pm: null,
134
+ conflict: 'keep'
135
+ };
136
+
137
+ for (let i = 0; i < args.length; i++) {
138
+ const a = args[i];
139
+ if ((a === 'pre' || a === 'preinstall' || a === 'preupdate') && opts.dir === '.' && !opts.pre) {
140
+ opts.pre = true;
141
+ continue;
142
+ }
143
+ if (!a.startsWith('-') && opts.dir === '.') {
144
+ opts.dir = a;
145
+ continue;
146
+ }
147
+ if (a === '-t' || a === '--template') {
148
+ opts.template = args[++i];
149
+ continue;
150
+ }
151
+ if (a === '--no-install') opts.install = false;
152
+ if (a === '--no-start') opts.start = false;
153
+ if (a === '--pre') opts.pre = true;
154
+ if (a === '--force') opts.force = true;
155
+ if (a === '--pm') opts.pm = args[++i];
156
+ if (a === '--conflict') {
157
+ const v = args[++i];
158
+ if (v === 'keep' || v === 'overwrite') opts.conflict = v;
159
+ }
160
+ }
161
+ return opts;
162
+ };
163
+
164
+ const opts1 = parseArgs(['node', 'index.js', 'my-project']);
165
+ assertEqual(opts1.dir, 'my-project');
166
+ assertEqual(opts1.install, true);
167
+
168
+ const opts2 = parseArgs(['node', 'index.js', '--no-install', '--no-start']);
169
+ assertEqual(opts2.install, false);
170
+ assertEqual(opts2.start, false);
171
+
172
+ const opts3 = parseArgs(['node', 'index.js', '--pm', 'pnpm']);
173
+ assertEqual(opts3.pm, 'pnpm');
174
+
175
+ const opts4 = parseArgs(['node', 'index.js', '--conflict', 'overwrite']);
176
+ assertEqual(opts4.conflict, 'overwrite');
177
+ });
178
+
179
+ // ============ 集成测试 ============
180
+
181
+ test('集成测试 - 创建临时项目', async () => {
182
+ const testDir = path.join(os.tmpdir(), `axhub-test-${Date.now()}`);
183
+
184
+ try {
185
+ await fs.ensureDir(testDir);
186
+
187
+ // 创建测试文件
188
+ await fs.writeFile(path.join(testDir, 'test.txt'), 'hello');
189
+
190
+ // 验证文件存在
191
+ const exists = await fs.pathExists(path.join(testDir, 'test.txt'));
192
+ assert(exists, '测试文件应该存在');
193
+
194
+ // 清理
195
+ await fs.remove(testDir);
196
+
197
+ const stillExists = await fs.pathExists(testDir);
198
+ assert(!stillExists, '测试目录应该被删除');
199
+ } catch (err) {
200
+ await fs.remove(testDir);
201
+ throw err;
202
+ }
203
+ });
204
+
205
+ test('文件比较 - filesEqual', async () => {
206
+ const testDir = path.join(os.tmpdir(), `axhub-test-${Date.now()}`);
207
+
208
+ try {
209
+ await fs.ensureDir(testDir);
210
+
211
+ const file1 = path.join(testDir, 'file1.txt');
212
+ const file2 = path.join(testDir, 'file2.txt');
213
+ const file3 = path.join(testDir, 'file3.txt');
214
+
215
+ await fs.writeFile(file1, 'same content');
216
+ await fs.writeFile(file2, 'same content');
217
+ await fs.writeFile(file3, 'different content');
218
+
219
+ const filesEqual = async (a, b) => {
220
+ try {
221
+ const [abuf, bbuf] = await Promise.all([fs.readFile(a), fs.readFile(b)]);
222
+ if (abuf.length !== bbuf.length) return false;
223
+ return abuf.equals(bbuf);
224
+ } catch {
225
+ return false;
226
+ }
227
+ };
228
+
229
+ assert(await filesEqual(file1, file2), 'file1 和 file2 应该相同');
230
+ assert(!(await filesEqual(file1, file3)), 'file1 和 file3 应该不同');
231
+
232
+ await fs.remove(testDir);
233
+ } catch (err) {
234
+ await fs.remove(testDir);
235
+ throw err;
236
+ }
237
+ });
238
+
239
+ // ============ 运行测试 ============
240
+
241
+ async function runTests() {
242
+ console.log(chalk.blue.bold('\n🧪 开始测试 Axhub Scaffold\n'));
243
+
244
+ for (const { name, fn } of tests) {
245
+ try {
246
+ await fn();
247
+ console.log(chalk.green(`✓ ${name}`));
248
+ passed++;
249
+ } catch (err) {
250
+ console.log(chalk.red(`✗ ${name}`));
251
+ console.log(chalk.red(` ${err.message}`));
252
+ failed++;
253
+ }
254
+ }
255
+
256
+ console.log(chalk.blue('\n' + '='.repeat(50)));
257
+ console.log(chalk.green(`通过: ${passed}`));
258
+ if (failed > 0) {
259
+ console.log(chalk.red(`失败: ${failed}`));
260
+ }
261
+ console.log(chalk.blue('='.repeat(50) + '\n'));
262
+
263
+ process.exit(failed > 0 ? 1 : 0);
264
+ }
265
+
266
+ runTests();