ai-git-tools 2.0.59 → 2.0.60

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/bin/cli.js CHANGED
@@ -142,8 +142,9 @@ program
142
142
  .command('write-and-test')
143
143
  .description('AI 為指定檔案生成 Jest 測試,並自動執行與修復(最多 2 次)')
144
144
  .requiredOption('--file <path>', '原始碼路徑')
145
- .option('--test-type <type>', '測試類型:unit 或 component(預設 unit)', 'unit')
145
+ .option('--test-type <type>', '測試類型:auto、unit 或 component(預設 auto)', 'auto')
146
146
  .option('--max-fixes <number>', '最大自動修復次數(預設 2)', '2')
147
+ .option('--no-confirm', '跳過確認直接生成並執行測試')
147
148
  .option('--model <model>', '指定 AI 模型')
148
149
  .action(async (options) => {
149
150
  try {
@@ -159,10 +160,11 @@ program
159
160
  .command('auto-dev')
160
161
  .description('一鍵自動化:從 GitHub Issue 到代碼生成、測試與提交')
161
162
  .requiredOption('--issue <number>', 'GitHub Issue 編號')
162
- .option('--file <path>', '目標檔案路徑(若無則提示)')
163
+ .option('--file <path>', '目標檔案路徑(若無則自動推斷)')
163
164
  .option('--context <description>', '額外說明或補充需求')
164
- .option('--test-type <type>', '測試類型:unit 或 component(預設 unit)', 'unit')
165
+ .option('--test-type <type>', '測試類型:auto、unit 或 component(預設 auto)', 'auto')
165
166
  .option('--max-lines <number>', '最大行數限制(預設 500)', '500')
167
+ .option('--max-fixes <number>', '最大自動修復次數(預設 2)', '2')
166
168
  .option('--no-confirm', '全自動模式,跳過所有確認')
167
169
  .option('--model <model>', '指定 AI 模型')
168
170
  .action(async (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.0.59",
3
+ "version": "2.0.60",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -63,9 +63,6 @@
63
63
  "jest": {
64
64
  "testEnvironment": "node",
65
65
  "transform": {},
66
- "extensionsToTreatAsEsm": [
67
- ".js"
68
- ],
69
66
  "moduleNameMapper": {
70
67
  "^(\\.{1,2}/.+)\\.js$": "$1"
71
68
  }
@@ -4,14 +4,15 @@
4
4
  * 支援全自動模式(--no-confirm)和分步互動模式
5
5
  */
6
6
 
7
- import { resolve, dirname, basename, join } from 'path';
7
+ import { resolve } from 'path';
8
+ import { existsSync, readFileSync } from 'fs';
9
+ import { execSync } from 'child_process';
8
10
  import inquirer from 'inquirer';
9
11
  import { IssueReader } from '../core/issue-reader.js';
10
12
  import { CodeGenerator } from '../core/code-generator.js';
11
- import { TestGenerator } from '../core/test-generator.js';
12
- import { TestRunner } from '../core/test-runner.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
16
  import { Logger } from '../utils/logger.js';
16
17
 
17
18
  export async function autoDevCommand(options = {}) {
@@ -71,13 +72,25 @@ export async function autoDevCommand(options = {}) {
71
72
  }
72
73
 
73
74
  // ============================================================
74
- // 第四步:詢問目標檔案路徑(若未指定)
75
+ // 第四步:決定目標檔案路徑(優先自動推斷)
75
76
  // ============================================================
76
77
  if (!filePath) {
78
+ filePath = await inferTargetFilePath(issue, plan, options.model);
79
+ if (filePath) {
80
+ logger.info(`已自動推斷目標檔案:${filePath}`);
81
+ }
82
+ }
83
+
84
+ if (!filePath) {
85
+ if (noConfirm) {
86
+ logger.error('無法自動推斷目標檔案路徑,請改用 --file 明確指定。');
87
+ throw new Error('缺少可推斷的目標檔案路徑');
88
+ }
89
+
77
90
  const { file } = await inquirer.prompt([{
78
91
  type: 'input',
79
92
  name: 'file',
80
- message: '請輸入要生成的檔案路徑(例如:src/core/new-feature.js):',
93
+ message: '無法自動推斷,請輸入要生成的檔案路徑(例如:src/core/new-feature.js):',
81
94
  validate: (input) => input.trim().length > 0 ? true : '路徑不能為空',
82
95
  }]);
83
96
  filePath = file;
@@ -106,80 +119,35 @@ export async function autoDevCommand(options = {}) {
106
119
 
107
120
  console.log('');
108
121
 
109
- // ============================================================
110
- // 第六步:詢問是否執行測試(若非全自動)
111
- // ============================================================
112
- if (!noConfirm) {
113
- const { runTest } = await inquirer.prompt([{
114
- type: 'confirm',
115
- name: 'runTest',
116
- message: '代碼已生成,是否為該檔案生成並執行測試?',
117
- default: true,
118
- }]);
119
-
120
- if (!runTest) {
121
- logger.success('完成!代碼已生成。可稍後手動運行:');
122
- console.log(` ai-git-tools write-and-test --file ${filePath}`);
123
- return;
124
- }
125
- }
122
+ logger.step('正在驗證專案可用的測試腳本...');
123
+ ensureJestAvailable();
124
+ logger.success('已確認可執行 Jest 測試。');
125
+ console.log('');
126
126
 
127
127
  // ============================================================
128
- // 第七步:生成與執行測試(自動修復最多 2 次)
128
+ // 第六步:強制執行測試流程
129
129
  // ============================================================
130
- const testFilePath = deriveTestFilePath(absoluteFilePath);
131
- const maxFixes = 2;
132
-
133
- logger.step('正在生成測試...');
134
- await TestGenerator.generateTests(
135
- absoluteFilePath,
136
- testFilePath,
137
- options.testType || 'unit',
138
- options.model
139
- );
140
- logger.success(`測試已生成:${testFilePath}`);
141
- console.log('');
142
-
143
- let lastErrors = [];
144
- for (let attempt = 0; attempt <= maxFixes; attempt++) {
145
- if (attempt > 0) {
146
- logger.step(`進行第 ${attempt}/${maxFixes} 次自動修復...`);
147
- await TestGenerator.generateFix(absoluteFilePath, lastErrors, attempt, options.model);
148
- logger.success('修復代碼已更新。');
149
- }
150
-
151
- logger.step(`執行測試${attempt > 0 ? `(第 ${attempt} 次修復後)` : ''}...`);
152
- const result = await TestRunner.runTests(testFilePath);
130
+ const testResult = await writeAndTestCommand({
131
+ file: filePath,
132
+ testType: options.testType || 'auto',
133
+ maxFixes: options.maxFixes || '2',
134
+ model: options.model,
135
+ noConfirm: true,
136
+ skipCommit: true,
137
+ });
153
138
 
154
- if (result.success) {
155
- logger.success('所有測試通過! 🎉');
156
- console.log('');
157
- break;
158
- }
159
-
160
- lastErrors = result.errors;
161
- if (attempt < maxFixes) {
162
- logger.warning(`測試失敗(${result.errors.length} 個錯誤),嘗試自動修復...`);
163
- } else {
164
- logger.warning(`測試在第 ${maxFixes} 次修復後仍未通過`);
165
- console.log('');
166
- lastErrors.slice(0, 2).forEach((e, i) => {
167
- console.log(`\n[錯誤 ${i + 1}]`);
168
- console.log(e.split('\n').slice(0, 8).join('\n'));
169
- });
170
- console.log('');
171
- logger.error('自動修復失敗,需要手動介入。');
172
- return;
173
- }
139
+ if (!testResult?.success) {
140
+ logger.error('測試流程未完成,自動化工作流中止。');
141
+ return;
174
142
  }
175
143
 
176
144
  // ============================================================
177
- // 第八步:自動 Commit
145
+ // 第七步:自動 Commit
178
146
  // ============================================================
179
147
  logger.step('正在自動提交...');
180
148
  try {
181
- GitOperations.exec(`git add "${absoluteFilePath}" "${testFilePath}"`, { silent: true });
182
- const commitMsg = `feat(auto-dev): Issue #${issue.number} - ${issue.title}\n\n生成檔案:\n- ${filePath}\n- ${testFilePath.replace(process.cwd(), '.')}`;
149
+ GitOperations.exec(`git add "${absoluteFilePath}" "${testResult.testFilePath}"`, { silent: true });
150
+ const commitMsg = `feat(auto-dev): Issue #${issue.number} - ${issue.title}\n\n生成檔案:\n- ${filePath}\n- ${testResult.testFilePath.replace(process.cwd(), '.')}`;
183
151
  GitOperations.exec(`git commit -m "${commitMsg.split('\n')[0]}"`, { silent: true });
184
152
  logger.success('已自動提交');
185
153
  } catch {
@@ -190,7 +158,8 @@ export async function autoDevCommand(options = {}) {
190
158
  logger.section('✅ 自動化工作流完成');
191
159
  console.log(`Issue #${issue.number}:${issue.title}`);
192
160
  console.log(`生成檔案:${filePath}`);
193
- console.log(`測試檔案:${testFilePath.replace(process.cwd(), '.')}`);
161
+ console.log(`測試檔案:${testResult.testFilePath.replace(process.cwd(), '.')}`);
162
+ console.log(`測試類型:${testResult.testType}`);
194
163
  console.log(`合計行數:${codeResult.linesCount + ' (程式碼)'}`);
195
164
  }
196
165
 
@@ -220,11 +189,89 @@ ${issue.labels.length ? `標籤:${issue.labels.join(', ')}` : ''}
220
189
  }
221
190
 
222
191
  /**
223
- * 依據原始檔案路徑推導測試檔案路徑
192
+ * 優先以規則與計畫內容推斷最可能的目標檔案
224
193
  */
225
- function deriveTestFilePath(sourceFilePath) {
226
- const dir = dirname(sourceFilePath);
227
- const name = basename(sourceFilePath).replace(/\.(js|ts|mjs|cjs)$/, '');
228
- const ext = sourceFilePath.endsWith('.ts') ? '.ts' : '.js';
229
- return join(dir, '__tests__', `${name}.test${ext}`);
194
+ async function inferTargetFilePath(issue, plan, model) {
195
+ const planCandidates = extractFileCandidates(plan);
196
+ if (planCandidates.length === 1) {
197
+ return planCandidates[0];
198
+ }
199
+
200
+ if (planCandidates.length > 1) {
201
+ const preferredCandidate = planCandidates.find((candidate) => candidate.startsWith('src/'));
202
+ if (preferredCandidate) {
203
+ return preferredCandidate;
204
+ }
205
+ return planCandidates[0];
206
+ }
207
+
208
+ const prompt = `請根據以下 GitHub Issue 與實作計畫,推斷「最主要的實作檔案路徑」。
209
+
210
+ ## 規則
211
+ - 只回傳單一檔案路徑
212
+ - 優先回傳 src/ 底下的實作檔,不要回傳測試檔、文件檔、設定檔
213
+ - 副檔名限制為 .js、.ts、.jsx、.tsx、.mjs、.cjs
214
+ - 不要包含程式碼區塊、不要加說明
215
+
216
+ ## Issue #${issue.number}
217
+ 標題:${issue.title}
218
+ 描述:
219
+ ${issue.body || '(無描述)'}
220
+
221
+ ## 實作計畫
222
+ ${plan}`.trim();
223
+
224
+ const inferred = (await AIClient.sendAndWait(prompt, model)).trim().split(/\s+/)[0];
225
+ return isValidTargetPath(inferred) ? inferred : null;
226
+ }
227
+
228
+ function extractFileCandidates(plan) {
229
+ const matches = plan.match(/([A-Za-z0-9_./-]+\.(?:js|ts|jsx|tsx|mjs|cjs))/g) || [];
230
+ const uniqueMatches = [...new Set(matches.filter(isValidTargetPath))];
231
+
232
+ return uniqueMatches.filter((candidate) => {
233
+ const normalized = candidate.toLowerCase();
234
+ return !normalized.includes('__tests__/')
235
+ && !normalized.includes('.test.')
236
+ && !normalized.includes('.spec.')
237
+ && !normalized.endsWith('.config.js');
238
+ });
239
+ }
240
+
241
+ function isValidTargetPath(value) {
242
+ return typeof value === 'string'
243
+ && /^[A-Za-z0-9_./-]+\.(js|ts|jsx|tsx|mjs|cjs)$/.test(value)
244
+ && !value.startsWith('/');
245
+ }
246
+
247
+ function ensureJestAvailable() {
248
+ try {
249
+ execSync('npx jest --version', {
250
+ encoding: 'utf-8',
251
+ stdio: 'pipe',
252
+ cwd: process.cwd(),
253
+ });
254
+ } 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
+ const hasJestDependency = Boolean(
268
+ packageJson.dependencies?.jest || packageJson.devDependencies?.jest
269
+ );
270
+
271
+ if (!hasJestDependency) {
272
+ throw new Error('目前專案未安裝 Jest,請先安裝後再執行 auto-dev。');
273
+ }
274
+
275
+ throw new Error('偵測到 Jest 依賴,但無法執行 npx jest,請檢查安裝或 lockfile 狀態。');
276
+ }
230
277
  }
@@ -15,7 +15,7 @@ export async function writeAndTestCommand(options = {}) {
15
15
  const logger = new Logger();
16
16
 
17
17
  const filePath = options.file;
18
- const testType = options.testType || 'unit';
18
+ const requestedTestType = options.testType || 'auto';
19
19
  const maxFixes = parseInt(options.maxFixes || '2', 10);
20
20
 
21
21
  if (!filePath) {
@@ -35,27 +35,35 @@ export async function writeAndTestCommand(options = {}) {
35
35
  logger.header('write-and-test');
36
36
  console.log(`原始碼 :${absoluteSourcePath}`);
37
37
  console.log(`測試檔案:${testFilePath}`);
38
- console.log(`測試類型:${testType}`);
38
+ console.log(`測試類型:${requestedTestType}`);
39
39
  console.log(`最大修復:${maxFixes} 次`);
40
40
  console.log('');
41
41
 
42
42
  // 1. 詢問是否繼續
43
- const { confirmed } = await inquirer.prompt([{
44
- type: 'confirm',
45
- name: 'confirmed',
46
- message: `確認為 ${filePath} 生成 ${testType} 測試並執行?`,
47
- default: true,
48
- }]);
49
-
50
- if (!confirmed) {
51
- logger.info('已取消。');
52
- return;
43
+ if (!options.noConfirm) {
44
+ const { confirmed } = await inquirer.prompt([{
45
+ type: 'confirm',
46
+ name: 'confirmed',
47
+ message: `確認為 ${filePath} 生成 ${requestedTestType} 測試並執行?`,
48
+ default: true,
49
+ }]);
50
+
51
+ if (!confirmed) {
52
+ logger.info('已取消。');
53
+ return;
54
+ }
53
55
  }
54
56
 
55
57
  // 2. 生成測試
56
58
  logger.step('正在生成測試代碼...');
57
- await TestGenerator.generateTests(absoluteSourcePath, testFilePath, testType, options.model);
59
+ const generatedTest = await TestGenerator.generateTests(
60
+ absoluteSourcePath,
61
+ testFilePath,
62
+ requestedTestType,
63
+ options.model
64
+ );
58
65
  logger.success(`測試已寫入:${testFilePath}`);
66
+ logger.info(`實際測試類型:${generatedTest.testType}`);
59
67
 
60
68
  // 3. 執行測試 + 自動修復循環
61
69
  let lastErrors = [];
@@ -73,8 +81,10 @@ export async function writeAndTestCommand(options = {}) {
73
81
  logger.success('所有測試通過! 🎉');
74
82
 
75
83
  // 4. 自動 commit 測試檔案
76
- await commitTestFile(testFilePath, absoluteSourcePath, logger);
77
- return { success: true, testFilePath };
84
+ if (!options.skipCommit) {
85
+ await commitGeneratedFiles(testFilePath, absoluteSourcePath, logger);
86
+ }
87
+ return { success: true, testFilePath, testType: generatedTest.testType };
78
88
  }
79
89
 
80
90
  lastErrors = result.errors;
@@ -97,7 +107,12 @@ export async function writeAndTestCommand(options = {}) {
97
107
  console.log('');
98
108
  console.log(`原始碼路徑:${absoluteSourcePath}`);
99
109
  console.log(`測試路徑 :${testFilePath}`);
100
- return { success: false, testFilePath, errors: lastErrors };
110
+ return {
111
+ success: false,
112
+ testFilePath,
113
+ errors: lastErrors,
114
+ testType: generatedTest.testType,
115
+ };
101
116
  }
102
117
  }
103
118
  }
@@ -114,15 +129,15 @@ function deriveTestFilePath(sourceFilePath) {
114
129
  }
115
130
 
116
131
  /**
117
- * 將測試檔案 commit 到 git
132
+ * 將原始碼與測試檔案一併 commit 到 git
118
133
  */
119
- async function commitTestFile(testFilePath, sourceFilePath, logger) {
134
+ async function commitGeneratedFiles(testFilePath, sourceFilePath, logger) {
120
135
  try {
121
- execSync(`git add "${testFilePath}"`, { stdio: 'pipe' });
136
+ execSync(`git add "${sourceFilePath}" "${testFilePath}"`, { stdio: 'pipe' });
122
137
  const name = basename(sourceFilePath);
123
138
  execSync(`git commit -m "test: 為 ${name} 新增自動生成的測試"`, { stdio: 'pipe' });
124
- logger.success('測試檔案已自動 commit。');
125
- } catch (error) {
126
- logger.warning('無法自動 commit 測試檔案(可能沒有 staged 變更或不在 git 倉庫中)。');
139
+ logger.success('原始碼與測試檔案已自動 commit。');
140
+ } catch {
141
+ logger.warning('無法自動 commit 產生的檔案(可能沒有 staged 變更或不在 git 倉庫中)。');
127
142
  }
128
143
  }
@@ -12,11 +12,11 @@ export class TestGenerator {
12
12
  * 為原始檔案生成測試並寫入測試檔案
13
13
  * @param {string} sourceFilePath - 原始碼路徑(絕對路徑)
14
14
  * @param {string} testFilePath - 測試檔案輸出路徑(絕對路徑)
15
- * @param {'unit'|'component'} testType - 測試類型(預設 'unit')
15
+ * @param {'auto'|'unit'|'component'} testType - 測試類型(預設 'auto')
16
16
  * @param {string} [model] - AI 模型(可選)
17
- * @returns {Promise<{ testFilePath, linesCount }>}
17
+ * @returns {Promise<{ testFilePath, linesCount, testType: 'unit'|'component' }>}
18
18
  */
19
- static async generateTests(sourceFilePath, testFilePath, testType = 'unit', model) {
19
+ static async generateTests(sourceFilePath, testFilePath, testType = 'auto', model) {
20
20
  let sourceCode;
21
21
  try {
22
22
  sourceCode = readFileSync(sourceFilePath, 'utf-8');
@@ -24,15 +24,47 @@ export class TestGenerator {
24
24
  throw new Error(`無法讀取原始碼:${sourceFilePath}`);
25
25
  }
26
26
 
27
- const prompt = TestGenerator._buildPrompt(sourceCode, sourceFilePath, testType);
27
+ const resolvedTestType = TestGenerator.resolveTestType(sourceFilePath, sourceCode, testType);
28
+ const prompt = TestGenerator._buildPrompt(sourceCode, sourceFilePath, resolvedTestType);
28
29
  const rawTests = await AIClient.sendAndWait(prompt, model);
29
- const testCode = TestGenerator._cleanCode(rawTests);
30
+ const testCode = TestGenerator._finalizeTestCode(
31
+ TestGenerator._cleanCode(rawTests),
32
+ resolvedTestType
33
+ );
30
34
 
31
35
  // 確保目錄存在
32
36
  mkdirSync(dirname(testFilePath), { recursive: true });
33
37
  writeFileSync(testFilePath, testCode, 'utf-8');
34
38
 
35
- return { testFilePath, linesCount: testCode.split('\n').length };
39
+ return {
40
+ testFilePath,
41
+ linesCount: testCode.split('\n').length,
42
+ testType: resolvedTestType,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * 解析最終測試類型
48
+ * @param {string} sourceFilePath
49
+ * @param {string} sourceCode
50
+ * @param {'auto'|'unit'|'component'} requestedTestType
51
+ * @returns {'unit'|'component'}
52
+ */
53
+ static resolveTestType(sourceFilePath, sourceCode, requestedTestType = 'auto') {
54
+ if (requestedTestType === 'unit' || requestedTestType === 'component') {
55
+ return requestedTestType;
56
+ }
57
+
58
+ const normalizedPath = sourceFilePath.toLowerCase();
59
+ const looksLikeComponentPath = /\.(jsx|tsx)$/.test(normalizedPath)
60
+ || /component|page|view|screen/.test(normalizedPath);
61
+ const hasReactImport = /from\s+['"]react['"]|from\s+['"]react-dom['"]/.test(sourceCode);
62
+ const hasTestingLibraryHint = /use(State|Effect|Memo|Callback|Reducer|Ref|Context)|React\./.test(sourceCode);
63
+ const hasJsx = /<([A-Z][\w]*|[a-z][\w-]*)(\s[^>]*)?>/.test(sourceCode);
64
+
65
+ return looksLikeComponentPath || hasReactImport || hasTestingLibraryHint || hasJsx
66
+ ? 'component'
67
+ : 'unit';
36
68
  }
37
69
 
38
70
  /**
@@ -75,11 +107,14 @@ ${sourceCode}
75
107
  3. Mock 所有外部依賴(檔案系統、網路、子程序等)
76
108
  4. 使用 beforeEach / afterEach 管理測試狀態
77
109
  5. 測試名稱使用繁體中文描述
110
+ ${testType === 'component'
111
+ ? '6. 使用 @testing-library/react 撰寫互動與渲染測試\n7. 檔案最前面必須加上 /** @jest-environment jsdom */ 註解'
112
+ : '6. 以純邏輯與副作用隔離為主,避免使用 DOM API'}
78
113
 
79
114
  ## 輸出規則
80
115
  - 只輸出可直接執行的測試程式碼
81
116
  - 不要 markdown 區塊(\`\`\`)、不要任何說明文字
82
- - 第一行必須是 import 或 require 陳述式`.trim();
117
+ - 第一行必須是 /** @jest-environment jsdom */ import / require 陳述式`.trim();
83
118
  }
84
119
 
85
120
  /**
@@ -112,4 +147,19 @@ ${testErrors.join('\n')}
112
147
  .replace(/\n?```$/gm, '')
113
148
  .trim();
114
149
  }
150
+
151
+ /**
152
+ * 針對元件測試補上必要的 jsdom 設定
153
+ */
154
+ static _finalizeTestCode(testCode, testType) {
155
+ if (testType !== 'component') {
156
+ return testCode;
157
+ }
158
+
159
+ if (testCode.startsWith('/** @jest-environment jsdom */')) {
160
+ return testCode;
161
+ }
162
+
163
+ return `/** @jest-environment jsdom */\n${testCode}`;
164
+ }
115
165
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { execSync } from 'child_process';
7
7
  import { existsSync } from 'fs';
8
+ import { resolve } from 'path';
8
9
 
9
10
  export class TestRunner {
10
11
  /**
@@ -18,10 +19,12 @@ export class TestRunner {
18
19
  }
19
20
 
20
21
  try {
21
- execSync(`npx jest "${testFilePath}" --no-coverage --forceExit`, {
22
+ const absoluteTestFilePath = resolve(testFilePath);
23
+ execSync(`npx jest --runInBand --runTestsByPath "${absoluteTestFilePath}" --no-coverage`, {
22
24
  encoding: 'utf-8',
23
25
  stdio: 'pipe',
24
26
  timeout: 120000, // 2 分鐘超時
27
+ cwd: process.cwd(),
25
28
  });
26
29
  return { success: true, errors: [] };
27
30
  } catch (error) {