ai-git-tools 2.0.60 → 2.0.61

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.60",
3
+ "version": "2.0.61",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -12,7 +12,8 @@ import { IssueReader } from '../core/issue-reader.js';
12
12
  import { CodeGenerator } from '../core/code-generator.js';
13
13
  import { AIClient } from '../core/ai-client.js';
14
14
  import { GitOperations } from '../core/git-operations.js';
15
- import { writeAndTestCommand } from './write-and-test.js';
15
+ import { TestGenerator } from '../core/test-generator.js';
16
+ import { writeAndTestCommand, TEST_TYPE_CHOICES, normalizeTestType } from './write-and-test.js';
16
17
  import { Logger } from '../utils/logger.js';
17
18
 
18
19
  export async function autoDevCommand(options = {}) {
@@ -107,7 +108,7 @@ export async function autoDevCommand(options = {}) {
107
108
  try {
108
109
  codeResult = await CodeGenerator.generateFile(issue, absoluteFilePath, {
109
110
  maxLines: parseInt(options.maxLines || '500', 10),
110
- language: filePath.endsWith('.ts') ? 'ts' : 'js',
111
+ language: getLanguageFromFilePath(filePath),
111
112
  extraContext: options.context || '',
112
113
  model: options.model,
113
114
  });
@@ -119,8 +120,15 @@ export async function autoDevCommand(options = {}) {
119
120
 
120
121
  console.log('');
121
122
 
123
+ const selectedTestType = await resolveTestTypeForAutoDev({
124
+ noConfirm,
125
+ requestedTestType: options.testType,
126
+ });
127
+
128
+ const resolvedRuntimeTestType = resolveRuntimeTestType(absoluteFilePath, selectedTestType);
129
+
122
130
  logger.step('正在驗證專案可用的測試腳本...');
123
- ensureJestAvailable();
131
+ ensureJestAvailable(resolvedRuntimeTestType);
124
132
  logger.success('已確認可執行 Jest 測試。');
125
133
  console.log('');
126
134
 
@@ -129,7 +137,7 @@ export async function autoDevCommand(options = {}) {
129
137
  // ============================================================
130
138
  const testResult = await writeAndTestCommand({
131
139
  file: filePath,
132
- testType: options.testType || 'auto',
140
+ testType: selectedTestType,
133
141
  maxFixes: options.maxFixes || '2',
134
142
  model: options.model,
135
143
  noConfirm: true,
@@ -244,26 +252,47 @@ function isValidTargetPath(value) {
244
252
  && !value.startsWith('/');
245
253
  }
246
254
 
247
- function ensureJestAvailable() {
255
+ function getLanguageFromFilePath(filePath) {
256
+ return /\.(ts|tsx)$/.test(filePath) ? 'ts' : 'js';
257
+ }
258
+
259
+ async function resolveTestTypeForAutoDev({ noConfirm, requestedTestType }) {
260
+ if (noConfirm) {
261
+ return normalizeTestType(requestedTestType || 'auto');
262
+ }
263
+
264
+ const { testType } = await inquirer.prompt([{
265
+ type: 'list',
266
+ name: 'testType',
267
+ message: '請選擇這次要執行的測試方式:',
268
+ choices: TEST_TYPE_CHOICES,
269
+ default: normalizeTestType(requestedTestType || 'auto'),
270
+ }]);
271
+
272
+ return normalizeTestType(testType);
273
+ }
274
+
275
+ function resolveRuntimeTestType(sourceFilePath, requestedTestType) {
276
+ if (requestedTestType !== 'auto') {
277
+ return requestedTestType;
278
+ }
279
+
280
+ const sourceCode = readFileSync(sourceFilePath, 'utf-8');
281
+ return TestGenerator.resolveTestType(sourceFilePath, sourceCode, 'auto');
282
+ }
283
+
284
+ function ensureJestAvailable(runtimeTestType = 'unit') {
285
+ const packageJson = readProjectPackageJson();
286
+
248
287
  try {
249
288
  execSync('npx jest --version', {
250
289
  encoding: 'utf-8',
251
290
  stdio: 'pipe',
252
291
  cwd: process.cwd(),
253
292
  });
293
+ ensureComponentTestDependencies(packageJson, runtimeTestType);
294
+ return;
254
295
  } catch {
255
- const packageJsonPath = resolve(process.cwd(), 'package.json');
256
- if (!existsSync(packageJsonPath)) {
257
- throw new Error('目前目錄沒有 package.json,無法執行 Jest 測試。');
258
- }
259
-
260
- let packageJson;
261
- try {
262
- packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
263
- } catch {
264
- throw new Error('無法讀取目前專案的 package.json,請確認 Jest 已安裝。');
265
- }
266
-
267
296
  const hasJestDependency = Boolean(
268
297
  packageJson.dependencies?.jest || packageJson.devDependencies?.jest
269
298
  );
@@ -272,6 +301,40 @@ function ensureJestAvailable() {
272
301
  throw new Error('目前專案未安裝 Jest,請先安裝後再執行 auto-dev。');
273
302
  }
274
303
 
304
+ ensureComponentTestDependencies(packageJson, runtimeTestType);
305
+
275
306
  throw new Error('偵測到 Jest 依賴,但無法執行 npx jest,請檢查安裝或 lockfile 狀態。');
276
307
  }
277
308
  }
309
+
310
+ function readProjectPackageJson() {
311
+ const packageJsonPath = resolve(process.cwd(), 'package.json');
312
+ if (!existsSync(packageJsonPath)) {
313
+ throw new Error('目前目錄沒有 package.json,無法執行 Jest 測試。');
314
+ }
315
+
316
+ try {
317
+ return JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
318
+ } catch {
319
+ throw new Error('無法讀取目前專案的 package.json,請確認 Jest 已安裝。');
320
+ }
321
+ }
322
+
323
+ function ensureComponentTestDependencies(packageJson, runtimeTestType) {
324
+ if (runtimeTestType !== 'component') {
325
+ return;
326
+ }
327
+
328
+ const hasJsdomDependency = Boolean(
329
+ packageJson.devDependencies?.['jest-environment-jsdom']
330
+ || packageJson.dependencies?.['jest-environment-jsdom']
331
+ );
332
+ const hasTestingLibraryDependency = Boolean(
333
+ packageJson.devDependencies?.['@testing-library/react']
334
+ || packageJson.dependencies?.['@testing-library/react']
335
+ );
336
+
337
+ if (!hasJsdomDependency || !hasTestingLibraryDependency) {
338
+ throw new Error('元件測試需要 jest-environment-jsdom 與 @testing-library/react,請先安裝後再執行 auto-dev。');
339
+ }
340
+ }
@@ -8,6 +8,7 @@ import { resolve } from 'path';
8
8
  import inquirer from 'inquirer';
9
9
  import { IssueReader } from '../core/issue-reader.js';
10
10
  import { CodeGenerator } from '../core/code-generator.js';
11
+ import { writeAndTestCommand, TEST_TYPE_CHOICES } from './write-and-test.js';
11
12
  import { Logger } from '../utils/logger.js';
12
13
 
13
14
  export async function generateCodeCommand(options = {}) {
@@ -61,7 +62,7 @@ export async function generateCodeCommand(options = {}) {
61
62
 
62
63
  const result = await CodeGenerator.generateFile(issue, absolutePath, {
63
64
  maxLines,
64
- language: absolutePath.endsWith('.ts') ? 'ts' : 'js',
65
+ language: /\.(ts|tsx)$/.test(absolutePath) ? 'ts' : 'js',
65
66
  extraContext,
66
67
  model: options.model,
67
68
  });
@@ -84,5 +85,31 @@ export async function generateCodeCommand(options = {}) {
84
85
  console.log('\n💡 下一步:執行 write-and-test 為此檔案生成並執行測試');
85
86
  console.log(` ai-git-tools write-and-test --file ${filePath}`);
86
87
 
88
+ if (!options.noConfirm) {
89
+ const { nextAction } = await inquirer.prompt([{
90
+ type: 'list',
91
+ name: 'nextAction',
92
+ message: '代碼已生成,接下來要如何處理測試?',
93
+ choices: [
94
+ ...TEST_TYPE_CHOICES,
95
+ {
96
+ name: '先不執行測試',
97
+ value: 'skip',
98
+ },
99
+ ],
100
+ default: 'auto',
101
+ }]);
102
+
103
+ if (nextAction !== 'skip') {
104
+ await writeAndTestCommand({
105
+ file: filePath,
106
+ testType: nextAction,
107
+ maxFixes: options.maxFixes || '2',
108
+ model: options.model,
109
+ noConfirm: true,
110
+ });
111
+ }
112
+ }
113
+
87
114
  return result;
88
115
  }
@@ -11,11 +11,44 @@ import { TestGenerator } from '../core/test-generator.js';
11
11
  import { TestRunner } from '../core/test-runner.js';
12
12
  import { Logger } from '../utils/logger.js';
13
13
 
14
+ export const TEST_TYPE_CHOICES = [
15
+ {
16
+ name: '寫測試(自動判斷 Jest 單元測試 / 元件測試)',
17
+ value: 'auto',
18
+ },
19
+ {
20
+ name: '寫 Jest 單元測試',
21
+ value: 'unit',
22
+ },
23
+ {
24
+ name: '寫元件測試',
25
+ value: 'component',
26
+ },
27
+ ];
28
+
29
+ export function normalizeTestType(testType = 'auto') {
30
+ const normalized = String(testType).trim().toLowerCase();
31
+
32
+ if (normalized === 'auto' || normalized === 'unit' || normalized === 'component') {
33
+ return normalized;
34
+ }
35
+
36
+ if (normalized === 'jest' || normalized === 'jest-unit' || normalized === 'unit-test') {
37
+ return 'unit';
38
+ }
39
+
40
+ if (normalized === 'component-test' || normalized === 'react-component') {
41
+ return 'component';
42
+ }
43
+
44
+ throw new Error(`不支援的測試類型:${testType}`);
45
+ }
46
+
14
47
  export async function writeAndTestCommand(options = {}) {
15
48
  const logger = new Logger();
16
49
 
17
50
  const filePath = options.file;
18
- const requestedTestType = options.testType || 'auto';
51
+ const requestedTestType = normalizeTestType(options.testType || 'auto');
19
52
  const maxFixes = parseInt(options.maxFixes || '2', 10);
20
53
 
21
54
  if (!filePath) {
@@ -121,10 +154,10 @@ export async function writeAndTestCommand(options = {}) {
121
154
  * 依據原始檔案路徑推導測試檔案路徑
122
155
  * e.g. src/core/foo.js → src/core/__tests__/foo.test.js
123
156
  */
124
- function deriveTestFilePath(sourceFilePath) {
157
+ export function deriveTestFilePath(sourceFilePath) {
125
158
  const dir = dirname(sourceFilePath);
126
- const name = basename(sourceFilePath).replace(/\.(js|ts|mjs|cjs)$/, '');
127
- const ext = sourceFilePath.endsWith('.ts') ? '.ts' : '.js';
159
+ const name = basename(sourceFilePath).replace(/\.(js|jsx|ts|tsx|mjs|cjs)$/, '');
160
+ const ext = /\.(ts|tsx)$/.test(sourceFilePath) ? '.ts' : '.js';
128
161
  return join(dir, '__tests__', `${name}.test${ext}`);
129
162
  }
130
163