ai-git-tools 2.0.39 → 2.0.41

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
@@ -15,6 +15,7 @@ import { commitCommand } from '../src/commands/commit.js';
15
15
  import { commitAllCommand } from '../src/commands/commit-all.js';
16
16
  import { prCommand } from '../src/commands/pr.js';
17
17
  import { initCommand } from '../src/commands/init.js';
18
+ import { autodevCommand } from '../src/commands/autodev.js';
18
19
 
19
20
  // 讀取 package.json 獲取版本號
20
21
  const __filename = fileURLToPath(import.meta.url);
@@ -96,4 +97,24 @@ program
96
97
  }
97
98
  });
98
99
 
100
+ // AutoDev 命令
101
+ program
102
+ .command('autodev <issue>')
103
+ .description('全自動開發流程:Issue → 測試 → commit-all → PR')
104
+ .option('--dry-run', '乾運行:顯示計劃但不執行')
105
+ .option('-v, --verbose', '詳細輸出')
106
+ .option('--framework <name>', '強制指定測試框架(jest / vitest / mocha)')
107
+ .option('--skip-commit', '跳過自動 commit')
108
+ .option('--skip-pr', '跳過自動 PR 建立')
109
+ .option('--skip-all', '只執行測試並發佈評論')
110
+ .option('--commit-only', '執行測試 + commit,不建立 PR')
111
+ .action(async (issue, options) => {
112
+ try {
113
+ await autodevCommand(issue, options);
114
+ process.exit(0);
115
+ } catch (error) {
116
+ process.exit(1);
117
+ }
118
+ });
119
+
99
120
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.0.39",
3
+ "version": "2.0.41",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * AutoDev 命令
3
+ * 完整自動化開發流程:Issue 解析 → 測試 → 發佈評論 → commit-all → PR
4
+ *
5
+ * 使用方式:
6
+ * ai autodev <issue>
7
+ * ai autodev 123
8
+ * ai autodev https://github.com/org/repo/issues/123
9
+ * ai autodev 123 --dry-run
10
+ * ai autodev 123 --skip-pr
11
+ */
12
+
13
+ import { AutodevWorkflow } from '../dev-modules/core/autodev-workflow.js';
14
+ import { loadAutodevConfig } from '../core/config-loader.js';
15
+ import { handleError } from '../utils/helpers.js';
16
+
17
+ /**
18
+ * AutoDev 命令主函數
19
+ * @param {string|number} issueInput - Issue 編號或 URL
20
+ * @param {Object} cliOptions - 來自 commander 的選項
21
+ */
22
+ export async function autodevCommand(issueInput, cliOptions = {}) {
23
+ if (!issueInput) {
24
+ console.error('❌ 請提供 Issue 編號或 URL');
25
+ console.error(' 用法:ai autodev <issue>');
26
+ console.error(' 範例:ai autodev 123');
27
+ process.exit(1);
28
+ }
29
+
30
+ try {
31
+ // 載入設定
32
+ const config = await loadAutodevConfig();
33
+
34
+ // 合併 CLI 選項(CLI 優先)
35
+ const options = {
36
+ dryRun: cliOptions.dryRun ?? false,
37
+ verbose: cliOptions.verbose ?? config.verbose ?? false,
38
+ framework: cliOptions.framework ?? config.framework ?? null,
39
+ skipCommit: cliOptions.skipCommit ?? config.skipCommit ?? false,
40
+ skipPr: cliOptions.skipPr ?? config.skipPr ?? false,
41
+ skipAll: cliOptions.skipAll ?? false,
42
+ commitOnly: cliOptions.commitOnly ?? false,
43
+ };
44
+
45
+ const workflow = new AutodevWorkflow(config);
46
+ await workflow.execute(issueInput, options);
47
+ } catch (error) {
48
+ handleError(error);
49
+ throw error;
50
+ }
51
+ }
@@ -459,3 +459,82 @@ export async function commitAllCommand() {
459
459
  throw error;
460
460
  }
461
461
  }
