ai-git-tools 2.1.6 → 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。狀態由 Redmine API 提供選單,只有使用者選擇的 status 才會寫入。
353
+ Preview 會以色彩分開顯示每個 Issue 的開發內容、程式修改重點、API 變更、重要技術細節、修改檔案與實際 note。工具會從 Redmine API 找到 `已解決` 狀態並直接套用,不顯示互動式狀態選單。
354
354
 
355
- 原始 Issue description 會保留原始內容;AI 可產生多張流程圖,每張流程圖都有穩定的用途 ID。相同 ID 會更新原流程圖,不同 ID 會追加新流程圖;既有沒有 ID 的 Mermaid 會保留,內容相同時不重複追加。更新摘要會寫入 notes,並使用 Markdown 標題、項目符號與雙反引號標示程式路徑、API path、function 與 class。只有找到明確測試或驗證證據時才會加入驗證段落;流程圖會使用 Redmine Mermaid macro 並放在 description(公司 Redmine 的 notes 不支援 Mermaid):
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 note 的基本責任分工如下:`description` 保存原始需求,`status` 保存目前狀態,`notes` 保存 Git 開發與同步紀錄。
365
+ Issue 的基本責任分工如下:`description` 保存原始需求與未來其它內容,`status` 保存目前狀態,`notes` 保存 Git 開發紀錄與 Mermaid 流程圖。
366
366
 
367
- 當 preview 選擇「已解決」後套用,工具會同步將完成百分比設為 `100%`,並依欄位名稱找到 `程式碼更版進度` custom field,勾選測試機選項 `測`。custom field ID 會從 Issue API 動態取得,不會寫死公司 Redmine 的 ID。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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
- selectIssueStatus,
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 = selectIssueStatus,
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 selectedStatus = await selectStatusFn(issue, { client });
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.flowcharts);
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,6 +20,21 @@ function formatBehaviorRules(rules = []) {
20
20
  return `### 行為規則\n\n| 情境 | 行為 |\n|---|---|\n${rows.join('\n')}\n`;
21
21
  }
22
22
 
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
+
23
38
  const MERMAID_START = '<!-- ai-git-tools:mermaid:start';
24
39
  const MERMAID_END = '<!-- ai-git-tools:mermaid:end';
25
40
 
@@ -156,6 +171,7 @@ export function formatRedmineNote({ issueId, issue = {}, analysis = {}, evidence
156
171
  const unresolved = analysis.unresolvedItems?.length
157
172
  ? `### 待確認\n${analysis.unresolvedItems.map(item => `- [ ] ${item}`).join('\n')}\n`
158
173
  : '';
174
+ const flowcharts = formatFlowcharts(analysis.flowcharts);
159
175
 
160
176
  sections.push(
161
177
  development,
@@ -165,22 +181,30 @@ export function formatRedmineNote({ issueId, issue = {}, analysis = {}, evidence
165
181
  codeChanges,
166
182
  apiChanges,
167
183
  technicalDetails,
184
+ flowcharts,
168
185
  verification
169
186
  );
170
187
  sections.push(unresolved);
171
188
 
172
- if (evidence.currentBranch || evidence.baseBranch) {
173
- sections.push(
174
- `### Git\n\n- Branch:\`\`${evidence.currentBranch || '—'}\`\``,
175
- `- Base:\`\`${evidence.baseBranch || '—'}\`\``,
176
- ''
177
- );
178
- }
179
-
180
- if (pullRequest) {
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
+ }
181
206
  sections.push(
182
- `GitHub PR:${pullRequest.url || `#${pullRequest.number}`}`,
183
- `PR 狀態:${pullRequest.state || '—'}${pullRequest.mergedAt ? `(合併於 ${pullRequest.mergedAt})` : ''}`,
207
+ gitLines.join('\n'),
184
208
  ''
185
209
  );
186
210
  }
@@ -240,6 +264,19 @@ export async function selectIssueStatus(issue, { client, prompt = inquirer.promp
240
264
  return { statusId: answer.statusId, statusName: selected?.name || String(answer.statusId) };
241
265
  }
242
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
+
243
280
  /**
244
281
  * 產生有色 terminal preview
245
282
  * @param {Array<object>} drafts
@@ -254,6 +291,7 @@ export function formatRedminePreview(drafts = []) {
254
291
  `${chalk.dim('目前狀態:')} ${draft.currentStatusName || draft.originalStatusName || '—'}`,
255
292
  `${chalk.green('更新狀態:')} ${draft.statusName || '未選擇'}`,
256
293
  draft.doneRatio !== undefined ? `${chalk.green('完成百分比:')} ${draft.doneRatio}%` : '',
294
+ draft.dueDate ? `${chalk.green('完成日期:')} ${draft.dueDate}` : '',
257
295
  draft.customFields?.length
258
296
  ? `${chalk.green('程式碼更版進度:')} ${draft.customFields.flatMap(field => field.value || []).join('、')}`
259
297
  : '',