ai-engineering-init 1.2.1 → 1.2.2

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 (3) hide show
  1. package/README.md +8 -1
  2. package/bin/index.js +192 -110
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -89,6 +89,12 @@ cd ai-engineering-init
89
89
 
90
90
  > **MCP Server 支持**:Codex CLI 可通过 `codex mcp-server` 作为 MCP Server 暴露给 Claude Code,
91
91
  > 配置写入 `.claude/settings.json` 的 `mcpServers` 后,Claude 可直接调用 `codex` / `codex-reply` 工具进行代码审查。
92
+ >
93
+ > **Windows 用户注意**:初始化后需将 `.claude/settings.json` 中 `mcpServers.codex.command` 的路径改为 Windows 上的实际路径,例如:
94
+ > ```json
95
+ > "command": "C:\\Users\\YourName\\AppData\\Roaming\\npm\\codex.cmd"
96
+ > ```
97
+ > 可通过 `where codex` 命令查询实际路径。
92
98
 
93
99
  ## Skills 列表(69个)
94
100
 
@@ -192,12 +198,13 @@ cd ai-engineering-init
192
198
  1. 修改 `AGENTS.md` 中的项目说明
193
199
  2. 使用 `.codex/skills/` 下的技能辅助开发
194
200
  3. (可选)以 MCP Server 接入 Claude Code:`.claude/settings.json` → `mcpServers.codex`,重启后 Claude 可直接调用 `codex` / `codex-reply` 工具
201
+ - **Windows 用户**:将 `command` 路径改为 `where codex` 查询到的实际路径(如 `C:\Users\YourName\AppData\Roaming\npm\codex.cmd`)
195
202
 
196
203
  ## 更新日志
197
204
 
198
205
  查看完整更新历史:[CHANGELOG.md](./CHANGELOG.md)
199
206
 
200
- **v1.2.1 新增**:`update` 命令一键更新框架文件;Codex MCP Server 集成;拆分独立 CHANGELOG.md。
207
+ **v1.2.2 新增**:README 补充 Windows MCP Server 路径配置说明;Windows 兼容性分析与说明。
201
208
 
202
209
  ## License
203
210
 
package/bin/index.js CHANGED
@@ -14,13 +14,22 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
  const readline = require('readline');
16
16
 
17
- // ── ANSI 颜色 ──────────────────────────────────────────────────────────────
18
- const c = {
17
+ // ── ANSI 颜色(Windows CMD/PowerShell 兼容)────────────────────────────────
18
+ // Windows Terminal 设置 WT_SESSION,ConEmu/Cmder 设置 COLORTERM,VSCode 设置 TERM_PROGRAM
19
+ const supportsColor = !!process.stdout.isTTY && (
20
+ process.platform !== 'win32' ||
21
+ !!process.env.WT_SESSION ||
22
+ !!process.env.COLORTERM ||
23
+ process.env.TERM_PROGRAM === 'vscode'
24
+ );
25
+ const ESC = supportsColor ? {
19
26
  reset: '\x1b[0m', bold: '\x1b[1m',
20
27
  red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
21
28
  blue: '\x1b[34m', cyan: '\x1b[36m', magenta: '\x1b[35m',
22
- };
23
- const fmt = (color, text) => `${c[color]}${text}${c.reset}`;
29
+ } : Object.fromEntries(
30
+ ['reset','bold','red','green','yellow','blue','cyan','magenta'].map(k => [k, ''])
31
+ );
32
+ const fmt = (color, text) => `${ESC[color]}${text}${ESC.reset}`;
24
33
 
25
34
  // ── 版本 ───────────────────────────────────────────────────────────────────
26
35
  const PKG_VERSION = require('../package.json').version;
@@ -34,27 +43,52 @@ console.log('');
34
43
 
35
44
  // ── 参数解析 ───────────────────────────────────────────────────────────────
36
45
  const args = process.argv.slice(2);
37
- let command = ''; // 'init' | 'update'
46
+ let command = ''; // 'update' | ''
38
47
  let tool = '';
