ai-git-tools 2.0.48 → 2.0.50

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.
@@ -1,144 +0,0 @@
1
- /**
2
- * Issue 解析模組
3
- * 負責解析 Issue 編號/URL,並從 GitHub API 擷取 Issue 資料
4
- */
5
-
6
- import { execSync } from 'child_process';
7
-
8
- /**
9
- * 標準化 IssueData 結構
10
- * @typedef {Object} IssueData
11
- * @property {number} number - Issue 編號
12
- * @property {string} title - Issue 標題
13
- * @property {string} body - Issue 說明
14
- * @property {string[]} labels - 標籤列表
15
- * @property {string} owner - 倉庫擁有者
16
- * @property {string} repo - 倉庫名稱
17
- * @property {string} url - Issue URL
18
- */
19
-
20
- export class IssueParser {
21
- /**
22
- * 解析輸入(數字字串或完整 URL)為 Issue 編號
23
- * @param {string|number} input - Issue 編號或 GitHub Issue URL
24
- * @returns {number} Issue 編號
25
- */
26
- parseIssueNumber(input) {
27
- const str = String(input).trim();
28
-
29
- // 純數字
30
- if (/^\d+$/.test(str)) {
31
- return parseInt(str, 10);
32
- }
33
-
34
- // GitHub URL 格式:https://github.com/owner/repo/issues/123
35
- const urlMatch = str.match(/\/issues\/(\d+)/);
36
- if (urlMatch) {
37
- return parseInt(urlMatch[1], 10);
38
- }
39
-
40
- throw new Error(
41
- `無效的 Issue 輸入:「${input}」\n請提供 Issue 編號(如 123)或完整 URL(如 https://github.com/owner/repo/issues/123)`
42
- );
43
- }
44
-
45
- /**
46
- * 從 GitHub URL 解析倉庫資訊
47
- * @param {string} url
48
- * @returns {{ owner: string, repo: string, issueNumber: number }}
49
- */
50
- parseIssueURL(url) {
51
- const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
52
- if (!match) {
53
- throw new Error(`無法解析 GitHub Issue URL:${url}`);
54
- }
55
- return {
56
- owner: match[1],
57
- repo: match[2].replace(/\.git$/, ''),
58
- issueNumber: parseInt(match[3], 10),
59
- };
60
- }
61
-
62
- /**
63
- * 從 git remote 自動偵測 owner/repo
64
- * @returns {{ owner: string, repo: string }}
65
- */
66
- detectRepoFromRemote() {
67
- try {
68
- const remoteUrl = execSync('git remote get-url origin', {
69
- encoding: 'utf-8',
70
- stdio: ['pipe', 'pipe', 'pipe'],
71
- }).trim();
72
-
73
- // HTTPS: https://github.com/owner/repo.git
74
- const httpsMatch = remoteUrl.match(/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/);
75
- if (httpsMatch) {
76
- return { owner: httpsMatch[1], repo: httpsMatch[2] };
77
- }
78
-
79
- // SSH: git@github.com:owner/repo.git
80
- const sshMatch = remoteUrl.match(/github\.com:([^/]+)\/([^/\s]+?)(?:\.git)?$/);
81
- if (sshMatch) {
82
- return { owner: sshMatch[1], repo: sshMatch[2] };
83
- }
84
-
85
- throw new Error('無法從 git remote 解析 owner/repo');
86
- } catch (error) {
87
- throw new Error(`偵測 git remote 失敗:${error.message}`);
88
- }
89
- }
90
-
91
- /**
92
- * 透過 GitHub CLI 擷取 Issue 資料
93
- * @param {string} owner
94
- * @param {string} repo
95
- * @param {number} issueNumber
96
- * @returns {IssueData}
97
- */
98
- async fetchIssue(owner, repo, issueNumber) {
99
- try {
100
- const json = execSync(
101
- `gh api repos/${owner}/${repo}/issues/${issueNumber}`,
102
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
103
- );
104
- const data = JSON.parse(json);
105
-
106
- return {
107
- number: data.number,
108
- title: data.title,
109
- body: data.body || '',
110
- labels: (data.labels || []).map((l) => l.name),
111
- owner,
112
- repo,
113
- url: data.html_url,
114
- };
115
- } catch (error) {
116
- if (error.message.includes('404')) {
117
- throw new Error(
118
- `Issue #${issueNumber} 不存在於 ${owner}/${repo}\n請確認 Issue 編號正確,且你有存取該倉庫的權限`
119
- );
120
- }
121
- throw new Error(`擷取 Issue 失敗:${error.message}`);
122
- }
123
- }
124
-
125
- /**
126
- * 解析輸入並完整擷取 Issue 資料(主要入口)
127
- * @param {string|number} input - Issue 編號或 URL
128
- * @returns {Promise<IssueData>}
129
- */
130
- async parse(input) {
131
- const str = String(input).trim();
132
-
133
- // 如果是完整 URL,直接解析 owner/repo
134
- if (str.startsWith('http')) {
135
- const { owner, repo, issueNumber } = this.parseIssueURL(str);
136
- return this.fetchIssue(owner, repo, issueNumber);
137
- }
138
-
139
- // 否則從 git remote 偵測
140
- const issueNumber = this.parseIssueNumber(str);
141
- const { owner, repo } = this.detectRepoFromRemote();
142
- return this.fetchIssue(owner, repo, issueNumber);
143
- }
144
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * 測試執行器基礎類別
3
- * 定義標準介面與 TestResults 結構
4
- */
5
-
6
- /**
7
- * @typedef {Object} TestCase
8
- * @property {string} name - 測試名稱
9
- * @property {string} suite - 所屬 suite(describe 區塊)
10
- * @property {boolean} passed
11
- * @property {number} duration - 毫秒
12
- * @property {string|null} error - 失敗時的錯誤訊息
13
- */
14
-
15
- /**
16
- * @typedef {Object} TestResults
17
- * @property {string} framework
18
- * @property {number} total
19
- * @property {number} passed
20
- * @property {number} failed
21
- * @property {number} skipped
22
- * @property {number} duration - 總毫秒數
23
- * @property {TestCase[]} tests
24
- * @property {string|null} rawOutput
25
- */
26
-
27
- export class ExecutorBase {
28
- /**
29
- * @param {Object} options
30
- * @param {number} [options.timeout=300000] - 5 分鐘
31
- */
32
- constructor(options = {}) {
33
- this.timeout = options.timeout ?? 300_000;
34
- }
35
-
36
- /**
37
- * 執行測試(子類別實作)
38
- * @param {string[]} testPaths
39
- * @param {Object} options
40
- * @returns {Promise<TestResults>}
41
- */
42
- // eslint-disable-next-line no-unused-vars
43
- async run(testPaths, options = {}) {
44
- throw new Error('子類別必須實作 run() 方法');
45
- }
46
-
47
- /**
48
- * 解析 JSON 輸出(子類別實作)
49
- * @param {string} output
50
- * @returns {TestResults}
51
- */
52
- // eslint-disable-next-line no-unused-vars
53
- parseResults(output) {
54
- throw new Error('子類別必須實作 parseResults() 方法');
55
- }
56
-
57
- /**
58
- * 建立空的 TestResults 骨架
59
- * @param {string} framework
60
- * @returns {TestResults}
61
- */
62
- emptyResults(framework) {
63
- return {
64
- framework,
65
- total: 0,
66
- passed: 0,
67
- failed: 0,
68
- skipped: 0,
69
- duration: 0,
70
- tests: [],
71
- rawOutput: null,
72
- };
73
- }
74
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * 測試執行器工廠
3
- * 依框架名稱回傳對應的執行器實例
4
- */
5
-
6
- import { JestExecutor } from './jest-executor.js';
7
- import { VitestExecutor } from './vitest-executor.js';
8
- import { MochaExecutor } from './mocha-executor.js';
9
- import { FRAMEWORKS } from './test-detector.js';
10
-
11
- /**
12
- * @param {string} framework - 'jest' | 'vitest' | 'mocha'
13
- * @param {Object} options - 傳遞給執行器的選項(如 timeout)
14
- * @returns {import('./executor-base.js').ExecutorBase}
15
- */
16
- export function createExecutor(framework, options = {}) {
17
- switch (framework) {
18
- case FRAMEWORKS.JEST:
19
- return new JestExecutor(options);
20
- case FRAMEWORKS.VITEST:
21
- return new VitestExecutor(options);
22
- case FRAMEWORKS.MOCHA:
23
- return new MochaExecutor(options);
24
- default:
25
- throw new Error(
26
- `不支援的測試框架:「${framework}」\n目前支援:jest、vitest、mocha\n請在 .ai-git-config.js 的 autodev.framework 指定框架`
27
- );
28
- }
29
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * Jest 測試執行器
3
- * 使用 --json 取得結構化輸出
4
- */
5
-
6
- import { execSync } from 'child_process';
7
- import { existsSync } from 'fs';
8
- import { join } from 'path';
9
- import { ExecutorBase } from './executor-base.js';
10
-
11
- export class JestExecutor extends ExecutorBase {
12
- constructor(options = {}) {
13
- super(options);
14
- this.framework = 'jest';
15
- }
16
-
17
- /**
18
- * 執行 Jest,回傳標準化 TestResults
19
- * @param {string[]} testPaths - 若為空則執行全部
20
- * @param {Object} options
21
- * @param {boolean} [options.verbose]
22
- * @returns {Promise<import('./executor-base.js').TestResults>}
23
- */
24
- async run(testPaths = [], options = {}) {
25
- const pathArgs = testPaths.length > 0 ? testPaths.join(' ') : '';
26
- const cmd = `npx jest --json --forceExit --passWithNoTests ${pathArgs}`;
27
- const projectRoot = options.projectRoot || process.cwd();
28
-
29
- // 若 node_modules 不存在,自動執行 npm install 安裝依賴
30
- const nodeModulesPath = join(projectRoot, 'node_modules');
31
- if (!existsSync(nodeModulesPath)) {
32
- console.log(' 📦 偵測到 node_modules 不存在,自動執行 npm install...');
33
- try {
34
- execSync('npm install', {
35
- cwd: projectRoot,
36
- stdio: 'inherit',
37
- timeout: 120_000, // 2 分鐘安裝時間上限
38
- });
39
- console.log(' ✅ 依賴安裝完成');
40
- } catch (err) {
41
- console.warn(` ⚠️ npm install 失敗:${err.message}`);
42
- console.warn(' 跳過測試步驟,代碼已生成但未驗證');
43
- return this.emptyResults(this.framework);
44
- }
45
- }
46
-
47
- if (options.verbose) {
48
- console.log(` 執行命令:${cmd}`);
49
- }
50
-
51
- let output = '';
52
- try {
53
- output = execSync(cmd, {
54
- encoding: 'utf-8',
55
- timeout: this.timeout,
56
- stdio: ['pipe', 'pipe', 'pipe'],
57
- cwd: projectRoot,
58
- });
59
- } catch (error) {
60
- // Jest 測試失敗時 exit code 非 0,但 stdout 仍有 JSON
61
- output = error.stdout || error.output?.[1] || '';
62
- }
63
-
64
- return this.parseResults(output);
65
- }
66
-
67
- /**
68
- * 將 Jest --json 輸出解析為標準 TestResults
69
- * @param {string} output
70
- * @returns {import('./executor-base.js').TestResults}
71
- */
72
- parseResults(output) {
73
- const result = this.emptyResults(this.framework);
74
- result.rawOutput = output;
75
-
76
- let json;
77
- try {
78
- // Jest --json 可能混有非 JSON 前綴,找第一個 { 開始
79
- const start = output.indexOf('{');
80
- if (start === -1) return result;
81
- json = JSON.parse(output.slice(start));
82
- } catch (_) {
83
- return result;
84
- }
85
-
86
- result.total = json.numTotalTests ?? 0;
87
- result.passed = json.numPassedTests ?? 0;
88
- result.failed = json.numFailedTests ?? 0;
89
- result.skipped = (json.numPendingTests ?? 0) + (json.numTodoTests ?? 0);
90
- result.duration = json.testResults?.reduce((acc, s) => acc + (s.perfStats?.runtime ?? 0), 0) ?? 0;
91
-
92
- for (const suite of json.testResults ?? []) {
93
- const suiteName = suite.testFilePath?.replace(process.cwd(), '').replace(/^\//, '') ?? '';
94
- for (const t of suite.testResults ?? []) {
95
- result.tests.push({
96
- name: t.fullName ?? t.title,
97
- suite: suiteName,
98
- passed: t.status === 'passed',
99
- duration: t.duration ?? 0,
100
- error: t.failureMessages?.join('\n') || null,
101
- });
102
- }
103
- }
104
-
105
- return result;
106
- }
107
- }
@@ -1,95 +0,0 @@
1
- /**
2
- * Mocha 測試執行器
3
- * 使用 --reporter json 取得結構化輸出
4
- */
5
-
6
- import { execSync } from 'child_process';
7
- import { ExecutorBase } from './executor-base.js';
8
-
9
- export class MochaExecutor extends ExecutorBase {
10
- constructor(options = {}) {
11
- super(options);
12
- this.framework = 'mocha';
13
- }
14
-
15
- /**
16
- * 執行 Mocha,回傳標準化 TestResults
17
- * @param {string[]} testPaths
18
- * @param {Object} options
19
- * @returns {Promise<import('./executor-base.js').TestResults>}
20
- */
21
- async run(testPaths = [], options = {}) {
22
- const pathArgs =
23
- testPaths.length > 0 ? testPaths.map((p) => `"${p}"`).join(' ') : '"test/**/*.js" "tests/**/*.js"';
24
- const cmd = `npx mocha --reporter json ${pathArgs}`;
25
-
26
- if (options.verbose) {
27
- console.log(` 執行命令:${cmd}`);
28
- }
29
-
30
- let output = '';
31
- try {
32
- output = execSync(cmd, {
33
- encoding: 'utf-8',
34
- timeout: this.timeout,
35
- stdio: ['pipe', 'pipe', 'pipe'],
36
- cwd: options.projectRoot || process.cwd(),
37
- });
38
- } catch (error) {
39
- // Mocha 失敗時 exit code 非 0,stdout 仍有 JSON
40
- output = error.stdout || error.output?.[1] || '';
41
- }
42
-
43
- return this.parseResults(output);
44
- }
45
-
46
- /**
47
- * 將 Mocha --reporter json 輸出解析為標準 TestResults
48
- * @param {string} output
49
- * @returns {import('./executor-base.js').TestResults}
50
- */
51
- parseResults(output) {
52
- const result = this.emptyResults(this.framework);
53
- result.rawOutput = output;
54
-
55
- let json;
56
- try {
57
- const start = output.indexOf('{');
58
- if (start === -1) return result;
59
- json = JSON.parse(output.slice(start));
60
- } catch (_) {
61
- return result;
62
- }
63
-
64
- const stats = json.stats || {};
65
- result.total = stats.tests ?? 0;
66
- result.passed = stats.passes ?? 0;
67
- result.failed = stats.failures ?? 0;
68
- result.skipped = stats.pending ?? 0;
69
- result.duration = stats.duration ?? 0;
70
-
71
- // passes
72
- for (const t of json.passes ?? []) {
73
- result.tests.push({
74
- name: t.fullTitle ?? t.title,
75
- suite: t.fullTitle?.replace(t.title, '').trim() ?? '',
76
- passed: true,
77
- duration: t.duration ?? 0,
78
- error: null,
79
- });
80
- }
81
-
82
- // failures
83
- for (const t of json.failures ?? []) {
84
- result.tests.push({
85
- name: t.fullTitle ?? t.title,
86
- suite: t.fullTitle?.replace(t.title, '').trim() ?? '',
87
- passed: false,
88
- duration: t.duration ?? 0,
89
- error: t.err?.message || t.err?.stack || null,
90
- });
91
- }
92
-
93
- return result;
94
- }
95
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * 測試結果格式化模組
3
- * 將 TestResults 轉換為 GitHub Markdown 格式,供發佈到 Issue
4
- */
5
-
6
- /** 唯一識別標記,用於在多次執行時更新同一則評論 */
7
- export const COMMENT_MARKER = '<!-- ai-autodev-test-results -->';
8
-
9
- export class ResultFormatter {
10
- /**
11
- * 將 TestResults 格式化為 Markdown 字串
12
- * @param {import('./executor-base.js').TestResults} results
13
- * @param {import('../core/issue-parser.js').IssueData} issueData
14
- * @param {Object} [meta] - 額外的 meta 資訊
15
- * @returns {string}
16
- */
17
- formatAsMarkdown(results, issueData, meta = {}) {
18
- const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
19
- const passRate = results.total > 0
20
- ? ((results.passed / results.total) * 100).toFixed(1)
21
- : '0.0';
22
- const allPassed = results.failed === 0 && results.total > 0;
23
- const statusEmoji = allPassed ? '✅' : '❌';
24
- const durationSec = (results.duration / 1000).toFixed(2);
25
-
26
- const lines = [
27
- COMMENT_MARKER,
28
- '',
29
- `## ${statusEmoji} 自動測試報告`,
30
- '',
31
- `> 由 \`ai autodev\` 自動執行 · ${now} UTC`,
32
- '',
33
- '### 摘要',
34
- '',
35
- '| 項目 | 結果 |',
36
- '|------|------|',
37
- `| 框架 | \`${results.framework}\` |`,
38
- `| 總測試數 | ${results.total} |`,
39
- `| ✅ 通過 | ${results.passed} |`,
40
- `| ❌ 失敗 | ${results.failed} |`,
41
- `| ⏭️ 跳過 | ${results.skipped} |`,
42
- `| 通過率 | **${passRate}%** |`,
43
- `| 執行時間 | ${durationSec}s |`,
44
- '',
45
- ];
46
-
47
- // 失敗詳情
48
- const failedTests = results.tests.filter((t) => !t.passed);
49
- if (failedTests.length > 0) {
50
- lines.push('### ❌ 失敗測試');
51
- lines.push('');
52
- for (const t of failedTests.slice(0, 20)) {
53
- lines.push(`#### \`${t.name}\``);
54
- if (t.suite) lines.push(`- **Suite**: ${t.suite}`);
55
- if (t.error) {
56
- lines.push('```');
57
- lines.push(t.error.slice(0, 500));
58
- lines.push('```');
59
- }
60
- lines.push('');
61
- }
62
- if (failedTests.length > 20) {
63
- lines.push(`> ... 還有 ${failedTests.length - 20} 個失敗測試未顯示`);
64
- lines.push('');
65
- }
66
- }
67
-
68
- // 後續步驟
69
- lines.push('### 下一步');
70
- lines.push('');
71
- if (allPassed) {
72
- lines.push('- 🎉 所有測試通過!');
73
- if (meta.commitHash) {
74
- lines.push(`- 📦 已自動提交:\`${meta.commitHash}\``);
75
- }
76
- if (meta.prUrl) {
77
- lines.push(`- 🔗 已自動建立 PR:${meta.prUrl}`);
78
- }
79
- } else {
80
- lines.push('- 請修復上方列出的失敗測試');
81
- lines.push('- 修復後重新執行 `ai autodev <issue>`');
82
- if (results.failed > 0) {
83
- lines.push(`- 或執行 \`npx ${results.framework} --watch\` 進行偵錯`);
84
- }
85
- }
86
-
87
- lines.push('');
88
- return lines.join('\n');
89
- }
90
- }