462
+
463
+ // ─────────────────────────────────────────────────────────────
464
+ // 程序化 API(供 ai autodev 呼叫,不影響 CLI 行為)
465
+ // ─────────────────────────────────────────────────────────────
466
+
467
+ /**
468
+ * 程序化執行 commit-all 邏輯,供其他模組呼叫
469
+ * 不會呼叫 process.exit(),而是回傳結果物件
470
+ *
471
+ * @param {Object} [options]
472
+ * @param {boolean} [options.verbose]
473
+ * @returns {Promise<{ success: boolean, commitCount: number, message: string }>}
474
+ */
475
+ export async function commitAllProgrammatic(options = {}) {
476
+ const logger = new Logger();
477
+
478
+ try {
479
+ const config = await loadCommitConfig();
480
+ if (options.verbose) {
481
+ config.output.verbose = true;
482
+ }
483
+
484
+ const changes = getAllChanges();
485
+ if (changes.length === 0) {
486
+ return { success: true, commitCount: 0, message: '沒有需要提交的變更' };
487
+ }
488
+
489
+ const groups = await analyzeAndGroupChanges(changes, config);
490
+ if (!groups || groups.length === 0) {
491
+ return { success: false, commitCount: 0, message: 'AI 分析失敗,無法產生分組' };
492
+ }
493
+
494
+ // 補入未分組的檔案
495
+ const groupedIndices = new Set(groups.flatMap((g) => g.file_indices));
496
+ const ungrouped = [...Array(changes.length).keys()].filter((i) => !groupedIndices.has(i));
497
+ if (ungrouped.length > 0) {
498
+ groups.push({
499
+ group_name: '其他變更',
500
+ commit_type: 'chore',
501
+ commit_scope: 'misc',
502
+ file_indices: ungrouped,
503
+ description: '未能自動分類的其他變更',
504
+ });
505
+ }
506
+
507
+ let successCount = 0;
508
+ for (let i = 0; i < groups.length; i++) {
509
+ const group = groups[i];
510
+ const groupFiles = group.file_indices.map((index) => changes[index]);
511
+ const ok = await commitGroup(group, groupFiles, config);
512
+ if (ok) successCount++;
513
+ }
514
+
515
+ // 清理 staged
516
+ try {
517
+ execSync('git reset HEAD -- .', { stdio: 'ignore' });
518
+ } catch (_) { /* 忽略 */ }
519
+
520
+ if (successCount === 0) {
521
+ return { success: false, commitCount: 0, message: '所有群組提交失敗' };
522
+ }
523
+
524
+ // 取得最新 commit hash
525
+ let commitHash = '';
526
+ try {
527
+ commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
528
+ } catch (_) { /* 忽略 */ }
529
+
530
+ return {
531
+ success: true,
532
+ commitCount: successCount,
533
+ commitHash,
534
+ message: `成功提交 ${successCount}/${groups.length} 個群組`,
535
+ };
536
+ } catch (error) {
537
+ logger.error(`commitAllProgrammatic 失敗:${error.message}`);
538
+ return { success: false, commitCount: 0, message: error.message };
539
+ }
540
+ }
@@ -62,3 +62,66 @@ export async function prCommand() {
62
62
  throw error;
63
63
  }
64
64
  }
65
+
66
+ // ─────────────────────────────────────────────────────────────
67
+ // 程序化 API(供 ai autodev 呼叫,不影響 CLI 行為)
68
+ // ─────────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * 程序化執行 PR 建立流程,供其他模組呼叫
72
+ * 不互動(noConfirm = true),直接建立 PR 並回傳結果
73
+ *
74
+ * @param {Object} [options]
75
+ * @param {string} [options.base] - 目標分支
76
+ * @param {string} [options.head] - 來源分支
77
+ * @param {boolean} [options.verbose]
78
+ * @param {Object} [options.testResults] - 注入測試結果到 PR 描述(保留供未來擴充)
79
+ * @returns {Promise<{ success: boolean, prUrl: string|null, prNumber: number|null, message: string }>}
80
+ */
81
+ export async function prProgrammatic(options = {}) {
82
+ const logger = new Logger();
83
+
84
+ if (!checkGHAuth(logger)) {
85
+ return { success: false, prUrl: null, prNumber: null, message: 'GitHub CLI 未登入' };
86
+ }
87
+
88
+ try {
89
+ const config = await loadConfig();
90
+
91
+ // 程序化呼叫:跳過互動確認
92
+ config.noConfirm = true;
93
+ if (options.base) config.baseBranch = options.base;
94
+ if (options.head) config.headBranch = options.head;
95
+ if (options.verbose) config.output.verbose = true;
96
+
97
+ const workflow = new PRWorkflow(config);
98
+
99
+ // 攔截 PRWorkflow 建立的 PR URL
100
+ let prUrl = null;
101
+ let prNumber = null;
102
+ const originalCreatePR = workflow.createPR?.bind(workflow);
103
+ if (originalCreatePR) {
104
+ workflow.createPR = async (...args) => {
105
+ const url = await originalCreatePR(...args);
106
+ prUrl = url;
107
+ if (url) {
108
+ const match = url.match(/\/pull\/(\d+)/);
109
+ if (match) prNumber = parseInt(match[1], 10);
110
+ }
111
+ return url;
112
+ };
113
+ }
114
+
115
+ await workflow.execute();
116
+
117
+ return {
118
+ success: true,
119
+ prUrl,
120
+ prNumber,
121
+ message: prUrl ? `PR 已建立:${prUrl}` : 'PR 執行完成',
122
+ };
123
+ } catch (error) {
124
+ logger.error(`prProgrammatic 失敗:${error.message}`);
125
+ return { success: false, prUrl: null, prNumber: null, message: error.message };
126
+ }
127
+ }
@@ -179,3 +179,46 @@ export async function loadPRConfig() {
179
179
 
180
180
  return config;
181
181
  }