39
48
  let targetDir = process.cwd();
40
49
  let force = false;
41
50
 
42
51
  for (let i = 0; i < args.length; i++) {
43
- switch (args[i]) {
44
- case 'update': command = 'update'; break;
45
- case '--tool': case '-t': tool = args[++i]; break;
46
- case '--dir': case '-d': targetDir = path.resolve(args[++i]); break;
47
- case '--force':case '-f': force = true; break;
52
+ const arg = args[i];
53
+ switch (arg) {
54
+ case 'update':
55
+ command = 'update';
56
+ break;
57
+ case '--tool': case '-t':
58
+ if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
59
+ console.error(fmt('red', `错误:${arg} 需要一个值(claude | cursor | codex | all)`));
60
+ process.exit(1);
61
+ }
62
+ tool = args[++i];
63
+ break;
64
+ case '--dir': case '-d':
65
+ if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
66
+ console.error(fmt('red', `错误:${arg} 需要一个目录路径`));
67
+ process.exit(1);
68
+ }
69
+ targetDir = path.resolve(args[++i]);
70
+ break;
71
+ case '--force': case '-f':
72
+ force = true;
73
+ break;
48
74
  case '--help': case '-h':
49
75
  printHelp();
50
76
  process.exit(0);
77
+ break;
78
+ default:
79
+ // 拒绝未知选项,避免静默忽略导致行为不符预期
80
+ if (arg.startsWith('-')) {
81
+ console.error(fmt('red', `错误:未知选项 "${arg}",运行 --help 查看用法`));
82
+ process.exit(1);
83
+ }
84
+ break;
51
85
  }
52
86
  }
53
87
 
