team-toon-tack 1.0.11 → 1.6.0

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,64 +1,70 @@
1
- import {
2
- require_prompts
3
- } from "../cli-6rkvcjaj.js";
4
- import {
5
- getLinearClient,
6
- getTeamId,
7
- loadConfig,
8
- loadCycleData,
9
- loadLocalConfig,
10
- saveCycleData
11
- } from "./utils.js";
12
- import {
13
- __toESM
14
- } from "../cli-pyanjjwn.js";
15
-
16
- // scripts/done-job.ts
17
- var import_prompts = __toESM(require_prompts(), 1);
18
1
  import { execSync } from "node:child_process";
2
+ import prompts from "prompts";
3
+ import { getLinearClient, getTeamId, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
19
4
  async function getLatestCommit() {
20
- try {
21
- const shortHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
22
- const fullHash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
23
- const message = execSync("git log -1 --format=%s", { encoding: "utf-8" }).trim();
24
- const diffStat = execSync("git diff HEAD~1 --stat --stat-width=60", { encoding: "utf-8" }).trim();
25
- let commitUrl = null;
26
5
  try {
27
- const remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
28
- if (remoteUrl.includes("gitlab")) {
29
- const match = remoteUrl.match(/(?:git@|https:\/\/)([^:\/]+)[:\\/](.+?)(?:\.git)?$/);
30
- if (match) {
31
- commitUrl = `https://${match[1]}/${match[2]}/-/commit/${fullHash}`;
6
+ const shortHash = execSync("git rev-parse --short HEAD", {
7
+ encoding: "utf-8",
8
+ }).trim();
9
+ const fullHash = execSync("git rev-parse HEAD", {
10
+ encoding: "utf-8",
11
+ }).trim();
12
+ const message = execSync("git log -1 --format=%s", {
13
+ encoding: "utf-8",
14
+ }).trim();
15
+ const diffStat = execSync("git diff HEAD~1 --stat --stat-width=60", {
16
+ encoding: "utf-8",
17
+ }).trim();
18
+ // Get remote URL and construct commit link
19
+ let commitUrl = null;
20
+ try {
21
+ const remoteUrl = execSync("git remote get-url origin", {
22
+ encoding: "utf-8",
23
+ }).trim();
24
+ // Handle SSH or HTTPS URLs
25
+ // git@gitlab.com:org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
26
+ // https://gitlab.com/org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
27
+ if (remoteUrl.includes("gitlab")) {
28
+ const match = remoteUrl.match(/(?:git@|https:\/\/)([^:/]+)[:\\/](.+?)(?:\.git)?$/);
29
+ if (match) {
30
+ commitUrl = `https://${match[1]}/${match[2]}/-/commit/${fullHash}`;
31
+ }
32
+ }
33
+ else if (remoteUrl.includes("github")) {
34
+ const match = remoteUrl.match(/(?:git@|https:\/\/)([^:/]+)[:\\/](.+?)(?:\.git)?$/);
35
+ if (match) {
36
+ commitUrl = `https://${match[1]}/${match[2]}/commit/${fullHash}`;
37
+ }
38
+ }
32
39
  }
33
- } else if (remoteUrl.includes("github")) {
34
- const match = remoteUrl.match(/(?:git@|https:\/\/)([^:\/]+)[:\\/](.+?)(?:\.git)?$/);
35
- if (match) {
36
- commitUrl = `https://${match[1]}/${match[2]}/commit/${fullHash}`;
40
+ catch {
41
+ // Ignore if can't get remote URL
37
42
  }
38
- }
39
- } catch {}
40
- return { shortHash, fullHash, message, diffStat, commitUrl };
41
- } catch {
42
- return null;
43
- }
43
+ return { shortHash, fullHash, message, diffStat, commitUrl };
44
+ }
45
+ catch {
46
+ return null;
47
+ }
44
48
  }
45
49
  function parseArgs(args) {
46
- let issueId;
47
- let message;
48
- for (let i = 0;i < args.length; i++) {
49
- const arg = args[i];
50
- if (arg === "-m" || arg === "--message") {
51
- message = args[++i];
52
- } else if (!arg.startsWith("-")) {
53
- issueId = arg;
50
+ let issueId;
51
+ let message;
52
+ for (let i = 0; i < args.length; i++) {
53
+ const arg = args[i];
54
+ if (arg === "-m" || arg === "--message") {
55
+ message = args[++i];
56
+ }
57
+ else if (!arg.startsWith("-")) {
58
+ issueId = arg;
59
+ }
54
60
  }
55
- }
56
- return { issueId, message };
61
+ return { issueId, message };
57
62
  }
58
63
  async function doneJob() {
59
- const args = process.argv.slice(2);
60
- if (args.includes("--help") || args.includes("-h")) {
61
- console.log(`Usage: ttt done [issue-id] [-m message]
64
+ const args = process.argv.slice(2);
65
+ // Handle help flag
66
+ if (args.includes("--help") || args.includes("-h")) {
67
+ console.log(`Usage: ttt done [issue-id] [-m message]
62
68
 
63
69
  Arguments:
64
70
  issue-id Issue ID (e.g., MP-624). Optional if only one task is in-progress
@@ -71,148 +77,171 @@ Examples:
71
77
  ttt done MP-624 # Complete specific task
72
78
  ttt done -m "Fixed null check" # With completion message
73
79
  ttt done MP-624 -m "Refactored" # Specific task with message`);
74
- process.exit(0);
75
- }
76
- const { issueId: argIssueId, message: argMessage } = parseArgs(args);
77
- let issueId = argIssueId;
78
- const config = await loadConfig();
79
- const localConfig = await loadLocalConfig();
80
- const data = await loadCycleData();
81
- if (!data) {
82
- console.error("No cycle data found. Run /sync-linear first.");
83
- process.exit(1);
84
- }
85
- const inProgressTasks = data.tasks.filter((t) => t.localStatus === "in-progress");
86
- if (inProgressTasks.length === 0) {
87
- console.log("沒有進行中的任務");
88
- process.exit(0);
89
- }
90
- if (!issueId) {
91
- if (inProgressTasks.length === 1) {
92
- issueId = inProgressTasks[0].id;
93
- console.log(`Auto-selected: ${issueId}`);
94
- } else if (process.stdin.isTTY) {
95
- const choices = inProgressTasks.map((task2) => ({
96
- title: `${task2.id}: ${task2.title}`,
97
- value: task2.id,
98
- description: task2.labels.join(", ")
99
- }));
100
- const response = await import_prompts.default({
101
- type: "select",
102
- name: "issueId",
103
- message: "選擇要完成的任務:",
104
- choices
105
- });
106
- if (!response.issueId) {
107
- console.log("已取消");
108
80
  process.exit(0);
109
- }
110
- issueId = response.issueId;
111
- } else {
112
- console.error("多個進行中任務,請指定 issue ID:");
113
- inProgressTasks.forEach((t) => console.log(` - ${t.id}: ${t.title}`));
114
- process.exit(1);
115
81
  }
116
- }
117
- const task = data.tasks.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
118
- if (!task) {
119
- console.error(`Issue ${issueId} not found in current cycle.`);
120
- process.exit(1);
121
- }
122
- if (task.localStatus !== "in-progress") {
123
- console.log(`⚠️ 任務 ${task.id} 不在進行中狀態 (目前: ${task.localStatus})`);
124
- process.exit(1);
125
- }
126
- const commit = await getLatestCommit();
127
- let aiMessage = argMessage || "";
128
- if (!aiMessage && process.stdin.isTTY) {
129
- const aiMsgResponse = await import_prompts.default({
130
- type: "text",
131
- name: "aiMessage",
132
- message: "AI 修復說明 (如何解決此問題):"
133
- });
134
- aiMessage = aiMsgResponse.aiMessage || "";
135
- }
136
- if (task.linearId && process.env.LINEAR_API_KEY) {
137
- try {
138
- const client = getLinearClient();
139
- const workflowStates = await client.workflowStates({
140
- filter: { team: { id: { eq: getTeamId(config, localConfig.team) } } }
141
- });
142
- const doneState = workflowStates.nodes.find((s) => s.name === "Done");
143
- if (doneState) {
144
- await client.updateIssue(task.linearId, { stateId: doneState.id });
145
- console.log(`Linear: ${task.id} → Done`);
146
- }
147
- if (commit) {
148
- const commitLink = commit.commitUrl ? `[${commit.shortHash}](${commit.commitUrl})` : `\`${commit.shortHash}\``;
149
- const commentParts = [
150
- "## ✅ 開發完成",
151
- "",
152
- "### \uD83E\uDD16 AI 修復說明",
153
- aiMessage || "_No description provided_",
154
- "",
155
- "### \uD83D\uDCDD Commit Info",
156
- `**Commit:** ${commitLink}`,
157
- `**Message:** ${commit.message}`,
158
- "",
159
- "### \uD83D\uDCCA Changes",
160
- "```",
161
- commit.diffStat,
162
- "```"
163
- ];
164
- await client.createComment({
165
- issueId: task.linearId,
166
- body: commentParts.join(`
167
- `)
82
+ const { issueId: argIssueId, message: argMessage } = parseArgs(args);
83
+ let issueId = argIssueId;
84
+ const config = await loadConfig();
85
+ const localConfig = await loadLocalConfig();
86
+ const data = await loadCycleData();
87
+ if (!data) {
88
+ console.error("No cycle data found. Run /sync-linear first.");
89
+ process.exit(1);
90
+ }
91
+ // Find in-progress tasks
92
+ const inProgressTasks = data.tasks.filter((t) => t.localStatus === "in-progress");
93
+ if (inProgressTasks.length === 0) {
94
+ console.log("沒有進行中的任務");
95
+ process.exit(0);
96
+ }
97
+ // Phase 0: Issue Resolution
98
+ if (!issueId) {
99
+ if (inProgressTasks.length === 1) {
100
+ issueId = inProgressTasks[0].id;
101
+ console.log(`Auto-selected: ${issueId}`);
102
+ }
103
+ else if (process.stdin.isTTY) {
104
+ const choices = inProgressTasks.map((task) => ({
105
+ title: `${task.id}: ${task.title}`,
106
+ value: task.id,
107
+ description: task.labels.join(", "),
108
+ }));
109
+ const response = await prompts({
110
+ type: "select",
111
+ name: "issueId",
112
+ message: "選擇要完成的任務:",
113
+ choices: choices,
114
+ });
115
+ if (!response.issueId) {
116
+ console.log("已取消");
117
+ process.exit(0);
118
+ }
119
+ issueId = response.issueId;
120
+ }
121
+ else {
122
+ console.error("多個進行中任務,請指定 issue ID:");
123
+ for (const t of inProgressTasks) {
124
+ console.log(` - ${t.id}: ${t.title}`);
125
+ }
126
+ process.exit(1);
127
+ }
128
+ }
129
+ // Phase 1: Find task
130
+ const task = data.tasks.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
131
+ if (!task) {
132
+ console.error(`Issue ${issueId} not found in current cycle.`);
133
+ process.exit(1);
134
+ }
135
+ if (task.localStatus !== "in-progress") {
136
+ console.log(`⚠️ 任務 ${task.id} 不在進行中狀態 (目前: ${task.localStatus})`);
137
+ process.exit(1);
138
+ }
139
+ // Get latest commit for comment
140
+ const commit = await getLatestCommit();
141
+ // Phase 2: Get AI summary message
142
+ let aiMessage = argMessage || "";
143
+ if (!aiMessage && process.stdin.isTTY) {
144
+ const aiMsgResponse = await prompts({
145
+ type: "text",
146
+ name: "aiMessage",
147
+ message: "AI 修復說明 (如何解決此問題):",
168
148
  });
169
- console.log(`Linear: 已新增 commit 留言`);
170
- }
171
- if (task.parentIssueId) {
149
+ aiMessage = aiMsgResponse.aiMessage || "";
150
+ }
151
+ // Phase 3: Update Linear
152
+ if (task.linearId && process.env.LINEAR_API_KEY) {
172
153
  try {
173
- const searchResult = await client.searchIssues(task.parentIssueId);
174
- const parentIssue = searchResult.nodes.find((issue) => issue.identifier === task.parentIssueId);
175
- if (parentIssue) {
176
- const parentTeam = await parentIssue.team;
177
- if (parentTeam) {
178
- const parentWorkflowStates = await client.workflowStates({
179
- filter: { team: { id: { eq: parentTeam.id } } }
180
- });
181
- const testingState = parentWorkflowStates.nodes.find((s) => s.name === "Testing");
182
- if (testingState) {
183
- await client.updateIssue(parentIssue.id, { stateId: testingState.id });
184
- console.log(`Linear: Parent ${task.parentIssueId} → Testing`);
185
- }
154
+ const client = getLinearClient();
155
+ const workflowStates = await client.workflowStates({
156
+ filter: { team: { id: { eq: getTeamId(config, localConfig.team) } } },
157
+ });
158
+ // Get status names from config or use defaults
159
+ const doneStatusName = config.status_transitions?.done || "Done";
160
+ const testingStatusName = config.status_transitions?.testing || "Testing";
161
+ const doneState = workflowStates.nodes.find((s) => s.name === doneStatusName);
162
+ // Update issue to Done
163
+ if (doneState) {
164
+ await client.updateIssue(task.linearId, { stateId: doneState.id });
165
+ console.log(`Linear: ${task.id} → ${doneStatusName}`);
186
166
  }
187
- }
188
- } catch (parentError) {
189
- console.error("Failed to update parent issue:", parentError);
167
+ // Add comment with commit info and AI summary
168
+ if (commit) {
169
+ const commitLink = commit.commitUrl
170
+ ? `[${commit.shortHash}](${commit.commitUrl})`
171
+ : `\`${commit.shortHash}\``;
172
+ const commentParts = [
173
+ "## ✅ 開發完成",
174
+ "",
175
+ "### 🤖 AI 修復說明",
176
+ aiMessage || "_No description provided_",
177
+ "",
178
+ "### 📝 Commit Info",
179
+ `**Commit:** ${commitLink}`,
180
+ `**Message:** ${commit.message}`,
181
+ "",
182
+ "### 📊 Changes",
183
+ "```",
184
+ commit.diffStat,
185
+ "```",
186
+ ];
187
+ await client.createComment({
188
+ issueId: task.linearId,
189
+ body: commentParts.join("\n"),
190
+ });
191
+ console.log(`Linear: 已新增 commit 留言`);
192
+ }
193
+ // Update parent to Testing if exists and testing status is configured
194
+ if (task.parentIssueId && testingStatusName) {
195
+ try {
196
+ // Find parent issue by identifier
197
+ const searchResult = await client.searchIssues(task.parentIssueId);
198
+ const parentIssue = searchResult.nodes.find((issue) => issue.identifier === task.parentIssueId);
199
+ if (parentIssue) {
200
+ // Get parent's team workflow states
201
+ const parentTeam = await parentIssue.team;
202
+ if (parentTeam) {
203
+ const parentWorkflowStates = await client.workflowStates({
204
+ filter: { team: { id: { eq: parentTeam.id } } },
205
+ });
206
+ const testingState = parentWorkflowStates.nodes.find((s) => s.name === testingStatusName);
207
+ if (testingState) {
208
+ await client.updateIssue(parentIssue.id, {
209
+ stateId: testingState.id,
210
+ });
211
+ console.log(`Linear: Parent ${task.parentIssueId} → ${testingStatusName}`);
212
+ }
213
+ }
214
+ }
215
+ }
216
+ catch (parentError) {
217
+ console.error("Failed to update parent issue:", parentError);
218
+ }
219
+ }
220
+ }
221
+ catch (e) {
222
+ console.error("Failed to update Linear:", e);
190
223
  }
191
- }
192
- } catch (e) {
193
- console.error("Failed to update Linear:", e);
194
224
  }
195
- }
196
- task.localStatus = "completed";
197
- await saveCycleData(data);
198
- console.log(`Local: ${task.id} → completed`);
199
- console.log(`
200
- ${"═".repeat(50)}`);
201
- console.log(`✅ ${task.id}: ${task.title}`);
202
- console.log(`${"═".repeat(50)}`);
203
- if (commit) {
204
- console.log(`Commit: ${commit.shortHash} - ${commit.message}`);
205
- if (commit.commitUrl) {
206
- console.log(`URL: ${commit.commitUrl}`);
225
+ // Phase 4: Update local status
226
+ task.localStatus = "completed";
227
+ await saveCycleData(data);
228
+ console.log(`Local: ${task.id} → completed`);
229
+ // Phase 5: Summary
230
+ console.log(`\n${"═".repeat(50)}`);
231
+ console.log(`✅ ${task.id}: ${task.title}`);
232
+ console.log(`${"═".repeat(50)}`);
233
+ if (commit) {
234
+ console.log(`Commit: ${commit.shortHash} - ${commit.message}`);
235
+ if (commit.commitUrl) {
236
+ console.log(`URL: ${commit.commitUrl}`);
237
+ }
238
+ }
239
+ if (aiMessage) {
240
+ console.log(`AI: ${aiMessage}`);
241
+ }
242
+ if (task.parentIssueId && config.status_transitions?.testing) {
243
+ console.log(`Parent: ${task.parentIssueId} → ${config.status_transitions.testing}`);
207
244
  }
208
- }
209
- if (aiMessage) {
210
- console.log(`AI: ${aiMessage}`);
211
- }
212
- if (task.parentIssueId) {
213
- console.log(`Parent: ${task.parentIssueId} → Testing`);
214
- }
215
- console.log(`
216
- \uD83C\uDF89 任務完成!`);
245
+ console.log(`\n🎉 任務完成!`);
217
246
  }
218
247
  doneJob().catch(console.error);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};