ai-git-tools 2.1.5 → 2.1.7
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/README.md
CHANGED
|
@@ -350,9 +350,9 @@ ai-git-tools redmine-update --apply --from redmine-update.json
|
|
|
350
350
|
|
|
351
351
|
`--pr <number>` 是可選的補充 evidence,會加入 PR title、body、URL、merge state、commits 與 changed files;沒有 PR 仍可完整分析本機 Git diff。沒有 PR 時,工具不會自動推論已 merge、已部署或必須關閉 Issue。
|
|
352
352
|
|
|
353
|
-
Preview 會以色彩分開顯示每個 Issue 的開發內容、程式修改重點、API 變更、重要技術細節、修改檔案與實際 note
|
|
353
|
+
Preview 會以色彩分開顯示每個 Issue 的開發內容、程式修改重點、API 變更、重要技術細節、修改檔案與實際 note。工具會從 Redmine API 找到 `已解決` 狀態並直接套用,不顯示互動式狀態選單。
|
|
354
354
|
|
|
355
|
-
原始 Issue description
|
|
355
|
+
原始 Issue description 會保留原始內容,供未來其它內容使用。AI 可產生多張流程圖,每張流程圖都有穩定的用途 ID,流程圖會放在 notes(公司 Redmine 已支援 notes Mermaid)。更新摘要會使用 Markdown 標題、項目符號與雙反引號標示程式路徑、API path、function 與 class。只有找到明確測試或驗證證據時才會加入驗證段落:
|
|
356
356
|
|
|
357
357
|
```text
|
|
358
358
|
{{mermaid
|
|
@@ -362,9 +362,9 @@ flowchart TD
|
|
|
362
362
|
}}
|
|
363
363
|
```
|
|
364
364
|
|
|
365
|
-
Issue
|
|
365
|
+
Issue 的基本責任分工如下:`description` 保存原始需求與未來其它內容,`status` 保存目前狀態,`notes` 保存 Git 開發紀錄與 Mermaid 流程圖。
|
|
366
366
|
|
|
367
|
-
當 preview
|
|
367
|
+
當 preview 套用時,工具會直接將狀態設為 `已解決`,並同步將完成百分比設為 `100%`、完成日期設為執行當天,依欄位名稱找到 `程式碼更版進度` custom field,勾選測試機選項 `測`。custom field ID 會從 Issue API 動態取得,不會寫死公司 Redmine 的 ID。
|
|
368
368
|
|
|
369
369
|
同一個 Issue 若已經有先前的 notes,後續再次執行更新仍會新增一筆新的 Redmine journal,不會因為已有 notes 而略過;只有 Issue 的需求內容或狀態在產生草稿後被其他人修改時,才會停下來要求重新確認。
|
|
370
370
|
|
package/package.json
CHANGED
|
@@ -12,6 +12,38 @@ const ANALYSIS_FIELDS = [
|
|
|
12
12
|
'evidence',
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
+
function normalizeFlowcharts(value) {
|
|
16
|
+
const flowcharts = Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: value
|
|
19
|
+
? [{ source: value }]
|
|
20
|
+
: [];
|
|
21
|
+
|
|
22
|
+
const usedIds = new Set();
|
|
23
|
+
return flowcharts
|
|
24
|
+
.filter(flowchart => flowchart && typeof flowchart === 'object')
|
|
25
|
+
.map((flowchart, index) => {
|
|
26
|
+
const title = typeof flowchart.title === 'string' ? flowchart.title.trim() : '';
|
|
27
|
+
const requestedId = typeof flowchart.id === 'string' ? flowchart.id.trim() : '';
|
|
28
|
+
const stableId = (requestedId || title || `flowchart-${index + 1}`)
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
31
|
+
.replace(/^-+|-+$/g, '') || `flowchart-${index + 1}`;
|
|
32
|
+
|
|
33
|
+
let uniqueId = stableId;
|
|
34
|
+
if (usedIds.has(uniqueId)) uniqueId = `${stableId}-${index + 1}`;
|
|
35
|
+
usedIds.add(uniqueId);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id: uniqueId,
|
|
39
|
+
title,
|
|
40
|
+
source: typeof flowchart.source === 'string' ? flowchart.source.trim() : '',
|
|
41
|
+
evidence: asStringArray(flowchart.evidence),
|
|
42
|
+
};
|
|
43
|
+
})
|
|
44
|
+
.filter(flowchart => flowchart.source);
|
|
45
|
+
}
|
|
46
|
+
|
|
15
47
|
function asStringArray(value) {
|
|
16
48
|
return Array.isArray(value) ? value.filter(item => typeof item === 'string') : [];
|
|
17
49
|
}
|
|
@@ -52,7 +84,8 @@ export function buildIssueAnalysisPrompt({ issue, evidence }) {
|
|
|
52
84
|
- 不得捏造 API endpoint、參數、檔案、function、class、測試或部署結果。
|
|
53
85
|
- 沒有明確測試或驗證證據時,verification 必須是空陣列。
|
|
54
86
|
- 沒有可靠 API 變更證據時,apiChanges 必須是空陣列。
|
|
55
|
-
-
|
|
87
|
+
- requirementDescription 應整理指定 Issue 的需求重點;behaviorRules 使用 scenario/behavior 物件陣列。
|
|
88
|
+
- 若能從實際流程推導出可靠的複製流程,才產生 flowcharts;否則為空陣列。每張流程圖必須有穩定、描述用途的 id,source 必須以 graph TD 開始,不要加入 Mermaid wrapper 或 code fence。
|
|
56
89
|
- developmentSummary、codeChanges、apiChanges、technicalDetails 中的程式路徑、API path、function 與 class 使用雙反引號標示;需要多行程式片段時使用 code block。
|
|
57
90
|
- 不要產生 suggestedStatus、coverage 或 confidence,也不要決定 Redmine status。
|
|
58
91
|
|
|
@@ -73,12 +106,12 @@ ${JSON.stringify(evidence, null, 2)}
|
|
|
73
106
|
"technicalDetails": [],
|
|
74
107
|
"modifiedFiles": [],
|
|
75
108
|
"verification": [],
|
|
76
|
-
"
|
|
109
|
+
"flowcharts": [],
|
|
77
110
|
"unresolvedItems": [],
|
|
78
111
|
"evidence": []
|
|
79
112
|
}
|
|
80
113
|
|
|
81
|
-
developmentSummary、codeChanges、apiChanges、technicalDetails、modifiedFiles、verification、unresolvedItems 與 evidence 必須是字串陣列;requirementDescription 必須是字串;behaviorRules 必須是包含 scenario 與 behavior 字串的物件陣列;
|
|
114
|
+
developmentSummary、codeChanges、apiChanges、technicalDetails、modifiedFiles、verification、unresolvedItems 與 evidence 必須是字串陣列;requirementDescription 必須是字串;behaviorRules 必須是包含 scenario 與 behavior 字串的物件陣列;flowcharts 必須是包含 id、title、source 與 evidence 的物件陣列。`;
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
/**
|
|
@@ -97,10 +130,9 @@ export function parseIssueAnalysis(content, issueId) {
|
|
|
97
130
|
: asStringArray(parsed[field]);
|
|
98
131
|
}
|
|
99
132
|
result.behaviorRules = asBehaviorRules(parsed.behaviorRules);
|
|
133
|
+
result.flowcharts = normalizeFlowcharts(parsed.flowcharts || parsed.flowchart);
|
|
100
134
|
|
|
101
|
-
result.flowchart =
|
|
102
|
-
? parsed.flowchart.trim()
|
|
103
|
-
: null;
|
|
135
|
+
result.flowchart = result.flowcharts[0]?.source || null;
|
|
104
136
|
|
|
105
137
|
return result;
|
|
106
138
|
}
|
|
@@ -2,15 +2,21 @@ import { analyzeIssue as defaultAnalyzeIssue } from './issue-analyzer.js';
|
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import {
|
|
4
4
|
formatRedmineNote,
|
|
5
|
-
formatRedmineDescription,
|
|
6
5
|
formatRedminePreview,
|
|
7
|
-
|
|
6
|
+
resolveIssueStatus,
|
|
8
7
|
} from './redmine-formatters.js';
|
|
9
8
|
|
|
10
9
|
function uniqueIssueIds(issueIds = []) {
|
|
11
10
|
return [...new Set(issueIds.map(id => Number(id)).filter(Number.isInteger))];
|
|
12
11
|
}
|
|
13
12
|
|
|
13
|
+
function getCurrentDate() {
|
|
14
|
+
const now = new Date();
|
|
15
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
16
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
17
|
+
return `${now.getFullYear()}-${month}-${day}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* 判斷 Redmine 狀態是否代表已解決
|
|
16
22
|
* @param {string} statusName
|
|
@@ -35,6 +41,7 @@ export function buildCompletionUpdate(issue, selectedStatus) {
|
|
|
35
41
|
if (!progressField) {
|
|
36
42
|
return {
|
|
37
43
|
doneRatio: 100,
|
|
44
|
+
dueDate: getCurrentDate(),
|
|
38
45
|
customFields: [],
|
|
39
46
|
warning: '找不到 custom field「程式碼更版進度」,已略過 checkbox 更新',
|
|
40
47
|
};
|
|
@@ -42,6 +49,7 @@ export function buildCompletionUpdate(issue, selectedStatus) {
|
|
|
42
49
|
|
|
43
50
|
return {
|
|
44
51
|
doneRatio: 100,
|
|
52
|
+
dueDate: getCurrentDate(),
|
|
45
53
|
customFields: [{ id: progressField.id, value: ['測'] }],
|
|
46
54
|
};
|
|
47
55
|
}
|
|
@@ -72,7 +80,7 @@ export async function generateRedmineDraft({
|
|
|
72
80
|
evidence,
|
|
73
81
|
pullRequest = null,
|
|
74
82
|
analyzeIssueFn = defaultAnalyzeIssue,
|
|
75
|
-
selectStatusFn =
|
|
83
|
+
selectStatusFn = resolveIssueStatus,
|
|
76
84
|
model,
|
|
77
85
|
maxRetries,
|
|
78
86
|
}) {
|
|
@@ -82,9 +90,11 @@ export async function generateRedmineDraft({
|
|
|
82
90
|
try {
|
|
83
91
|
const issue = await client.getIssue(issueId);
|
|
84
92
|
const analysis = await analyzeIssueFn({ issue, evidence: { ...evidence, pullRequest }, model, maxRetries });
|
|
85
|
-
const
|
|
93
|
+
const fallbackStatuses = issue.allowedStatuses?.length ? [] : await client.getIssueStatuses();
|
|
94
|
+
const selectedStatus = selectStatusFn === resolveIssueStatus
|
|
95
|
+
? resolveIssueStatus(issue, fallbackStatuses)
|
|
96
|
+
: await selectStatusFn(issue, { client });
|
|
86
97
|
const completion = buildCompletionUpdate(issue, selectedStatus);
|
|
87
|
-
const description = formatRedmineDescription(issue.description, analysis.flowchart);
|
|
88
98
|
const syncId = createSyncId({ issueId, evidence, pullRequest });
|
|
89
99
|
const note = formatRedmineNote({
|
|
90
100
|
issueId,
|
|
@@ -104,9 +114,9 @@ export async function generateRedmineDraft({
|
|
|
104
114
|
statusId: selectedStatus.statusId,
|
|
105
115
|
statusName: selectedStatus.statusName,
|
|
106
116
|
doneRatio: completion.doneRatio,
|
|
117
|
+
dueDate: completion.dueDate,
|
|
107
118
|
customFields: completion.customFields,
|
|
108
119
|
customFieldWarning: completion.warning || null,
|
|
109
|
-
description: description !== issue.description ? description : undefined,
|
|
110
120
|
analysis,
|
|
111
121
|
note,
|
|
112
122
|
syncId,
|
|
@@ -150,6 +160,7 @@ export async function applyRedmineDraft({ draft, client, force = false }) {
|
|
|
150
160
|
|
|
151
161
|
const update = { statusId: item.statusId, notes: item.note };
|
|
152
162
|
if (item.doneRatio !== undefined) update.doneRatio = item.doneRatio;
|
|
163
|
+
if (item.dueDate !== undefined) update.dueDate = item.dueDate;
|
|
153
164
|
if (item.description !== undefined) update.description = item.description;
|
|
154
165
|
if (item.customFields?.length) update.customFields = item.customFields;
|
|
155
166
|
|
|
@@ -131,13 +131,14 @@ export class RedmineClient {
|
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* @param {number|string} issueId
|
|
134
|
-
* @param {{statusId?: number, doneRatio?: number, description?: string, notes?: string, customFields?: Array<object>}} update
|
|
134
|
+
* @param {{statusId?: number, doneRatio?: number, dueDate?: string, description?: string, notes?: string, customFields?: Array<object>}} update
|
|
135
135
|
* @returns {Promise<object>}
|
|
136
136
|
*/
|
|
137
|
-
async updateIssue(issueId, { statusId, doneRatio, description, notes, customFields }) {
|
|
137
|
+
async updateIssue(issueId, { statusId, doneRatio, dueDate, description, notes, customFields }) {
|
|
138
138
|
const issue = {};
|
|
139
139
|
if (statusId !== undefined && statusId !== null) issue.status_id = statusId;
|
|
140
140
|
if (doneRatio !== undefined && doneRatio !== null) issue.done_ratio = doneRatio;
|
|
141
|
+
if (dueDate !== undefined && dueDate !== null) issue.due_date = dueDate;
|
|
141
142
|
if (description !== undefined) issue.description = description;
|
|
142
143
|
if (notes) issue.notes = notes;
|
|
143
144
|
if (Array.isArray(customFields) && customFields.length > 0) {
|
|
@@ -20,8 +20,67 @@ function formatBehaviorRules(rules = []) {
|
|
|
20
20
|
return `### 行為規則\n\n| 情境 | 行為 |\n|---|---|\n${rows.join('\n')}\n`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
function formatFlowcharts(flowcharts = []) {
|
|
24
|
+
if (!Array.isArray(flowcharts) || flowcharts.length === 0) return '';
|
|
25
|
+
|
|
26
|
+
return flowcharts
|
|
27
|
+
.map((flowchart, index) => {
|
|
28
|
+
const mermaid = formatMermaid(flowchart?.source);
|
|
29
|
+
if (!mermaid) return '';
|
|
30
|
+
const id = normalizeFlowchartId(flowchart?.id, index);
|
|
31
|
+
const title = flowchart?.title?.trim() || '開發流程';
|
|
32
|
+
return `### 流程圖:${title}\n\n<!-- flowchart-id: ${id} -->\n${mermaid}\n`;
|
|
33
|
+
})
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const MERMAID_START = '<!-- ai-git-tools:mermaid:start';
|
|
39
|
+
const MERMAID_END = '<!-- ai-git-tools:mermaid:end';
|
|
40
|
+
|
|
41
|
+
function escapeRegExp(value) {
|
|
42
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeFlowchartId(id, index) {
|
|
46
|
+
const source = typeof id === 'string' && id.trim() ? id : `flowchart-${index + 1}`;
|
|
47
|
+
return source
|
|
48
|
+
.trim()
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
51
|
+
.replace(/^-+|-+$/g, '') || `flowchart-${index + 1}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeFlowchartInput(flowcharts) {
|
|
55
|
+
if (Array.isArray(flowcharts)) return flowcharts;
|
|
56
|
+
if (typeof flowcharts === 'string' && flowcharts.trim()) {
|
|
57
|
+
return [{ id: 'flowchart-1', title: '開發流程', source: flowcharts }];
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasSameMermaid(description, mermaid) {
|
|
63
|
+
const existing = description.match(/\{\{\s*mermaid\b[\s\S]*?\}\}/gi) || [];
|
|
64
|
+
return existing.some(block => formatMermaid(block) === mermaid);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function managedFlowchartPattern(id) {
|
|
68
|
+
const escapedId = escapeRegExp(id);
|
|
69
|
+
return new RegExp(
|
|
70
|
+
`${escapeRegExp(MERMAID_START)} id="${escapedId}" -->[\\s\\S]*?${escapeRegExp(MERMAID_END)} id="${escapedId}" -->`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatManagedFlowchart(flowchart, index) {
|
|
75
|
+
const mermaid = formatMermaid(flowchart.source);
|
|
76
|
+
if (!mermaid) return '';
|
|
77
|
+
|
|
78
|
+
const id = normalizeFlowchartId(flowchart.id, index);
|
|
79
|
+
const title = typeof flowchart.title === 'string' && flowchart.title.trim()
|
|
80
|
+
? flowchart.title.trim()
|
|
81
|
+
: '開發流程';
|
|
82
|
+
return `${MERMAID_START} id="${id}" -->\n\n### 流程圖:${title}\n\n${mermaid}\n\n${MERMAID_END} id="${id}" -->`;
|
|
83
|
+
}
|
|
25
84
|
|
|
26
85
|
/**
|
|
27
86
|
* 將 Mermaid source 包成 Redmine macro
|
|
@@ -37,9 +96,9 @@ export function formatMermaid(source) {
|
|
|
37
96
|
.replace(/\s*```$/, '')
|
|
38
97
|
.trim();
|
|
39
98
|
|
|
40
|
-
if (
|
|
99
|
+
if (/^\{\{\s*mermaid\b/i.test(cleanSource)) {
|
|
41
100
|
cleanSource = cleanSource
|
|
42
|
-
.replace(/^\{\{mermaid\s
|
|
101
|
+
.replace(/^\{\{\s*mermaid\s*/i, '')
|
|
43
102
|
.replace(/\s*\}\}$/, '')
|
|
44
103
|
.trim();
|
|
45
104
|
}
|
|
@@ -64,19 +123,27 @@ export function formatMermaid(source) {
|
|
|
64
123
|
* @returns {string}
|
|
65
124
|
*/
|
|
66
125
|
export function formatRedmineDescription(description = '', source = null) {
|
|
67
|
-
|
|
68
|
-
|
|
126
|
+
let result = description;
|
|
127
|
+
const flowcharts = normalizeFlowchartInput(source);
|
|
69
128
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
);
|
|
129
|
+
flowcharts.forEach((flowchart, index) => {
|
|
130
|
+
const mermaid = formatMermaid(flowchart?.source);
|
|
131
|
+
if (!mermaid) return;
|
|
74
132
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
const block = formatManagedFlowchart(flowchart, index);
|
|
134
|
+
const id = normalizeFlowchartId(flowchart?.id, index);
|
|
135
|
+
const markerPattern = managedFlowchartPattern(id);
|
|
136
|
+
if (markerPattern.test(result)) {
|
|
137
|
+
result = result.replace(markerPattern, block);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
78
140
|
|
|
79
|
-
|
|
141
|
+
if (!hasSameMermaid(result, mermaid)) {
|
|
142
|
+
result = `${result.trim()}\n\n${block}`.trim();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return result;
|
|
80
147
|
}
|
|
81
148
|
|
|
82
149
|
/**
|
|
@@ -104,6 +171,7 @@ export function formatRedmineNote({ issueId, issue = {}, analysis = {}, evidence
|
|
|
104
171
|
const unresolved = analysis.unresolvedItems?.length
|
|
105
172
|
? `### 待確認\n${analysis.unresolvedItems.map(item => `- [ ] ${item}`).join('\n')}\n`
|
|
106
173
|
: '';
|
|
174
|
+
const flowcharts = formatFlowcharts(analysis.flowcharts);
|
|
107
175
|
|
|
108
176
|
sections.push(
|
|
109
177
|
development,
|
|
@@ -113,22 +181,30 @@ export function formatRedmineNote({ issueId, issue = {}, analysis = {}, evidence
|
|
|
113
181
|
codeChanges,
|
|
114
182
|
apiChanges,
|
|
115
183
|
technicalDetails,
|
|
184
|
+
flowcharts,
|
|
116
185
|
verification
|
|
117
186
|
);
|
|
118
187
|
sections.push(unresolved);
|
|
119
188
|
|
|
120
|
-
if (evidence.currentBranch || evidence.baseBranch) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
''
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
189
|
+
if (evidence.currentBranch || evidence.baseBranch || pullRequest) {
|
|
190
|
+
const branch = evidence.currentBranch || '—';
|
|
191
|
+
const branchUrl = evidence.repository
|
|
192
|
+
? `https://github.com/${evidence.repository}/tree/${encodeURI(branch)}`
|
|
193
|
+
: '';
|
|
194
|
+
const gitLines = [
|
|
195
|
+
'### Git',
|
|
196
|
+
'',
|
|
197
|
+
`- Branch:${branchUrl ? `[${branch}](${branchUrl})` : `\`\`${branch}\`\``}`,
|
|
198
|
+
`- Base:${evidence.baseBranch || '—'}`,
|
|
199
|
+
];
|
|
200
|
+
if (pullRequest) {
|
|
201
|
+
gitLines.push(
|
|
202
|
+
`- PR:${pullRequest.url ? `[${pullRequest.url}](${pullRequest.url})` : `#${pullRequest.number}`}`,
|
|
203
|
+
`- PR 狀態:${pullRequest.state || '—'}${pullRequest.mergedAt ? `(合併於 ${pullRequest.mergedAt})` : ''}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
129
206
|
sections.push(
|
|
130
|
-
|
|
131
|
-
`PR 狀態:${pullRequest.state || '—'}${pullRequest.mergedAt ? `(合併於 ${pullRequest.mergedAt})` : ''}`,
|
|
207
|
+
gitLines.join('\n'),
|
|
132
208
|
''
|
|
133
209
|
);
|
|
134
210
|
}
|
|
@@ -188,6 +264,19 @@ export async function selectIssueStatus(issue, { client, prompt = inquirer.promp
|
|
|
188
264
|
return { statusId: answer.statusId, statusName: selected?.name || String(answer.statusId) };
|
|
189
265
|
}
|
|
190
266
|
|
|
267
|
+
/**
|
|
268
|
+
* 直接取得已解決狀態,不啟動互動式選單
|
|
269
|
+
* @param {object} issue
|
|
270
|
+
* @param {Array<object>} fallbackStatuses
|
|
271
|
+
* @returns {{statusId: number, statusName: string}}
|
|
272
|
+
*/
|
|
273
|
+
export function resolveIssueStatus(issue, fallbackStatuses = []) {
|
|
274
|
+
const statuses = getAvailableStatuses(issue, fallbackStatuses);
|
|
275
|
+
const resolved = statuses.find(status => /已解決|resolved/i.test(status.name || ''));
|
|
276
|
+
if (!resolved) throw new Error(`Issue #${issue.id} 找不到「已解決」狀態`);
|
|
277
|
+
return { statusId: resolved.id, statusName: resolved.name };
|
|
278
|
+
}
|
|
279
|
+
|
|
191
280
|
/**
|
|
192
281
|
* 產生有色 terminal preview
|
|
193
282
|
* @param {Array<object>} drafts
|
|
@@ -202,6 +291,7 @@ export function formatRedminePreview(drafts = []) {
|
|
|
202
291
|
`${chalk.dim('目前狀態:')} ${draft.currentStatusName || draft.originalStatusName || '—'}`,
|
|
203
292
|
`${chalk.green('更新狀態:')} ${draft.statusName || '未選擇'}`,
|
|
204
293
|
draft.doneRatio !== undefined ? `${chalk.green('完成百分比:')} ${draft.doneRatio}%` : '',
|
|
294
|
+
draft.dueDate ? `${chalk.green('完成日期:')} ${draft.dueDate}` : '',
|
|
205
295
|
draft.customFields?.length
|
|
206
296
|
? `${chalk.green('程式碼更版進度:')} ${draft.customFields.flatMap(field => field.value || []).join('、')}`
|
|
207
297
|
: '',
|