team-toon-tack 1.0.12 → 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/{bin → dist/bin}/cli.js +41 -19
- package/dist/scripts/config.js +271 -0
- package/{scripts → dist/scripts}/done-job.js +79 -62
- package/dist/scripts/init.d.ts +2 -0
- package/dist/scripts/init.js +492 -0
- package/dist/scripts/status.d.ts +2 -0
- package/dist/scripts/status.js +251 -0
- package/dist/scripts/sync.js +247 -0
- package/{scripts → dist/scripts}/utils.d.ts +11 -3
- package/{scripts → dist/scripts}/utils.js +33 -27
- package/{scripts → dist/scripts}/work-on.js +55 -41
- package/package.json +52 -50
- 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/bin/cli.ts +0 -125
- package/scripts/done-job.ts +0 -263
- package/scripts/init.js +0 -331
- package/scripts/init.ts +0 -375
- package/scripts/sync.js +0 -178
- package/scripts/sync.ts +0 -211
- package/scripts/utils.ts +0 -236
- package/scripts/work-on.ts +0 -161
- /package/{bin → dist/bin}/cli.d.ts +0 -0
- /package/{scripts/init.d.ts → dist/scripts/config.d.ts} +0 -0
- /package/{scripts → dist/scripts}/done-job.d.ts +0 -0
- /package/{scripts → dist/scripts}/sync.d.ts +0 -0
- /package/{scripts → dist/scripts}/work-on.d.ts +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import prompts from "prompts";
|
|
3
|
+
import { getLinearClient, getTeamId, loadConfig, loadLocalConfig, saveConfig, saveLocalConfig, } from "./utils.js";
|
|
4
|
+
async function config() {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
// Handle help flag
|
|
7
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
8
|
+
console.log(`Usage: ttt config [subcommand]
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
show Show current configuration
|
|
12
|
+
status Configure status mappings (todo, in_progress, done, testing)
|
|
13
|
+
filters Configure filters (label, exclude_labels, exclude_assignees)
|
|
14
|
+
teams Configure team selection
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
ttt config # Show current config
|
|
18
|
+
ttt config show # Show current config
|
|
19
|
+
ttt config status # Configure status mappings
|
|
20
|
+
ttt config filters # Configure filter settings
|
|
21
|
+
ttt config teams # Configure team selection`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
const subcommand = args[0] || "show";
|
|
25
|
+
const configData = await loadConfig();
|
|
26
|
+
const localConfig = await loadLocalConfig();
|
|
27
|
+
if (subcommand === "show") {
|
|
28
|
+
console.log("📋 Current Configuration:\n");
|
|
29
|
+
// Teams
|
|
30
|
+
console.log("Teams:");
|
|
31
|
+
if (localConfig.teams && localConfig.teams.length > 0) {
|
|
32
|
+
console.log(` Selected: ${localConfig.teams.join(", ")}`);
|
|
33
|
+
console.log(` Primary: ${localConfig.team}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.log(` ${localConfig.team}`);
|
|
37
|
+
}
|
|
38
|
+
// User
|
|
39
|
+
console.log(`\nUser: ${localConfig.current_user}`);
|
|
40
|
+
// Filters
|
|
41
|
+
console.log("\nFilters:");
|
|
42
|
+
console.log(` Label: ${localConfig.label || "(all)"}`);
|
|
43
|
+
console.log(` Exclude labels: ${localConfig.exclude_labels?.join(", ") || "(none)"}`);
|
|
44
|
+
console.log(` Exclude users: ${localConfig.exclude_assignees?.join(", ") || "(none)"}`);
|
|
45
|
+
// Status Mappings
|
|
46
|
+
console.log("\nStatus Mappings:");
|
|
47
|
+
if (configData.status_transitions) {
|
|
48
|
+
const st = configData.status_transitions;
|
|
49
|
+
console.log(` Todo: ${st.todo}`);
|
|
50
|
+
console.log(` In Progress: ${st.in_progress}`);
|
|
51
|
+
console.log(` Done: ${st.done}`);
|
|
52
|
+
if (st.testing) {
|
|
53
|
+
console.log(` Testing: ${st.testing}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log(" (not configured)");
|
|
58
|
+
}
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
if (subcommand === "status") {
|
|
62
|
+
console.log("📊 Configure Status Mappings\n");
|
|
63
|
+
const client = getLinearClient();
|
|
64
|
+
const teamId = getTeamId(configData, localConfig.team);
|
|
65
|
+
// Fetch workflow states
|
|
66
|
+
const statesData = await client.workflowStates({
|
|
67
|
+
filter: { team: { id: { eq: teamId } } },
|
|
68
|
+
});
|
|
69
|
+
const states = statesData.nodes;
|
|
70
|
+
if (states.length === 0) {
|
|
71
|
+
console.error("No workflow states found for this team.");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const stateChoices = states.map((s) => ({
|
|
75
|
+
title: `${s.name} (${s.type})`,
|
|
76
|
+
value: s.name,
|
|
77
|
+
}));
|
|
78
|
+
// Get current values or defaults
|
|
79
|
+
const current = configData.status_transitions || {};
|
|
80
|
+
const defaultTodo = current.todo ||
|
|
81
|
+
states.find((s) => s.type === "unstarted")?.name ||
|
|
82
|
+
"Todo";
|
|
83
|
+
const defaultInProgress = current.in_progress ||
|
|
84
|
+
states.find((s) => s.type === "started")?.name ||
|
|
85
|
+
"In Progress";
|
|
86
|
+
const defaultDone = current.done ||
|
|
87
|
+
states.find((s) => s.type === "completed")?.name ||
|
|
88
|
+
"Done";
|
|
89
|
+
const defaultTesting = current.testing || states.find((s) => s.name === "Testing")?.name;
|
|
90
|
+
const todoResponse = await prompts({
|
|
91
|
+
type: "select",
|
|
92
|
+
name: "todo",
|
|
93
|
+
message: 'Select status for "Todo" (pending tasks):',
|
|
94
|
+
choices: stateChoices,
|
|
95
|
+
initial: stateChoices.findIndex((c) => c.value === defaultTodo),
|
|
96
|
+
});
|
|
97
|
+
const inProgressResponse = await prompts({
|
|
98
|
+
type: "select",
|
|
99
|
+
name: "in_progress",
|
|
100
|
+
message: 'Select status for "In Progress" (working tasks):',
|
|
101
|
+
choices: stateChoices,
|
|
102
|
+
initial: stateChoices.findIndex((c) => c.value === defaultInProgress),
|
|
103
|
+
});
|
|
104
|
+
const doneResponse = await prompts({
|
|
105
|
+
type: "select",
|
|
106
|
+
name: "done",
|
|
107
|
+
message: 'Select status for "Done" (completed tasks):',
|
|
108
|
+
choices: stateChoices,
|
|
109
|
+
initial: stateChoices.findIndex((c) => c.value === defaultDone),
|
|
110
|
+
});
|
|
111
|
+
// Testing is optional
|
|
112
|
+
const testingChoices = [
|
|
113
|
+
{ title: "(None)", value: undefined },
|
|
114
|
+
...stateChoices,
|
|
115
|
+
];
|
|
116
|
+
const testingResponse = await prompts({
|
|
117
|
+
type: "select",
|
|
118
|
+
name: "testing",
|
|
119
|
+
message: 'Select status for "Testing" (optional, for parent tasks):',
|
|
120
|
+
choices: testingChoices,
|
|
121
|
+
initial: defaultTesting
|
|
122
|
+
? testingChoices.findIndex((c) => c.value === defaultTesting)
|
|
123
|
+
: 0,
|
|
124
|
+
});
|
|
125
|
+
const statusTransitions = {
|
|
126
|
+
todo: todoResponse.todo || defaultTodo,
|
|
127
|
+
in_progress: inProgressResponse.in_progress || defaultInProgress,
|
|
128
|
+
done: doneResponse.done || defaultDone,
|
|
129
|
+
testing: testingResponse.testing,
|
|
130
|
+
};
|
|
131
|
+
configData.status_transitions = statusTransitions;
|
|
132
|
+
await saveConfig(configData);
|
|
133
|
+
console.log("\n✅ Status mappings updated:");
|
|
134
|
+
console.log(` Todo: ${statusTransitions.todo}`);
|
|
135
|
+
console.log(` In Progress: ${statusTransitions.in_progress}`);
|
|
136
|
+
console.log(` Done: ${statusTransitions.done}`);
|
|
137
|
+
if (statusTransitions.testing) {
|
|
138
|
+
console.log(` Testing: ${statusTransitions.testing}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (subcommand === "filters") {
|
|
142
|
+
console.log("🔍 Configure Filters\n");
|
|
143
|
+
const client = getLinearClient();
|
|
144
|
+
const teamId = getTeamId(configData, localConfig.team);
|
|
145
|
+
// Fetch labels
|
|
146
|
+
const labelsData = await client.issueLabels({
|
|
147
|
+
filter: { team: { id: { eq: teamId } } },
|
|
148
|
+
});
|
|
149
|
+
const labels = labelsData.nodes;
|
|
150
|
+
// Fetch users
|
|
151
|
+
const team = await client.team(teamId);
|
|
152
|
+
const members = await team.members();
|
|
153
|
+
const users = members.nodes;
|
|
154
|
+
// Label filter (optional)
|
|
155
|
+
const labelChoices = [
|
|
156
|
+
{ title: "(No filter - sync all labels)", value: "" },
|
|
157
|
+
...labels.map((l) => ({ title: l.name, value: l.name })),
|
|
158
|
+
];
|
|
159
|
+
const labelResponse = await prompts({
|
|
160
|
+
type: "select",
|
|
161
|
+
name: "label",
|
|
162
|
+
message: "Select label filter (optional):",
|
|
163
|
+
choices: labelChoices,
|
|
164
|
+
initial: localConfig.label
|
|
165
|
+
? labelChoices.findIndex((c) => c.value === localConfig.label)
|
|
166
|
+
: 0,
|
|
167
|
+
});
|
|
168
|
+
// Exclude labels
|
|
169
|
+
const excludeLabelsResponse = await prompts({
|
|
170
|
+
type: "multiselect",
|
|
171
|
+
name: "excludeLabels",
|
|
172
|
+
message: "Select labels to exclude (space to select):",
|
|
173
|
+
choices: labels.map((l) => ({
|
|
174
|
+
title: l.name,
|
|
175
|
+
value: l.name,
|
|
176
|
+
selected: localConfig.exclude_labels?.includes(l.name),
|
|
177
|
+
})),
|
|
178
|
+
});
|
|
179
|
+
// Exclude users
|
|
180
|
+
const excludeUsersResponse = await prompts({
|
|
181
|
+
type: "multiselect",
|
|
182
|
+
name: "excludeUsers",
|
|
183
|
+
message: "Select users to exclude (space to select):",
|
|
184
|
+
choices: users.map((u) => {
|
|
185
|
+
const key = (u.displayName ||
|
|
186
|
+
u.name ||
|
|
187
|
+
u.email?.split("@")[0] ||
|
|
188
|
+
"user")
|
|
189
|
+
.toLowerCase()
|
|
190
|
+
.replace(/[^a-z0-9]/g, "_");
|
|
191
|
+
return {
|
|
192
|
+
title: `${u.displayName || u.name} (${u.email})`,
|
|
193
|
+
value: key,
|
|
194
|
+
selected: localConfig.exclude_assignees?.includes(key),
|
|
195
|
+
};
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
localConfig.label = labelResponse.label || undefined;
|
|
199
|
+
localConfig.exclude_labels =
|
|
200
|
+
excludeLabelsResponse.excludeLabels?.length > 0
|
|
201
|
+
? excludeLabelsResponse.excludeLabels
|
|
202
|
+
: undefined;
|
|
203
|
+
localConfig.exclude_assignees =
|
|
204
|
+
excludeUsersResponse.excludeUsers?.length > 0
|
|
205
|
+
? excludeUsersResponse.excludeUsers
|
|
206
|
+
: undefined;
|
|
207
|
+
await saveLocalConfig(localConfig);
|
|
208
|
+
console.log("\n✅ Filters updated:");
|
|
209
|
+
console.log(` Label: ${localConfig.label || "(all)"}`);
|
|
210
|
+
console.log(` Exclude labels: ${localConfig.exclude_labels?.join(", ") || "(none)"}`);
|
|
211
|
+
console.log(` Exclude users: ${localConfig.exclude_assignees?.join(", ") || "(none)"}`);
|
|
212
|
+
}
|
|
213
|
+
if (subcommand === "teams") {
|
|
214
|
+
console.log("👥 Configure Teams\n");
|
|
215
|
+
const client = getLinearClient();
|
|
216
|
+
const teamsData = await client.teams();
|
|
217
|
+
const teams = teamsData.nodes;
|
|
218
|
+
if (teams.length === 0) {
|
|
219
|
+
console.error("No teams found.");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
// Multi-select teams
|
|
223
|
+
const teamsResponse = await prompts({
|
|
224
|
+
type: "multiselect",
|
|
225
|
+
name: "teamKeys",
|
|
226
|
+
message: "Select teams to sync (space to select):",
|
|
227
|
+
choices: teams.map((t) => {
|
|
228
|
+
const key = t.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
229
|
+
const currentTeams = localConfig.teams || [localConfig.team];
|
|
230
|
+
return {
|
|
231
|
+
title: t.name,
|
|
232
|
+
value: key,
|
|
233
|
+
selected: currentTeams.includes(key),
|
|
234
|
+
};
|
|
235
|
+
}),
|
|
236
|
+
min: 1,
|
|
237
|
+
});
|
|
238
|
+
const selectedTeamKeys = teamsResponse.teamKeys || [localConfig.team];
|
|
239
|
+
// If multiple teams, ask for primary
|
|
240
|
+
let primaryTeam = localConfig.team;
|
|
241
|
+
if (selectedTeamKeys.length > 1) {
|
|
242
|
+
const primaryResponse = await prompts({
|
|
243
|
+
type: "select",
|
|
244
|
+
name: "primary",
|
|
245
|
+
message: "Select primary team:",
|
|
246
|
+
choices: selectedTeamKeys.map((key) => ({
|
|
247
|
+
title: key,
|
|
248
|
+
value: key,
|
|
249
|
+
})),
|
|
250
|
+
initial: selectedTeamKeys.indexOf(localConfig.team),
|
|
251
|
+
});
|
|
252
|
+
primaryTeam = primaryResponse.primary || selectedTeamKeys[0];
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
primaryTeam = selectedTeamKeys[0];
|
|
256
|
+
}
|
|
257
|
+
localConfig.team = primaryTeam;
|
|
258
|
+
localConfig.teams =
|
|
259
|
+
selectedTeamKeys.length > 1 ? selectedTeamKeys : undefined;
|
|
260
|
+
await saveLocalConfig(localConfig);
|
|
261
|
+
console.log("\n✅ Teams updated:");
|
|
262
|
+
if (localConfig.teams) {
|
|
263
|
+
console.log(` Selected: ${localConfig.teams.join(", ")}`);
|
|
264
|
+
console.log(` Primary: ${localConfig.team}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
console.log(` Team: ${localConfig.team}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
config().catch(console.error);
|
|
@@ -1,27 +1,37 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { getLinearClient,
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import prompts from "prompts";
|
|
3
|
+
import { getLinearClient, getTeamId, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
|
|
4
4
|
async function getLatestCommit() {
|
|
5
5
|
try {
|
|
6
|
-
const shortHash = execSync(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
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();
|
|
10
18
|
// Get remote URL and construct commit link
|
|
11
19
|
let commitUrl = null;
|
|
12
20
|
try {
|
|
13
|
-
const remoteUrl = execSync(
|
|
21
|
+
const remoteUrl = execSync("git remote get-url origin", {
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
}).trim();
|
|
14
24
|
// Handle SSH or HTTPS URLs
|
|
15
25
|
// git@gitlab.com:org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
|
|
16
26
|
// https://gitlab.com/org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
|
|
17
|
-
if (remoteUrl.includes(
|
|
18
|
-
const match = remoteUrl.match(/(?:git@|https:\/\/)([
|
|
27
|
+
if (remoteUrl.includes("gitlab")) {
|
|
28
|
+
const match = remoteUrl.match(/(?:git@|https:\/\/)([^:/]+)[:\\/](.+?)(?:\.git)?$/);
|
|
19
29
|
if (match) {
|
|
20
30
|
commitUrl = `https://${match[1]}/${match[2]}/-/commit/${fullHash}`;
|
|
21
31
|
}
|
|
22
32
|
}
|
|
23
|
-
else if (remoteUrl.includes(
|
|
24
|
-
const match = remoteUrl.match(/(?:git@|https:\/\/)([
|
|
33
|
+
else if (remoteUrl.includes("github")) {
|
|
34
|
+
const match = remoteUrl.match(/(?:git@|https:\/\/)([^:/]+)[:\\/](.+?)(?:\.git)?$/);
|
|
25
35
|
if (match) {
|
|
26
36
|
commitUrl = `https://${match[1]}/${match[2]}/commit/${fullHash}`;
|
|
27
37
|
}
|
|
@@ -41,10 +51,10 @@ function parseArgs(args) {
|
|
|
41
51
|
let message;
|
|
42
52
|
for (let i = 0; i < args.length; i++) {
|
|
43
53
|
const arg = args[i];
|
|
44
|
-
if (arg ===
|
|
54
|
+
if (arg === "-m" || arg === "--message") {
|
|
45
55
|
message = args[++i];
|
|
46
56
|
}
|
|
47
|
-
else if (!arg.startsWith(
|
|
57
|
+
else if (!arg.startsWith("-")) {
|
|
48
58
|
issueId = arg;
|
|
49
59
|
}
|
|
50
60
|
}
|
|
@@ -53,7 +63,7 @@ function parseArgs(args) {
|
|
|
53
63
|
async function doneJob() {
|
|
54
64
|
const args = process.argv.slice(2);
|
|
55
65
|
// Handle help flag
|
|
56
|
-
if (args.includes(
|
|
66
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
57
67
|
console.log(`Usage: ttt done [issue-id] [-m message]
|
|
58
68
|
|
|
59
69
|
Arguments:
|
|
@@ -75,13 +85,13 @@ Examples:
|
|
|
75
85
|
const localConfig = await loadLocalConfig();
|
|
76
86
|
const data = await loadCycleData();
|
|
77
87
|
if (!data) {
|
|
78
|
-
console.error(
|
|
88
|
+
console.error("No cycle data found. Run /sync-linear first.");
|
|
79
89
|
process.exit(1);
|
|
80
90
|
}
|
|
81
91
|
// Find in-progress tasks
|
|
82
|
-
const inProgressTasks = data.tasks.filter(t => t.localStatus ===
|
|
92
|
+
const inProgressTasks = data.tasks.filter((t) => t.localStatus === "in-progress");
|
|
83
93
|
if (inProgressTasks.length === 0) {
|
|
84
|
-
console.log(
|
|
94
|
+
console.log("沒有進行中的任務");
|
|
85
95
|
process.exit(0);
|
|
86
96
|
}
|
|
87
97
|
// Phase 0: Issue Resolution
|
|
@@ -91,63 +101,68 @@ Examples:
|
|
|
91
101
|
console.log(`Auto-selected: ${issueId}`);
|
|
92
102
|
}
|
|
93
103
|
else if (process.stdin.isTTY) {
|
|
94
|
-
const choices = inProgressTasks.map(task => ({
|
|
104
|
+
const choices = inProgressTasks.map((task) => ({
|
|
95
105
|
title: `${task.id}: ${task.title}`,
|
|
96
106
|
value: task.id,
|
|
97
|
-
description: task.labels.join(
|
|
107
|
+
description: task.labels.join(", "),
|
|
98
108
|
}));
|
|
99
109
|
const response = await prompts({
|
|
100
|
-
type:
|
|
101
|
-
name:
|
|
102
|
-
message:
|
|
103
|
-
choices: choices
|
|
110
|
+
type: "select",
|
|
111
|
+
name: "issueId",
|
|
112
|
+
message: "選擇要完成的任務:",
|
|
113
|
+
choices: choices,
|
|
104
114
|
});
|
|
105
115
|
if (!response.issueId) {
|
|
106
|
-
console.log(
|
|
116
|
+
console.log("已取消");
|
|
107
117
|
process.exit(0);
|
|
108
118
|
}
|
|
109
119
|
issueId = response.issueId;
|
|
110
120
|
}
|
|
111
121
|
else {
|
|
112
|
-
console.error(
|
|
113
|
-
|
|
122
|
+
console.error("多個進行中任務,請指定 issue ID:");
|
|
123
|
+
for (const t of inProgressTasks) {
|
|
124
|
+
console.log(` - ${t.id}: ${t.title}`);
|
|
125
|
+
}
|
|
114
126
|
process.exit(1);
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
// Phase 1: Find task
|
|
118
|
-
const task = data.tasks.find(t => t.id === issueId || t.id === `MP-${issueId}`);
|
|
130
|
+
const task = data.tasks.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
|
|
119
131
|
if (!task) {
|
|
120
132
|
console.error(`Issue ${issueId} not found in current cycle.`);
|
|
121
133
|
process.exit(1);
|
|
122
134
|
}
|
|
123
|
-
if (task.localStatus !==
|
|
135
|
+
if (task.localStatus !== "in-progress") {
|
|
124
136
|
console.log(`⚠️ 任務 ${task.id} 不在進行中狀態 (目前: ${task.localStatus})`);
|
|
125
137
|
process.exit(1);
|
|
126
138
|
}
|
|
127
139
|
// Get latest commit for comment
|
|
128
140
|
const commit = await getLatestCommit();
|
|
129
141
|
// Phase 2: Get AI summary message
|
|
130
|
-
let aiMessage = argMessage ||
|
|
142
|
+
let aiMessage = argMessage || "";
|
|
131
143
|
if (!aiMessage && process.stdin.isTTY) {
|
|
132
144
|
const aiMsgResponse = await prompts({
|
|
133
|
-
type:
|
|
134
|
-
name:
|
|
135
|
-
message:
|
|
145
|
+
type: "text",
|
|
146
|
+
name: "aiMessage",
|
|
147
|
+
message: "AI 修復說明 (如何解決此問題):",
|
|
136
148
|
});
|
|
137
|
-
aiMessage = aiMsgResponse.aiMessage ||
|
|
149
|
+
aiMessage = aiMsgResponse.aiMessage || "";
|
|
138
150
|
}
|
|
139
151
|
// Phase 3: Update Linear
|
|
140
152
|
if (task.linearId && process.env.LINEAR_API_KEY) {
|
|
141
153
|
try {
|
|
142
154
|
const client = getLinearClient();
|
|
143
155
|
const workflowStates = await client.workflowStates({
|
|
144
|
-
filter: { team: { id: { eq: getTeamId(config, localConfig.team) } } }
|
|
156
|
+
filter: { team: { id: { eq: getTeamId(config, localConfig.team) } } },
|
|
145
157
|
});
|
|
146
|
-
|
|
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);
|
|
147
162
|
// Update issue to Done
|
|
148
163
|
if (doneState) {
|
|
149
164
|
await client.updateIssue(task.linearId, { stateId: doneState.id });
|
|
150
|
-
console.log(`Linear: ${task.id} →
|
|
165
|
+
console.log(`Linear: ${task.id} → ${doneStatusName}`);
|
|
151
166
|
}
|
|
152
167
|
// Add comment with commit info and AI summary
|
|
153
168
|
if (commit) {
|
|
@@ -155,64 +170,66 @@ Examples:
|
|
|
155
170
|
? `[${commit.shortHash}](${commit.commitUrl})`
|
|
156
171
|
: `\`${commit.shortHash}\``;
|
|
157
172
|
const commentParts = [
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
aiMessage ||
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
"## ✅ 開發完成",
|
|
174
|
+
"",
|
|
175
|
+
"### 🤖 AI 修復說明",
|
|
176
|
+
aiMessage || "_No description provided_",
|
|
177
|
+
"",
|
|
178
|
+
"### 📝 Commit Info",
|
|
164
179
|
`**Commit:** ${commitLink}`,
|
|
165
180
|
`**Message:** ${commit.message}`,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
181
|
+
"",
|
|
182
|
+
"### 📊 Changes",
|
|
183
|
+
"```",
|
|
169
184
|
commit.diffStat,
|
|
170
|
-
|
|
185
|
+
"```",
|
|
171
186
|
];
|
|
172
187
|
await client.createComment({
|
|
173
188
|
issueId: task.linearId,
|
|
174
|
-
body: commentParts.join(
|
|
189
|
+
body: commentParts.join("\n"),
|
|
175
190
|
});
|
|
176
191
|
console.log(`Linear: 已新增 commit 留言`);
|
|
177
192
|
}
|
|
178
|
-
// Update parent to Testing if exists
|
|
179
|
-
if (task.parentIssueId) {
|
|
193
|
+
// Update parent to Testing if exists and testing status is configured
|
|
194
|
+
if (task.parentIssueId && testingStatusName) {
|
|
180
195
|
try {
|
|
181
196
|
// Find parent issue by identifier
|
|
182
197
|
const searchResult = await client.searchIssues(task.parentIssueId);
|
|
183
|
-
const parentIssue = searchResult.nodes.find(issue => issue.identifier === task.parentIssueId);
|
|
198
|
+
const parentIssue = searchResult.nodes.find((issue) => issue.identifier === task.parentIssueId);
|
|
184
199
|
if (parentIssue) {
|
|
185
200
|
// Get parent's team workflow states
|
|
186
201
|
const parentTeam = await parentIssue.team;
|
|
187
202
|
if (parentTeam) {
|
|
188
203
|
const parentWorkflowStates = await client.workflowStates({
|
|
189
|
-
filter: { team: { id: { eq: parentTeam.id } } }
|
|
204
|
+
filter: { team: { id: { eq: parentTeam.id } } },
|
|
190
205
|
});
|
|
191
|
-
const testingState = parentWorkflowStates.nodes.find(s => s.name ===
|
|
206
|
+
const testingState = parentWorkflowStates.nodes.find((s) => s.name === testingStatusName);
|
|
192
207
|
if (testingState) {
|
|
193
|
-
await client.updateIssue(parentIssue.id, {
|
|
194
|
-
|
|
208
|
+
await client.updateIssue(parentIssue.id, {
|
|
209
|
+
stateId: testingState.id,
|
|
210
|
+
});
|
|
211
|
+
console.log(`Linear: Parent ${task.parentIssueId} → ${testingStatusName}`);
|
|
195
212
|
}
|
|
196
213
|
}
|
|
197
214
|
}
|
|
198
215
|
}
|
|
199
216
|
catch (parentError) {
|
|
200
|
-
console.error(
|
|
217
|
+
console.error("Failed to update parent issue:", parentError);
|
|
201
218
|
}
|
|
202
219
|
}
|
|
203
220
|
}
|
|
204
221
|
catch (e) {
|
|
205
|
-
console.error(
|
|
222
|
+
console.error("Failed to update Linear:", e);
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
225
|
// Phase 4: Update local status
|
|
209
|
-
task.localStatus =
|
|
226
|
+
task.localStatus = "completed";
|
|
210
227
|
await saveCycleData(data);
|
|
211
228
|
console.log(`Local: ${task.id} → completed`);
|
|
212
229
|
// Phase 5: Summary
|
|
213
|
-
console.log(`\n${
|
|
230
|
+
console.log(`\n${"═".repeat(50)}`);
|
|
214
231
|
console.log(`✅ ${task.id}: ${task.title}`);
|
|
215
|
-
console.log(`${
|
|
232
|
+
console.log(`${"═".repeat(50)}`);
|
|
216
233
|
if (commit) {
|
|
217
234
|
console.log(`Commit: ${commit.shortHash} - ${commit.message}`);
|
|
218
235
|
if (commit.commitUrl) {
|
|
@@ -222,8 +239,8 @@ Examples:
|
|
|
222
239
|
if (aiMessage) {
|
|
223
240
|
console.log(`AI: ${aiMessage}`);
|
|
224
241
|
}
|
|
225
|
-
if (task.parentIssueId) {
|
|
226
|
-
console.log(`Parent: ${task.parentIssueId} →
|
|
242
|
+
if (task.parentIssueId && config.status_transitions?.testing) {
|
|
243
|
+
console.log(`Parent: ${task.parentIssueId} → ${config.status_transitions.testing}`);
|
|
227
244
|
}
|
|
228
245
|
console.log(`\n🎉 任務完成!`);
|
|
229
246
|
}
|