182
+
183
+ /**
184
+ * 載入 AutoDev 配置
185
+ * 讀取 .ai-git-config.js(或 .mjs)的 autodev 區塊
186
+ * 所有欄位皆選填,未設定時使用合理預設值
187
+ *
188
+ * @returns {Promise<Object>}
189
+ */
190
+ export async function loadAutodevConfig() {
191
+ const defaults = {
192
+ framework: null, // null = 自動偵測
193
+ testPaths: [], // 空 = 自動發現
194
+ testTimeout: 300_000, // 5 分鐘
195
+ skipCommit: false,
196
+ skipPr: false,
197
+ verbose: false,
198
+ projectRoot: process.cwd(),
199
+ };
200
+
201
+ // 支援 .ai-git-config.js 和 .ai-git-config.mjs
202
+ const candidates = [
203
+ resolve(process.cwd(), '.ai-git-config.js'),
204
+ resolve(process.cwd(), '.ai-git-config.mjs'),
205
+ ];
206
+
207
+ let userAutodev = {};
208
+ for (const configPath of candidates) {
209
+ if (existsSync(configPath)) {
210
+ try {
211
+ const imported = await import(`file://${configPath}`);
212
+ userAutodev = (imported.default || {}).autodev || {};
213
+ break;
214
+ } catch (error) {
215
+ console.warn(`⚠️ 載入 autodev 配置失敗: ${error.message}`);
216
+ }
217
+ }
218
+ }
219
+
220
+ return {
221
+ ...defaults,
222
+ ...userAutodev,
223
+ };
224
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * AI 代碼生成器
3
+ * 基於 Issue 描述,調用 AI 生成代碼框架/實現
4
+ */
5
+
6
+ import { AIClient } from '../../core/ai-client.js';
7
+
8
+ /**
9
+ * @typedef {Object} GeneratedCode
10
+ * @property {string} filePath - 要建立/修改的檔案路徑
11
+ * @property {string} content - 代碼內容
12
+ * @property {string} type - 'new' | 'modify' | 'skip'
13
+ * @property {string|null} reason - 若 skip 時的原因
14
+ */
15
+
16
+ export class CodeGenerator {
17
+ constructor(config = {}) {
18
+ this.config = config;
19
+ this.model = config.aiModel || 'gpt-4.1';
20
+ }
21
+
22
+ /**
23
+ * 基於 Issue 生成代碼檔案
24
+ * @param {import('../core/issue-parser.js').IssueData} issueData
25
+ * @param {string} [projectRoot] - 專案根目錄
26
+ * @returns {Promise<GeneratedCode[]>}
27
+ */
28
+ async generateCodeFiles(issueData, projectRoot = process.cwd()) {
29
+ const prompt = this._buildPrompt(issueData, projectRoot);
30
+
31
+ console.log(' 🤖 調用 AI 生成代碼框架...');
32
+
33
+ let response;
34
+ try {
35
+ response = await AIClient.sendAndWait(
36
+ prompt,
37
+ this.model,
38
+ this.config.maxRetries || 3
39
+ );
40
+ } catch (error) {
41
+ console.warn(` ⚠️ AI 調用失敗:${error.message}`);
42
+ return [];
43
+ }
44
+
45
+ // 解析 AI 回應
46
+ const files = this._parseResponse(response);
47
+ console.log(` 生成 ${files.length} 個檔案`);
48
+ return files;
49
+ }
50
+
51
+ /**
52
+ * 寫入生成的檔案到磁盤
53
+ * @param {GeneratedCode[]} files
54
+ * @param {string} projectRoot
55
+ * @returns {Promise<{ written: string[], skipped: string[] }>}
56
+ */
57
+ async writeFiles(files, projectRoot = process.cwd()) {
58
+ const { writeFileSync, mkdirSync, existsSync } = await import('fs');
59
+ const { dirname, resolve } = await import('path');
60
+
61
+ const written = [];
62
+ const skipped = [];
63
+
64
+ for (const file of files) {
65
+ if (file.type === 'skip') {
66
+ skipped.push(`${file.filePath} (${file.reason})`);
67
+ continue;
68
+ }
69
+
70
+ const fullPath = resolve(projectRoot, file.filePath);
71
+ const dir = dirname(fullPath);
72
+
73
+ // 建立目錄
74
+ try {
75
+ if (!existsSync(dir)) {
76
+ mkdirSync(dir, { recursive: true });
77
+ }
78
+ } catch (e) {
79
+ console.warn(` ⚠️ 無法建立目錄 ${dir}`);
80
+ skipped.push(file.filePath);
81
+ continue;
82
+ }
83
+
84
+ // 寫入檔案
85
+ try {
86
+ writeFileSync(fullPath, file.content, 'utf-8');
87
+ written.push(file.filePath);
88
+ } catch (e) {
89
+ console.warn(` ⚠️ 無法寫入 ${file.filePath}`);
90
+ skipped.push(file.filePath);
91
+ }
92
+ }
93
+
94
+ return { written, skipped };
95
+ }
96
+
97
+ // ── 私有輔助方法
98
+
99
+ _buildPrompt(issueData, projectRoot) {
100
+ const { title, body, labels } = issueData;
101
+
102
+ return `你是一個資深軟體工程師,負責基於 GitHub Issue 生成代碼框架。
103
+
104
+ ## Issue 資訊
105
+ 標題:${title}
106
+ 標籤:${labels.join(', ') || '無'}
107
+ 描述:
108
+ ${body}
109
+
110
+ ## 任務
111
+ 1. 分析上方 Issue 的需求
112
+ 2. 設計必要的檔案結構和代碼框架
113
+ 3. 生成初始實現(可以有 TODO 註釋表示待完成部分)
114
+
115
+ ## 輸出格式
116
+ 請回傳 JSON 陣列,每個物件包含:
117
+ \`\`\`json
118
+ [
119
+ {
120
+ "filePath": "src/components/MyComponent.tsx",
121
+ "content": "// 完整的檔案內容...",
122
+ "type": "new"
123
+ },
124
+ {
125
+ "filePath": "src/utils/helper.ts",
126
+ "content": "// ...",
127
+ "type": "new"
128
+ }
129
+ ]
130
+ \`\`\`
131
+
132
+ 只回傳 JSON,不要其他文字。
133
+
134
+ ## 檔案類型指南
135
+ - TypeScript/JavaScript:包含正確的 import/export
136
+ - React:使用 functional components + hooks
137
+ - 包含基本的 JSDoc 註釋
138
+ - 包含 TODO 或 FIXME 註釋提示需要補完的部分
139
+
140
+ 專案根目錄:${projectRoot}`;
141
+ }
142
+
143
+ _parseResponse(response) {
144
+ try {
145
+ // 找 JSON 陣列
146
+ const match = response.match(/\[[\s\S]*\]/);
147
+ if (!match) {
148
+ console.warn(' ⚠️ 無法從 AI 回應找到 JSON 陣列');
149
+ return [];
150
+ }
151
+
152
+ const files = JSON.parse(match[0]);
153
+ if (!Array.isArray(files)) {
154
+ console.warn(' ⚠️ AI 回應不是陣列格式');
155
+ return [];
156
+ }
157
+
158
+ return files.map((f) => ({
159
+ filePath: f.filePath || '',
160
+ content: f.content || '',
161
+ type: f.type || 'new',
162
+ reason: f.reason || null,
163
+ }));
164
+ } catch (error) {
165
+ console.warn(` ⚠️ 解析 AI 回應失敗:${error.message}`);
166
+ return [];
167
+ }
168
+ }
169
+ }