ai-git-tools 2.0.65 → 2.0.66

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.65",
3
+ "version": "2.0.66",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -108,13 +108,20 @@ export async function writeAndTestCommand(options = {}) {
108
108
 
109
109
  // 自動從 state 檔讀取 file / issue(指令列將覆蓋)
110
110
  const state = loadDevState();
111
- const filePath = options.file || state?.filePath;
111
+ let filePath = options.file || state?.filePath;
112
112
  const issueNumber = options.issue || state?.issueNumber;
113
- const fromState = !options.file && state?.filePath;
113
+ const fromState = !options.file && Boolean(state?.filePath);
114
114
  const maxFixes = parseInt(options.maxFixes || '2', 10);
115
115
 
116
+ // 若仍無 filePath,改用 git 偵測最近異動的原始碼檔案
117
+ let fromGit = false;
116
118
  if (!filePath) {
117
- logger.error('請提供原始碼路徑(--file <path>)或先執行 dev-from-issue');
119
+ filePath = await detectChangedSourceFile(logger);
120
+ fromGit = Boolean(filePath);
121
+ }
122
+
123
+ if (!filePath) {
124
+ logger.error('偵測不到異動的原始碼,請用 --file 指定目標檔案。');
118
125
  throw new Error('缺少原始碼路徑');
119
126
  }
120
127
 
@@ -124,8 +131,8 @@ export async function writeAndTestCommand(options = {}) {
124
131
  throw new Error(`找不到原始碼:${absoluteSourcePath}`);
125
132
  }
126
133
 
127
- // 自動從 state 讀取時,測試類型預設為 both
128
- const resolvedTestTypes = resolveRequestedTestTypes(absoluteSourcePath, options.testType || (fromState ? 'both' : 'auto'));
134
+ // 自動從 state 或 git 讀取時,測試類型預設為 both
135
+ const resolvedTestTypes = resolveRequestedTestTypes(absoluteSourcePath, options.testType || (fromState || fromGit ? 'both' : 'auto'));
129
136
  const testTargets = resolvedTestTypes.map((testType) => ({
130
137
  testType,
131
138
  testFilePath: deriveTestFilePath(absoluteSourcePath, testType),
@@ -134,6 +141,8 @@ export async function writeAndTestCommand(options = {}) {
134
141
  logger.header('write-and-test');
135
142
  if (fromState) {
136
143
  logger.info('從上一步 dev-from-issue 自動讀取');
144
+ } else if (fromGit) {
145
+ logger.info('從 git 異動記錄自動偵測');
137
146
  }
138
147
  console.log(`原始碼 :${absoluteSourcePath}`);
139
148
  console.log(`測試類型:${formatTestTypes(resolvedTestTypes)}`);
@@ -355,3 +364,62 @@ export function deriveTestFilePath(sourceFilePath, testType = 'unit') {
355
364
  const suffix = testType === 'component' ? 'component' : 'unit';
356
365
  return join(dir, '__tests__', `${name}.${suffix}.test${ext}`);
357
366
  }
367
+
368
+ /**
369
+ * 透過 git 偵測最近異動的原始碼檔案
370
+ * 優先順序:git staged → git unstaged → git 最新一筆 commit
371
+ * 排除測試檔、設定檔、lock 檔
372
+ */
373
+ async function detectChangedSourceFile(logger) {
374
+ const SOURCE_EXTENSIONS = /\.(js|jsx|ts|tsx|mjs|cjs)$/;
375
+ const EXCLUDED = /(__tests__|\.test\.|\.spec\.|\.config\.|node_modules|dist\/|\.lock$|package\.json)/;
376
+
377
+ function filterSourceFiles(files) {
378
+ return files
379
+ .split('\n')
380
+ .map((f) => f.trim().replace(/^[MADRCU?!\s]+/, ''))
381
+ .filter((f) => f && SOURCE_EXTENSIONS.test(f) && !EXCLUDED.test(f) && existsSync(resolve(process.cwd(), f)));
382
+ }
383
+
384
+ try {
385
+ // 1. staged 檔案
386
+ const staged = filterSourceFiles(
387
+ execSync('git diff --cached --name-only', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
388
+ );
389
+ if (staged.length > 0) {
390
+ logger.info(`偵測到 staged 異動:${staged[0]}`);
391
+ return staged[0];
392
+ }
393
+
394
+ // 2. unstaged 工作區異動
395
+ const unstaged = filterSourceFiles(
396
+ execSync('git diff --name-only', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
397
+ );
398
+ if (unstaged.length > 0) {
399
+ logger.info(`偵測到工作區異動:${unstaged[0]}`);
400
+ return unstaged[0];
401
+ }
402
+
403
+ // 3. untracked 新增檔案
404
+ const untracked = filterSourceFiles(
405
+ execSync('git ls-files --others --exclude-standard', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
406
+ );
407
+ if (untracked.length > 0) {
408
+ logger.info(`偵測到新增檔案:${untracked[0]}`);
409
+ return untracked[0];
410
+ }
411
+
412
+ // 4. 最新 commit 的異動
413
+ const lastCommit = filterSourceFiles(
414
+ execSync('git diff-tree --no-commit-id -r --name-only HEAD', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
415
+ );
416
+ if (lastCommit.length > 0) {
417
+ logger.info(`偵測到最後 commit 異動:${lastCommit[0]}`);
418
+ return lastCommit[0];
419
+ }
420
+ } catch {
421
+ // git 取得失敗(非 git 專案等),直接回傳 null
422
+ }
423
+
424
+ return null;
425
+ }