54
88
  function printHelp() {
55
89
  console.log(`用法: ${fmt('bold', 'npx ai-engineering-init')} [命令] [选项]\n`);
56
90
  console.log('命令:');
57
- console.log(` ${fmt('bold', '(无)') } 交互式初始化`);
91
+ console.log(` ${fmt('bold', '(无)')} 交互式初始化`);
58
92
  console.log(` ${fmt('bold', 'update')} 更新已安装的框架文件(跳过用户自定义文件)\n`);
59
93
  console.log('选项:');
60
94
  console.log(' --tool, -t <工具> 指定工具: claude | cursor | codex | all');
@@ -69,7 +103,7 @@ function printHelp() {
69
103
  console.log(' npx ai-engineering-init update --force # 强制更新,包括保留文件\n');
70
104
  }
71
105
 
72
- // ── 工具定义(init 用) ──────────────────────────────────────────────────────
106
+ // ── 工具定义(init 用)────────────────────────────────────────────────────
73
107
  const TOOLS = {
74
108
  claude: {
75
109
  label: 'Claude Code',
@@ -93,19 +127,19 @@ const TOOLS = {
93
127
  },
94
128
  };
95
129
 
96
- // ── 更新规则(update 用) ────────────────────────────────────────────────────
97
- // update: 框架文件,始终从 npm 包最新版本覆盖
98
- // preserve: 用户自定义文件,默认跳过(--force 时强制更新)
130
+ // ── 更新规则(update 用)──────────────────────────────────────────────────
131
+ // update: 框架文件,从本机安装版本覆盖
132
+ // preserve: 用户自定义文件,默认跳过(--force 时强制覆盖)
99
133
  const UPDATE_RULES = {
100
134
  claude: {
101
135
  label: 'Claude Code',
102
- detect: '.claude', // 检测目录,判断是否已安装
136
+ detect: '.claude',
103
137
  update: [
104
- { src: '.claude/skills', dest: '.claude/skills', label: 'Skills(技能库)', isDir: true },
105
- { src: '.claude/commands', dest: '.claude/commands', label: 'Commands(快捷命令)', isDir: true },
106
- { src: '.claude/agents', dest: '.claude/agents', label: 'Agents(子代理)', isDir: true },
107
- { src: '.claude/hooks', dest: '.claude/hooks', label: 'Hooks(钩子脚本)', isDir: true },
108
- { src: '.claude/templates', dest: '.claude/templates', label: 'Templates', isDir: true },
138
+ { src: '.claude/skills', dest: '.claude/skills', label: 'Skills(技能库)', isDir: true },
139
+ { src: '.claude/commands', dest: '.claude/commands', label: 'Commands(快捷命令)', isDir: true },
140
+ { src: '.claude/agents', dest: '.claude/agents', label: 'Agents(子代理)', isDir: true },
141
+ { src: '.claude/hooks', dest: '.claude/hooks', label: 'Hooks(钩子脚本)', isDir: true },
142
+ { src: '.claude/templates', dest: '.claude/templates', label: 'Templates', isDir: true },
109
143
  { src: '.claude/framework-config.json', dest: '.claude/framework-config.json', label: 'framework-config.json' },
110
144
  ],
111
145
  preserve: [
@@ -117,9 +151,9 @@ const UPDATE_RULES = {
117
151
  label: 'Cursor',
118
152
  detect: '.cursor',
119
153
  update: [
120
- { src: '.cursor/skills', dest: '.cursor/skills', label: 'Skills(技能库)', isDir: true },
121
- { src: '.cursor/agents', dest: '.cursor/agents', label: 'Agents(子代理)', isDir: true },
122
- { src: '.cursor/hooks', dest: '.cursor/hooks', label: 'Hooks(钩子脚本)', isDir: true },
154
+ { src: '.cursor/skills', dest: '.cursor/skills', label: 'Skills(技能库)', isDir: true },
155
+ { src: '.cursor/agents', dest: '.cursor/agents', label: 'Agents(子代理)', isDir: true },
156
+ { src: '.cursor/hooks', dest: '.cursor/hooks', label: 'Hooks(钩子脚本)', isDir: true },
123
157
  { src: '.cursor/hooks.json', dest: '.cursor/hooks.json', label: 'hooks.json(Hooks 配置)' },
124
158
  ],
125
159
  preserve: [
@@ -138,32 +172,50 @@ const UPDATE_RULES = {
138
172
  },
139
173
  };
140
174
 
141
- // ── 公共工具函数 ───────────────────────────────────────────────────────────
175
+ // ── 公共工具函数 ──────────────────────────────────────────────────────────
142
176
  const SOURCE_DIR = path.join(__dirname, '..');
143
177
 
144
- /** 统计目录中的文件总数 */
145
- function countFiles(dir) {
146
- if (!fs.existsSync(dir)) return 0;
147
- let count = 0;
148
- for (const entry of fs.readdirSync(dir)) {
149
- const full = path.join(dir, entry);
150
- count += fs.statSync(full).isDirectory() ? countFiles(full) : 1;
151
- }
152
- return count;
178
+ /** 安全判断路径是否为真实目录(避免 existsSync 将文件误判为已安装目录) */
179
+ function isRealDir(p) {
180
+ try { return fs.statSync(p).isDirectory(); } catch { return false; }
153
181
  }
154
182
 
155
- /** 递归复制目录 */
183
+ /** 递归复制目录,返回实际写入的文件数;单文件失败不中断整体 */
156
184
  function copyDir(src, dest) {
157
- if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
158
- for (const entry of fs.readdirSync(src)) {
185
+ let written = 0;
186
+ try {
187
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
188
+ } catch (e) {
189
+ console.log(` ${fmt('red', '✗')} 无法创建目录 ${dest}: ${e.message}`);
190
+ return written;
191
+ }
192
+ let entries;
193
+ try {
194
+ entries = fs.readdirSync(src);
195
+ } catch (e) {
196
+ console.log(` ${fmt('red', '✗')} 无法读取源目录 ${src}: ${e.message}`);
197
+ return written;
198
+ }
199
+ for (const entry of entries) {
159
200
  const s = path.join(src, entry);
160
201
  const d = path.join(dest, entry);
161
- fs.statSync(s).isDirectory() ? copyDir(s, d) : fs.copyFileSync(s, d);
202
+ try {
203
+ fs.statSync(s).isDirectory() ? (written += copyDir(s, d)) : (fs.copyFileSync(s, d), written++);
204
+ } catch (e) {
205
+ console.log(` ${fmt('yellow', '⚠')} 跳过文件 ${d}: ${e.message}`);
206
+ }
162
207
  }
208
+ return written;
209
+ }
210
+
211
+ /** 构建包含 --dir 上下文的提示命令(方便用户直接复制执行) */
212
+ function hintCmd(subCmd) {
213
+ const dirPart = targetDir !== process.cwd() ? ` --dir "${targetDir}"` : '';
214
+ return `npx ai-engineering-init${dirPart} ${subCmd}`;
163
215
  }
164
216
 
165
- // ── INIT 逻辑 ──────────────────────────────────────────────────────────────
166
- function copyItem({ src, dest, label, isDir }) {
217
+ // ── INIT 逻辑 ─────────────────────────────────────────────────────────────
218
+ function copyItem({ src, dest, label, isDir: srcIsDir }) {
167
219
  const srcPath = path.join(SOURCE_DIR, src);
168
220
  const destPath = path.join(targetDir, dest);
169
221
 
@@ -175,13 +227,25 @@ function copyItem({ src, dest, label, isDir }) {
175
227
  console.log(` ${fmt('yellow', '⚠')} ${label} 已存在,跳过(--force 可强制覆盖)`);
176
228
  return;
177
229
  }
178
- isDir ? copyDir(srcPath, destPath) : fs.copyFileSync(srcPath, destPath);
179
- console.log(` ${fmt('green', '✓')} ${label}`);
230
+ try {
231
+ if (srcIsDir) {
232
+ const n = copyDir(srcPath, destPath);
233
+ console.log(` ${fmt('green', '✓')} ${label} ${fmt('magenta', `(${n} 个文件)`)}`);
234
+ } else {
235
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
236
+ fs.copyFileSync(srcPath, destPath);
237
+ console.log(` ${fmt('green', '✓')} ${label}`);
238
+ }
239
+ } catch (e) {
240
+ console.log(` ${fmt('red', '✗')} ${label} 复制失败: ${e.message}`);
241
+ }
180
242
  }
181
243
 
182
244
  function initTool(toolKey) {
183
245
  const t = TOOLS[toolKey];
184
246
  console.log(fmt('cyan', `[${t.label}]`));
247
+ // 确保目标根目录存在(兼容 --dir 指向尚不存在的路径)
248
+ try { fs.mkdirSync(targetDir, { recursive: true }); } catch { /* 已存在忽略 */ }
185
249
  for (const f of t.files) copyItem(f);
186
250
  }
187
251
 
@@ -212,8 +276,8 @@ function showDoneHint(toolKey) {
212
276
  }
213
277
 
214
278
  function run(selectedTool) {
215
- if (!['claude', 'cursor', 'codex', 'all'].includes(selectedTool)) {
216
- console.error(fmt('red', `无效工具: ${selectedTool}。有效选项: claude | cursor | codex | all`));
279
+ if (!Object.keys(TOOLS).concat('all').includes(selectedTool)) {
280
+ console.error(fmt('red', `无效工具: "${selectedTool}"。有效选项: claude | cursor | codex | all`));
217
281
  process.exit(1);
218
282
  }
219
283
  console.log(` 目标目录: ${fmt('bold', targetDir)}`);
@@ -223,30 +287,28 @@ function run(selectedTool) {
223
287
  console.log('');
224
288
 
225
289
  if (selectedTool === 'all') {
226
- initTool('claude'); console.log(''); initTool('cursor'); console.log(''); initTool('codex');
290
+ Object.keys(TOOLS).forEach((k, i) => { if (i) console.log(''); initTool(k); });
227
291
  } else {
228
292
  initTool(selectedTool);
229
293
  }
230
294
  showDoneHint(selectedTool);
231
295
  }
232
296
 
233
- // ── UPDATE 逻辑 ─────────────────────────────────────────────────────────────
297
+ // ── UPDATE 逻辑 ───────────────────────────────────────────────────────────
234
298
 
235
- /** 检测当前目录已安装了哪些工具 */
299
+ /** 检测当前目录已安装了哪些工具(用 isRealDir 排除误判) */
236
300
  function detectInstalledTools() {
237
- return Object.keys(UPDATE_RULES).filter(key => {
238
- const detectPath = path.join(targetDir, UPDATE_RULES[key].detect);
239
- return fs.existsSync(detectPath);
240
- });
301
+ return Object.keys(UPDATE_RULES).filter(key =>
302
+ isRealDir(path.join(targetDir, UPDATE_RULES[key].detect))
303
+ );
241
304
  }
242
305
 
243
- /** 更新单个工具的框架文件 */
306
+ /** 更新单个工具,返回 { updated, failed, preserved } 文件数 */
244
307
  function updateTool(toolKey) {
245
308
  const rule = UPDATE_RULES[toolKey];
246
309
  console.log(fmt('cyan', `[${rule.label}]`));
247
310
 
248
- let updatedCount = 0;
249
- let skippedCount = 0;
311
+ let updated = 0, failed = 0, preserved = 0;
250
312
 
251
313
  // 更新框架文件
252
314
  for (const item of rule.update) {
@@ -257,16 +319,21 @@ function updateTool(toolKey) {
257
319
  console.log(` ${fmt('yellow', '⚠')} ${item.label} 源文件不存在,跳过`);
258
320
  continue;
259
321
  }
260
-
261
- const fileCount = item.isDir ? countFiles(srcPath) : 1;
262
- if (item.isDir) {
263
- copyDir(srcPath, destPath);
264
- } else {
265
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
266
- fs.copyFileSync(srcPath, destPath);
322
+ try {
323
+ if (item.isDir) {
324
+ const n = copyDir(srcPath, destPath);
325
+ console.log(` ${fmt('green', '✓')} ${item.label} ${fmt('magenta', `(${n} 个文件)`)}`);
326
+ updated += n;
327
+ } else {
328
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
329
+ fs.copyFileSync(srcPath, destPath);
330
+ console.log(` ${fmt('green', '✓')} ${item.label}`);
331
+ updated++;
332
+ }
333
+ } catch (e) {
334
+ console.log(` ${fmt('red', '✗')} ${item.label} 失败: ${e.message}`);
335
+ failed++;
267
336
  }
268
- console.log(` ${fmt('green', '✓')} ${item.label} ${fmt('magenta', `(${fileCount} 个文件)`)}`);
269
- updatedCount += fileCount;
270
337
  }
271
338
 
272
339
  // 处理保留文件
@@ -275,100 +342,115 @@ function updateTool(toolKey) {
275
342
  const srcPath = path.join(SOURCE_DIR, item.dest);
276
343
 
277
344
  if (force) {
278
- // --force 时强制更新保留文件
279
345
  if (fs.existsSync(srcPath)) {
280
- const stat = fs.statSync(srcPath);
281
- if (stat.isDirectory()) {
282
- copyDir(srcPath, destPath);
283
- } else {
284
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
285
- fs.copyFileSync(srcPath, destPath);
346
+ try {
347
+ if (isRealDir(srcPath)) {
348
+ const n = copyDir(srcPath, destPath);
349
+ updated += n;
350
+ } else {
351
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
352
+ fs.copyFileSync(srcPath, destPath);
353
+ updated++;
354
+ }
355
+ console.log(` ${fmt('green', '✓')} ${item.dest} ${fmt('yellow', '(强制更新)')}`);
356
+ } catch (e) {
357
+ console.log(` ${fmt('red', '✗')} ${item.dest} 强制更新失败: ${e.message}`);
358
+ failed++;
286
359
  }
287
- console.log(` ${fmt('green', '✓')} ${item.dest} ${fmt('yellow', '(强制更新)')}`);
288
- updatedCount++;
289
360
  }
290
361
  } else {
291
- if (fs.existsSync(destPath)) {
292
- console.log(` ${fmt('yellow', '')} ${item.dest} ${fmt('yellow', `已保留`)} — ${item.reason}`);
293
- skippedCount++;
294
- }
362
+ const exists = fs.existsSync(destPath);
363
+ const mark = exists ? fmt('yellow', '已保留') : fmt('yellow', '不存在,跳过');
364
+ console.log(` ${fmt('yellow', '⊘')} ${item.dest} ${mark} — ${item.reason}`);
365
+ if (exists) preserved++;
295
366
  }
296
367
  }
297
368
 
298
- return { updatedCount, skippedCount };
369
+ return { updated, failed, preserved };
299
370
  }
300
371
 
301
372
  /** update 命令主流程 */
302
373
  function runUpdate(selectedTool) {
303
374
  console.log(` 目标目录: ${fmt('bold', targetDir)}`);
304
- console.log(` 来源版本: ${fmt('bold', `v${PKG_VERSION}`)} (npm latest)`);
305
- if (force) console.log(` ${fmt('yellow', '⚠ --force 模式:将同时更新用户保留文件')}`);
375
+ console.log(` 本机版本: ${fmt('bold', `v${PKG_VERSION}`)}`);
376
+ if (force) console.log(` ${fmt('yellow', '⚠ --force 模式:将同时更新保留文件')}`);
306
377
  console.log('');
307
378
 
308
- // 确定要更新的工具列表
309
379
  let toolsToUpdate = [];
310
- if (selectedTool && selectedTool !== 'all') {
380
+
381
+ if (!selectedTool || selectedTool === 'all') {
382
+ // 无参数 或 all:只更新已检测到的工具(不主动创建新目录)
383
+ toolsToUpdate = detectInstalledTools();
384
+ if (toolsToUpdate.length === 0) {
385
+ console.log(fmt('yellow', '⚠ 当前目录未检测到已安装的 AI 工具配置。'));
386
+ console.log(` 请先运行: ${fmt('bold', hintCmd('--tool claude'))}\n`);
387
+ process.exit(1);
388
+ }
389
+ console.log(` 检测到已安装: ${fmt('bold', toolsToUpdate.join(', '))}`);
390
+ if (selectedTool === 'all') {
391
+ console.log(` ${fmt('yellow', '提示')}:--tool all 只更新已安装工具,如需初始化新工具请用 --tool <name>`);
392
+ }
393
+ } else {
394
+ // 指定单个工具
311
395
  if (!UPDATE_RULES[selectedTool]) {
312
- console.error(fmt('red', `无效工具: ${selectedTool}。有效选项: claude | cursor | codex | all`));
396
+ console.error(fmt('red', `无效工具: "${selectedTool}"。有效选项: claude | cursor | codex | all`));
313
397
  process.exit(1);
314
398
  }
315
- const detectPath = path.join(targetDir, UPDATE_RULES[selectedTool].detect);
316
- if (!fs.existsSync(detectPath)) {
399
+ if (!isRealDir(path.join(targetDir, UPDATE_RULES[selectedTool].detect))) {
317
400
  console.log(fmt('yellow', `⚠ ${selectedTool} 未在当前目录初始化,请先运行:`));
318
- console.log(` ${fmt('bold', `npx ai-engineering-init --tool ${selectedTool}`)}\n`);
401
+ console.log(` ${fmt('bold', hintCmd(`--tool ${selectedTool}`))}\n`);
319
402
  process.exit(1);
320
403
  }
321
404
  toolsToUpdate = [selectedTool];
322
- } else if (selectedTool === 'all') {
323
- toolsToUpdate = Object.keys(UPDATE_RULES);
324
- } else {
325
- // 自动检测
326
- toolsToUpdate = detectInstalledTools();
327
- if (toolsToUpdate.length === 0) {
328
- console.log(fmt('yellow', '⚠ 当前目录未检测到已安装的 AI 工具配置。'));
329
- console.log(` 请先运行 ${fmt('bold', 'npx ai-engineering-init')} 进行初始化。\n`);
330
- process.exit(1);
331
- }
332
- console.log(` 检测到已安装: ${fmt('bold', toolsToUpdate.join(', '))}`);
333
- console.log('');
334
405
  }
335
406
 
407
+ console.log('');
336
408
  console.log(fmt('bold', '正在更新框架文件...'));
337
409
  console.log('');
338
410
 
339
- let totalUpdated = 0;
340
- let totalSkipped = 0;
341
-
411
+ let totalUpdated = 0, totalFailed = 0, totalPreserved = 0;
342
412
  for (let i = 0; i < toolsToUpdate.length; i++) {
343
- const { updatedCount, skippedCount } = updateTool(toolsToUpdate[i]);
344
- totalUpdated += updatedCount;
345
- totalSkipped += skippedCount;
413
+ const { updated, failed, preserved } = updateTool(toolsToUpdate[i]);
414
+ totalUpdated += updated;
415
+ totalFailed += failed;
416
+ totalPreserved += preserved;
346
417
  if (i < toolsToUpdate.length - 1) console.log('');
347
418
  }
348
419
 
349
- // 汇总
350
420
  console.log('');
351
421
  console.log(fmt('green', fmt('bold', '✅ 更新完成!')));
352
422
  console.log('');
353
- console.log(` ${fmt('green', `✓ 更新文件: ${totalUpdated} 个`)}`);
354
- if (totalSkipped > 0) {
355
- console.log(` ${fmt('yellow', `⊘ 已保留文件: ${totalSkipped} 个`)}(用户自定义,使用 --force 可强制更新)`);
423
+ console.log(` ${fmt('green', `✓ 更新文件: ${totalUpdated} 个`)}`);
424
+ if (totalFailed > 0) {
425
+ console.log(` ${fmt('red', `✗ 失败文件: ${totalFailed} 个`)}(请检查目录权限)`);
426
+ }
427
+ if (totalPreserved > 0) {
428
+ console.log(` ${fmt('yellow', `⊘ 已保留文件: ${totalPreserved} 个`)}(--force 可强制更新)`);
356
429
  }
357
430
  console.log('');
358
431
  console.log(fmt('cyan', '提示:'));
359
432
  console.log(' 重启 Claude Code / Cursor 使新技能生效');
360
- if (!force && totalSkipped > 0) {
361
- console.log(` 如需同步保留文件,运行: ${fmt('bold', `npx ai-engineering-init update --force`)}`);
433
+ console.log(` ${fmt('yellow', '注意')}:update 只新增/覆盖文件,不删除旧版本已移除的文件`);
434
+ if (!force && totalPreserved > 0) {
435
+ console.log(` 强制更新保留文件: ${fmt('bold', hintCmd('update --force'))}`);
362
436
  }
363
437
  console.log('');
438
+
439
+ if (totalFailed > 0) process.exitCode = 1;
364
440
  }
365
441
 
366
- // ── 主入口 ─────────────────────────────────────────────────────────────────
442
+ // ── 主入口 ────────────────────────────────────────────────────────────────
367
443
  if (command === 'update') {
368
444
  runUpdate(tool);
369
445
  } else if (tool) {
370
446
  run(tool);
371
447
  } else {
448
+ // 非 TTY 环境(CI/管道)无法交互,强制要求显式指定 --tool
449
+ if (!process.stdin.isTTY) {
450
+ console.error(fmt('red', '错误:非交互环境下必须指定 --tool 参数'));
451
+ console.error(` 示例: ${fmt('bold', hintCmd('--tool claude'))}`);
452
+ process.exit(1);
453
+ }
372
454
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
373
455
  console.log(fmt('cyan', '请选择要初始化的 AI 工具:'));
374
456
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-engineering-init",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "AI 工程化配置初始化工具 — 一键为 Claude Code、OpenAI Codex 等 AI 工具初始化 Skills 和项目规范",
5
5
  "keywords": [
6
6
  "claude-code",