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.
- package/README.md +59 -8
- package/README.zh-TW.md +111 -22
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +87 -61
- package/dist/scripts/config.d.ts +2 -0
- package/dist/scripts/config.js +271 -0
- package/dist/scripts/done-job.d.ts +1 -0
- package/dist/scripts/done-job.js +215 -186
- package/dist/scripts/init.d.ts +2 -0
- package/dist/scripts/init.js +457 -278
- package/dist/scripts/status.d.ts +2 -0
- package/dist/scripts/status.js +251 -0
- package/dist/scripts/sync.d.ts +1 -0
- package/dist/scripts/sync.js +228 -152
- package/dist/scripts/utils.d.ts +109 -0
- package/dist/scripts/utils.js +116 -14766
- package/dist/scripts/work-on.d.ts +1 -0
- package/dist/scripts/work-on.js +132 -122
- package/package.json +52 -48
- package/templates/claude-code-commands/done-job.md +45 -0
- package/templates/claude-code-commands/sync-linear.md +32 -0
- package/templates/claude-code-commands/work-on.md +41 -0
- package/dist/cli-6rkvcjaj.js +0 -4923
- package/dist/cli-pyanjjwn.js +0 -21
package/dist/scripts/done-job.js
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
43
|
+
return { shortHash, fullHash, message, diffStat, commitUrl };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
49
|
function parseArgs(args) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
149
|
+
aiMessage = aiMsgResponse.aiMessage || "";
|
|
150
|
+
}
|
|
151
|
+
// Phase 3: Update Linear
|
|
152
|
+
if (task.linearId && process.env.LINEAR_API_KEY) {
|
|
172
153
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await client.updateIssue(
|
|
184
|
-
console.log(`Linear:
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
${"═".repeat(50)}`);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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);
|