ai-git-tools 2.0.66 → 2.0.67

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.0.66",
3
+ "version": "2.0.67",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -7,7 +7,6 @@ import { resolve, basename, dirname, join } from 'path';
7
7
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
8
8
  import { execSync } from 'child_process';
9
9
  import { tmpdir } from 'os';
10
- import inquirer from 'inquirer';
11
10
  import { TestGenerator } from '../core/test-generator.js';
12
11
  import { TestRunner } from '../core/test-runner.js';
13
12
  import { AIClient } from '../core/ai-client.js';
@@ -108,69 +107,72 @@ export async function writeAndTestCommand(options = {}) {
108
107
 
109
108
  // 自動從 state 檔讀取 file / issue(指令列將覆蓋)
110
109
  const state = loadDevState();
111
- let filePath = options.file || state?.filePath;
112
110
  const issueNumber = options.issue || state?.issueNumber;
113
- const fromState = !options.file && Boolean(state?.filePath);
114
111
  const maxFixes = parseInt(options.maxFixes || '2', 10);
115
112
 
116
- // 若仍無 filePath,改用 git 偵測最近異動的原始碼檔案
113
+ // 決定要處理的檔案清單
114
+ let sourceFiles = [];
115
+ let fromState = false;
117
116
  let fromGit = false;
118
- if (!filePath) {
119
- filePath = await detectChangedSourceFile(logger);
120
- fromGit = Boolean(filePath);
117
+
118
+ if (options.file) {
119
+ // 明確指定單一檔案
120
+ sourceFiles = [options.file];
121
+ } else if (state?.filePath) {
122
+ // 從上一步 dev-from-issue 讀取
123
+ sourceFiles = [state.filePath];
124
+ fromState = true;
125
+ } else {
126
+ // 從 git 偵測所有異動的原始碼檔案
127
+ sourceFiles = await detectChangedSourceFiles(logger);
128
+ fromGit = sourceFiles.length > 0;
121
129
  }
122
130
 
123
- if (!filePath) {
131
+ if (sourceFiles.length === 0) {
124
132
  logger.error('偵測不到異動的原始碼,請用 --file 指定目標檔案。');
125
133
  throw new Error('缺少原始碼路徑');
126
134
  }
127
135
 
136
+ logger.header('write-and-test');
137
+ if (fromState) {
138
+ logger.info('從上一步 dev-from-issue 自動讀取');
139
+ } else if (fromGit) {
140
+ logger.info(`從 git 異動記錄偵測到 ${sourceFiles.length} 個檔案`);
141
+ }
142
+ console.log('');
143
+
144
+ const allResults = [];
145
+ for (const filePath of sourceFiles) {
146
+ const result = await processOneFile({ filePath, issueNumber, maxFixes, fromState, fromGit, options, logger });
147
+ allResults.push(result);
148
+ }
149
+
150
+ return allResults.length === 1 ? allResults[0] : allResults;
151
+ }
152
+
153
+ async function processOneFile({ filePath, issueNumber, maxFixes, fromState, fromGit, options, logger }) {
128
154
  const absoluteSourcePath = resolve(process.cwd(), filePath);
129
155
  if (!existsSync(absoluteSourcePath)) {
130
156
  logger.error(`原始碼不存在:${absoluteSourcePath}`);
131
157
  throw new Error(`找不到原始碼:${absoluteSourcePath}`);
132
158
  }
133
159
 
134
- // 自動從 state 或 git 讀取時,測試類型預設為 both
160
+ // 自動偵測時預設 both,手動指定時依 --test-type
135
161
  const resolvedTestTypes = resolveRequestedTestTypes(absoluteSourcePath, options.testType || (fromState || fromGit ? 'both' : 'auto'));
136
162
  const testTargets = resolvedTestTypes.map((testType) => ({
137
163
  testType,
138
164
  testFilePath: deriveTestFilePath(absoluteSourcePath, testType),
139
165
  }));
140
166
 
141
- logger.header('write-and-test');
142
- if (fromState) {
143
- logger.info('從上一步 dev-from-issue 自動讀取');
144
- } else if (fromGit) {
145
- logger.info('從 git 異動記錄自動偵測');
146
- }
147
- console.log(`原始碼 :${absoluteSourcePath}`);
148
- console.log(`測試類型:${formatTestTypes(resolvedTestTypes)}`);
167
+ console.log(`── ${filePath}`);
168
+ console.log(` 測試類型:${formatTestTypes(resolvedTestTypes)}`);
149
169
  testTargets.forEach(({ testType, testFilePath }) => {
150
- console.log(`${testType === 'component' ? '元件測試' : '單元測試'}:${testFilePath}`);
170
+ console.log(` ${testType === 'component' ? '元件測試' : '單元測試'}:${testFilePath}`);
151
171
  });
152
- console.log(`最大修復:${maxFixes} 次`);
153
- if (issueNumber) {
154
- console.log(`報告 Issue:#${issueNumber}`);
155
- }
156
172
  console.log('');
157
173
 
158
- // 1. 詢問是否繼續(自動從 state 讀取時跳過)
159
- if (!options.noConfirm && !fromState) {
160
- const { confirmed } = await inquirer.prompt([{
161
- type: 'confirm',
162
- name: 'confirmed',
163
- message: `確認為 ${filePath} 生成 ${formatTestTypes(resolvedTestTypes)} 並執行?`,
164
- default: true,
165
- }]);
166
-
167
- if (!confirmed) {
168
- logger.info('已取消。');
169
- return;
170
- }
171
- }
172
174
 
173
- // 2. 生成測試
175
+ // 1. 生成測試
174
176
  logger.step('正在生成測試代碼...');
175
177
  const generatedTests = [];
176
178
  for (const { testType, testFilePath } of testTargets) {
@@ -185,7 +187,7 @@ export async function writeAndTestCommand(options = {}) {
185
187
  }
186
188
  logger.info(`實際測試類型:${formatTestTypes(generatedTests.map(({ testType }) => testType))}`);
187
189
 
188
- // 3. 執行測試 + 自動修復循環
190
+ // 2. 執行測試 + 自動修復循環
189
191
  let lastErrors = [];
190
192
  for (let attempt = 0; attempt <= maxFixes; attempt++) {
191
193
  if (attempt > 0) {
@@ -366,16 +368,16 @@ export function deriveTestFilePath(sourceFilePath, testType = 'unit') {
366
368
  }
367
369
 
368
370
  /**
369
- * 透過 git 偵測最近異動的原始碼檔案
370
- * 優先順序:git staged → git unstaged → git 最新一筆 commit
371
+ * 透過 git 偵測所有異動的原始碼檔案(回傳陣列)
372
+ * 優先順序:staged → unstaged → untracked 最新 commit
371
373
  * 排除測試檔、設定檔、lock 檔
372
374
  */
373
- async function detectChangedSourceFile(logger) {
375
+ async function detectChangedSourceFiles(logger) {
374
376
  const SOURCE_EXTENSIONS = /\.(js|jsx|ts|tsx|mjs|cjs)$/;
375
377
  const EXCLUDED = /(__tests__|\.test\.|\.spec\.|\.config\.|node_modules|dist\/|\.lock$|package\.json)/;
376
378
 
377
- function filterSourceFiles(files) {
378
- return files
379
+ function filterSourceFiles(raw) {
380
+ return raw
379
381
  .split('\n')
380
382
  .map((f) => f.trim().replace(/^[MADRCU?!\s]+/, ''))
381
383
  .filter((f) => f && SOURCE_EXTENSIONS.test(f) && !EXCLUDED.test(f) && existsSync(resolve(process.cwd(), f)));
@@ -387,39 +389,34 @@ async function detectChangedSourceFile(logger) {
387
389
  execSync('git diff --cached --name-only', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
388
390
  );
389
391
  if (staged.length > 0) {
390
- logger.info(`偵測到 staged 異動:${staged[0]}`);
391
- return staged[0];
392
+ logger.info(`偵測到 staged 異動 ${staged.length} 個檔案`);
393
+ return staged;
392
394
  }
393
395
 
394
- // 2. unstaged 工作區異動
396
+ // 2. unstaged 工作區異動(含 untracked 合併)
395
397
  const unstaged = filterSourceFiles(
396
398
  execSync('git diff --name-only', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
397
399
  );
398
- if (unstaged.length > 0) {
399
- logger.info(`偵測到工作區異動:${unstaged[0]}`);
400
- return unstaged[0];
401
- }
402
-
403
- // 3. untracked 新增檔案
404
400
  const untracked = filterSourceFiles(
405
401
  execSync('git ls-files --others --exclude-standard', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
406
402
  );
407
- if (untracked.length > 0) {
408
- logger.info(`偵測到新增檔案:${untracked[0]}`);
409
- return untracked[0];
403
+ const working = [...new Set([...unstaged, ...untracked])];
404
+ if (working.length > 0) {
405
+ logger.info(`偵測到工作區異動 ${working.length} 個檔案`);
406
+ return working;
410
407
  }
411
408
 
412
- // 4. 最新 commit 的異動
409
+ // 3. 最新 commit 的異動
413
410
  const lastCommit = filterSourceFiles(
414
411
  execSync('git diff-tree --no-commit-id -r --name-only HEAD', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
415
412
  );
416
413
  if (lastCommit.length > 0) {
417
- logger.info(`偵測到最後 commit 異動:${lastCommit[0]}`);
418
- return lastCommit[0];
414
+ logger.info(`偵測到最後 commit 異動 ${lastCommit.length} 個檔案`);
415
+ return lastCommit;
419
416
  }
420
417
  } catch {
421
- // git 取得失敗(非 git 專案等),直接回傳 null
418
+ // git 取得失敗,直接回傳空陣列
422
419
  }
423
420
 
424
- return null;
421
+ return [];
425
422
  }