team-toon-tack 2.0.3 โ 2.3.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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +18 -0
- package/README.md +67 -10
- package/README.zh-TW.md +67 -10
- package/commands/ttt-done.md +62 -0
- package/commands/ttt-get-issue.md +71 -0
- package/commands/ttt-status.md +65 -0
- package/commands/ttt-sync.md +51 -0
- package/commands/ttt-work-on.md +48 -0
- package/dist/bin/cli.js +15 -8
- package/dist/scripts/config/teams.js +166 -34
- package/dist/scripts/done-job.js +158 -44
- package/dist/scripts/get-issue.d.ts +1 -0
- package/dist/scripts/get-issue.js +61 -0
- package/dist/scripts/init.js +203 -265
- package/dist/scripts/lib/config-builder.d.ts +2 -2
- package/dist/scripts/lib/config-builder.js +5 -4
- package/dist/scripts/lib/images.js +4 -3
- package/dist/scripts/lib/sync.d.ts +8 -0
- package/dist/scripts/lib/sync.js +41 -29
- package/dist/scripts/sync.js +1 -1
- package/dist/scripts/utils.d.ts +10 -2
- package/dist/scripts/work-on.js +1 -1
- package/package.json +5 -3
- package/skills/linear-task-manager/SKILL.md +170 -0
- package/templates/claude-code-commands/done-job.md +0 -45
- package/templates/claude-code-commands/sync-linear.md +0 -32
- package/templates/claude-code-commands/work-on.md +0 -62
- package/templates/config.example.toon +0 -37
- package/templates/local.example.toon +0 -16
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import prompts from "prompts";
|
|
2
|
+
import { getDefaultStatusTransitions } from "../lib/config-builder.js";
|
|
2
3
|
import { getLinearClient, saveLocalConfig, } from "../utils.js";
|
|
3
4
|
export async function configureTeams(_config, localConfig) {
|
|
4
5
|
console.log("๐ฅ Configure Teams\n");
|
|
@@ -9,51 +10,182 @@ export async function configureTeams(_config, localConfig) {
|
|
|
9
10
|
console.error("No teams found.");
|
|
10
11
|
process.exit(1);
|
|
11
12
|
}
|
|
12
|
-
//
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
name
|
|
16
|
-
|
|
13
|
+
// Build team key mapping
|
|
14
|
+
const teamKeyMap = new Map(); // id -> key
|
|
15
|
+
for (const team of teams) {
|
|
16
|
+
const key = team.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
17
|
+
teamKeyMap.set(team.id, key);
|
|
18
|
+
}
|
|
19
|
+
// Fetch workflow states for all teams
|
|
20
|
+
const teamStatesMap = new Map();
|
|
21
|
+
for (const team of teams) {
|
|
22
|
+
try {
|
|
23
|
+
const statesData = await client.workflowStates({
|
|
24
|
+
filter: { team: { id: { eq: team.id } } },
|
|
25
|
+
});
|
|
26
|
+
teamStatesMap.set(team.id, statesData.nodes);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
teamStatesMap.set(team.id, []);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// 1. Select dev team (single)
|
|
33
|
+
console.log("\n๐จโ๐ป Dev Team:");
|
|
34
|
+
const devTeamResponse = await prompts({
|
|
35
|
+
type: "select",
|
|
36
|
+
name: "teamId",
|
|
37
|
+
message: "Select your dev team:",
|
|
17
38
|
choices: teams.map((t) => {
|
|
18
|
-
const key = t.
|
|
19
|
-
const currentTeams = localConfig.teams || [localConfig.team];
|
|
39
|
+
const key = teamKeyMap.get(t.id) || "";
|
|
20
40
|
return {
|
|
21
41
|
title: t.name,
|
|
22
|
-
value:
|
|
23
|
-
selected:
|
|
42
|
+
value: t.id,
|
|
43
|
+
selected: key === localConfig.team,
|
|
24
44
|
};
|
|
25
45
|
}),
|
|
26
|
-
min: 1,
|
|
27
46
|
});
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
const devTeamId = devTeamResponse.teamId;
|
|
48
|
+
const devTeamKey = teamKeyMap.get(devTeamId) || localConfig.team;
|
|
49
|
+
const devTeam = teams.find((t) => t.id === devTeamId);
|
|
50
|
+
// 2. Select dev team testing status
|
|
51
|
+
const devStates = teamStatesMap.get(devTeamId) || [];
|
|
52
|
+
const devDefaults = getDefaultStatusTransitions(devStates);
|
|
53
|
+
console.log("\n๐ Dev Team Testing/Review Status:");
|
|
54
|
+
const devTestingResponse = await prompts({
|
|
55
|
+
type: "select",
|
|
56
|
+
name: "testingStatus",
|
|
57
|
+
message: "Select testing/review status for dev team:",
|
|
58
|
+
choices: [
|
|
59
|
+
{ title: "(Skip - no testing status)", value: undefined },
|
|
60
|
+
...devStates.map((s) => ({
|
|
61
|
+
title: `${s.name} (${s.type})`,
|
|
62
|
+
value: s.name,
|
|
39
63
|
})),
|
|
40
|
-
|
|
64
|
+
],
|
|
65
|
+
initial: localConfig.dev_testing_status
|
|
66
|
+
? devStates.findIndex((s) => s.name === localConfig.dev_testing_status) +
|
|
67
|
+
1
|
|
68
|
+
: devDefaults.testing
|
|
69
|
+
? devStates.findIndex((s) => s.name === devDefaults.testing) + 1
|
|
70
|
+
: 0,
|
|
71
|
+
});
|
|
72
|
+
const devTestingStatus = devTestingResponse.testingStatus;
|
|
73
|
+
// 3. Select QA/PM teams (multiple)
|
|
74
|
+
const otherTeams = teams.filter((t) => t.id !== devTeamId);
|
|
75
|
+
const qaPmTeams = [];
|
|
76
|
+
if (otherTeams.length > 0) {
|
|
77
|
+
console.log("\n๐ QA/PM Teams:");
|
|
78
|
+
const qaPmResponse = await prompts({
|
|
79
|
+
type: "multiselect",
|
|
80
|
+
name: "teamIds",
|
|
81
|
+
message: "Select QA/PM teams for cross-team parent updates (space to select):",
|
|
82
|
+
choices: otherTeams.map((t) => {
|
|
83
|
+
const key = teamKeyMap.get(t.id) || "";
|
|
84
|
+
const currentQaPm = localConfig.qa_pm_teams || [];
|
|
85
|
+
return {
|
|
86
|
+
title: t.name,
|
|
87
|
+
value: t.id,
|
|
88
|
+
selected: currentQaPm.some((qp) => qp.team === key),
|
|
89
|
+
};
|
|
90
|
+
}),
|
|
91
|
+
hint: "- Press space to select, enter to confirm. Leave empty to skip.",
|
|
41
92
|
});
|
|
42
|
-
|
|
93
|
+
const selectedQaPmIds = qaPmResponse.teamIds || [];
|
|
94
|
+
// 4. For each QA/PM team, select testing status
|
|
95
|
+
for (const teamId of selectedQaPmIds) {
|
|
96
|
+
const team = teams.find((t) => t.id === teamId);
|
|
97
|
+
if (!team)
|
|
98
|
+
continue;
|
|
99
|
+
const teamKey = teamKeyMap.get(teamId) || "";
|
|
100
|
+
const teamStates = teamStatesMap.get(teamId) || [];
|
|
101
|
+
const defaults = getDefaultStatusTransitions(teamStates);
|
|
102
|
+
// Find existing config for this team
|
|
103
|
+
const existingConfig = localConfig.qa_pm_teams?.find((qp) => qp.team === teamKey);
|
|
104
|
+
const stateChoices = teamStates.map((s) => ({
|
|
105
|
+
title: `${s.name} (${s.type})`,
|
|
106
|
+
value: s.name,
|
|
107
|
+
}));
|
|
108
|
+
const statusResponse = await prompts({
|
|
109
|
+
type: "select",
|
|
110
|
+
name: "testingStatus",
|
|
111
|
+
message: `Select testing status for ${team.name}:`,
|
|
112
|
+
choices: [
|
|
113
|
+
{ title: "(Skip this team)", value: undefined },
|
|
114
|
+
...stateChoices,
|
|
115
|
+
],
|
|
116
|
+
initial: existingConfig?.testing_status
|
|
117
|
+
? stateChoices.findIndex((c) => c.value === existingConfig.testing_status) + 1
|
|
118
|
+
: defaults.testing
|
|
119
|
+
? stateChoices.findIndex((c) => c.value === defaults.testing) + 1
|
|
120
|
+
: 0,
|
|
121
|
+
});
|
|
122
|
+
if (statusResponse.testingStatus) {
|
|
123
|
+
qaPmTeams.push({
|
|
124
|
+
team: teamKey,
|
|
125
|
+
testing_status: statusResponse.testingStatus,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
43
129
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
130
|
+
// 5. Select completion mode
|
|
131
|
+
console.log("\nโ
Completion Mode:");
|
|
132
|
+
const currentMode = localConfig.completion_mode ||
|
|
133
|
+
(qaPmTeams.length > 0 ? "upstream_strict" : "simple");
|
|
134
|
+
const defaultModeIndex = currentMode === "simple"
|
|
135
|
+
? 0
|
|
136
|
+
: currentMode === "strict_review"
|
|
137
|
+
? 1
|
|
138
|
+
: currentMode === "upstream_strict"
|
|
139
|
+
? 2
|
|
140
|
+
: 3;
|
|
141
|
+
const modeResponse = await prompts({
|
|
142
|
+
type: "select",
|
|
143
|
+
name: "mode",
|
|
144
|
+
message: "How should tasks be completed?",
|
|
145
|
+
choices: [
|
|
146
|
+
{
|
|
147
|
+
title: "Simple",
|
|
148
|
+
value: "simple",
|
|
149
|
+
description: "Mark task as done directly",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
title: "Strict Review",
|
|
153
|
+
value: "strict_review",
|
|
154
|
+
description: "Mark task to dev team's testing status",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
title: "Upstream Strict (recommended with QA/PM)",
|
|
158
|
+
value: "upstream_strict",
|
|
159
|
+
description: "Done + parent to testing, fallback to testing if no parent",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
title: "Upstream Not Strict",
|
|
163
|
+
value: "upstream_not_strict",
|
|
164
|
+
description: "Done + parent to testing, no fallback",
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
initial: defaultModeIndex,
|
|
168
|
+
});
|
|
169
|
+
const completionMode = modeResponse.mode || (qaPmTeams.length > 0 ? "upstream_strict" : "simple");
|
|
170
|
+
// Update local config
|
|
171
|
+
localConfig.team = devTeamKey;
|
|
172
|
+
localConfig.dev_testing_status = devTestingStatus;
|
|
173
|
+
localConfig.qa_pm_teams = qaPmTeams.length > 0 ? qaPmTeams : undefined;
|
|
174
|
+
localConfig.completion_mode = completionMode;
|
|
175
|
+
// Remove deprecated fields
|
|
176
|
+
delete localConfig.teams;
|
|
177
|
+
delete localConfig.qa_pm_team;
|
|
50
178
|
await saveLocalConfig(localConfig);
|
|
51
179
|
console.log("\nโ
Teams updated:");
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.log(`
|
|
180
|
+
console.log(` Dev Team: ${devTeam?.name || devTeamKey}`);
|
|
181
|
+
if (devTestingStatus) {
|
|
182
|
+
console.log(` Dev Testing Status: ${devTestingStatus}`);
|
|
55
183
|
}
|
|
56
|
-
|
|
57
|
-
|
|
184
|
+
console.log(` Completion Mode: ${completionMode}`);
|
|
185
|
+
if (qaPmTeams.length > 0) {
|
|
186
|
+
console.log(" QA/PM Teams:");
|
|
187
|
+
for (const qp of qaPmTeams) {
|
|
188
|
+
console.log(` - ${qp.team}: ${qp.testing_status}`);
|
|
189
|
+
}
|
|
58
190
|
}
|
|
59
191
|
}
|
package/dist/scripts/done-job.js
CHANGED
|
@@ -3,6 +3,86 @@ import { buildCompletionComment, getLatestCommit } from "./lib/git.js";
|
|
|
3
3
|
import { addComment, getStatusTransitions, getWorkflowStates, updateIssueStatus, } from "./lib/linear.js";
|
|
4
4
|
import { syncSingleIssue } from "./lib/sync.js";
|
|
5
5
|
import { getLinearClient, loadConfig, loadCycleData, loadLocalConfig, } from "./utils.js";
|
|
6
|
+
// Helper function to update parent issue to a specific status
|
|
7
|
+
async function updateParentStatus(parentIssueId, targetStatus, _qaPmTeams, config) {
|
|
8
|
+
try {
|
|
9
|
+
const client = getLinearClient();
|
|
10
|
+
const searchResult = await client.searchIssues(parentIssueId);
|
|
11
|
+
const parentIssue = searchResult.nodes.find((issue) => issue.identifier === parentIssueId);
|
|
12
|
+
if (!parentIssue) {
|
|
13
|
+
return { success: false };
|
|
14
|
+
}
|
|
15
|
+
const parentTeam = await parentIssue.team;
|
|
16
|
+
if (!parentTeam) {
|
|
17
|
+
return { success: false };
|
|
18
|
+
}
|
|
19
|
+
// Find the matching team configuration
|
|
20
|
+
const teamEntries = Object.entries(config.teams);
|
|
21
|
+
const matchingTeamEntry = teamEntries.find(([_, t]) => t.id === parentTeam.id);
|
|
22
|
+
if (!matchingTeamEntry) {
|
|
23
|
+
return { success: false };
|
|
24
|
+
}
|
|
25
|
+
const [parentTeamKey] = matchingTeamEntry;
|
|
26
|
+
// Get workflow states for the parent's team
|
|
27
|
+
const parentStates = await getWorkflowStates(config, parentTeamKey);
|
|
28
|
+
const targetState = parentStates.find((s) => s.name === targetStatus);
|
|
29
|
+
if (!targetState) {
|
|
30
|
+
return { success: false };
|
|
31
|
+
}
|
|
32
|
+
// Update the parent issue
|
|
33
|
+
await client.updateIssue(parentIssue.id, {
|
|
34
|
+
stateId: targetState.id,
|
|
35
|
+
});
|
|
36
|
+
return { success: true, status: targetStatus };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error("Failed to update parent issue:", error);
|
|
40
|
+
return { success: false };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Helper function to update parent issue to testing status (uses QA team config)
|
|
44
|
+
async function updateParentToTesting(parentIssueId, qaPmTeams, config) {
|
|
45
|
+
try {
|
|
46
|
+
const client = getLinearClient();
|
|
47
|
+
const searchResult = await client.searchIssues(parentIssueId);
|
|
48
|
+
const parentIssue = searchResult.nodes.find((issue) => issue.identifier === parentIssueId);
|
|
49
|
+
if (!parentIssue) {
|
|
50
|
+
return { success: false };
|
|
51
|
+
}
|
|
52
|
+
const parentTeam = await parentIssue.team;
|
|
53
|
+
if (!parentTeam) {
|
|
54
|
+
return { success: false };
|
|
55
|
+
}
|
|
56
|
+
// Find the matching QA/PM team configuration
|
|
57
|
+
const teamEntries = Object.entries(config.teams);
|
|
58
|
+
const matchingTeamEntry = teamEntries.find(([_, t]) => t.id === parentTeam.id);
|
|
59
|
+
if (!matchingTeamEntry) {
|
|
60
|
+
return { success: false };
|
|
61
|
+
}
|
|
62
|
+
const [parentTeamKey] = matchingTeamEntry;
|
|
63
|
+
// Find the QA/PM team config for this parent's team
|
|
64
|
+
const qaPmConfig = qaPmTeams.find((qp) => qp.team === parentTeamKey);
|
|
65
|
+
if (!qaPmConfig) {
|
|
66
|
+
// Parent's team is not in the configured QA/PM teams
|
|
67
|
+
return { success: false };
|
|
68
|
+
}
|
|
69
|
+
// Get workflow states for the parent's team
|
|
70
|
+
const parentStates = await getWorkflowStates(config, parentTeamKey);
|
|
71
|
+
const testingState = parentStates.find((s) => s.name === qaPmConfig.testing_status);
|
|
72
|
+
if (!testingState) {
|
|
73
|
+
return { success: false };
|
|
74
|
+
}
|
|
75
|
+
// Update the parent issue
|
|
76
|
+
await client.updateIssue(parentIssue.id, {
|
|
77
|
+
stateId: testingState.id,
|
|
78
|
+
});
|
|
79
|
+
return { success: true, testingStatus: qaPmConfig.testing_status };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error("Failed to update parent issue:", error);
|
|
83
|
+
return { success: false };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
6
86
|
function parseArgs(args) {
|
|
7
87
|
let issueId;
|
|
8
88
|
let message;
|
|
@@ -110,10 +190,84 @@ Examples:
|
|
|
110
190
|
process.env.LINEAR_API_KEY &&
|
|
111
191
|
statusSource === "remote") {
|
|
112
192
|
const transitions = getStatusTransitions(config);
|
|
113
|
-
//
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
193
|
+
// Determine completion mode
|
|
194
|
+
const completionMode = localConfig.completion_mode ||
|
|
195
|
+
(localConfig.qa_pm_teams && localConfig.qa_pm_teams.length > 0
|
|
196
|
+
? "upstream_strict"
|
|
197
|
+
: "simple");
|
|
198
|
+
// Get the testing status to use (from dev team)
|
|
199
|
+
const devTestingStatus = localConfig.dev_testing_status || transitions.testing;
|
|
200
|
+
// Execute based on completion mode
|
|
201
|
+
let parentUpdateSuccess = false;
|
|
202
|
+
let parentTestingStatus;
|
|
203
|
+
switch (completionMode) {
|
|
204
|
+
case "simple": {
|
|
205
|
+
// Mark task as done
|
|
206
|
+
const success = await updateIssueStatus(task.linearId, transitions.done, config, localConfig.team);
|
|
207
|
+
if (success) {
|
|
208
|
+
console.log(`Linear: ${task.id} โ ${transitions.done}`);
|
|
209
|
+
}
|
|
210
|
+
// Also mark parent as done if exists
|
|
211
|
+
if (task.parentIssueId) {
|
|
212
|
+
const result = await updateParentStatus(task.parentIssueId, transitions.done, localConfig.qa_pm_teams, config);
|
|
213
|
+
if (result.success) {
|
|
214
|
+
console.log(`Linear: Parent ${task.parentIssueId} โ ${transitions.done}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case "strict_review": {
|
|
220
|
+
// Mark task to dev team's testing status
|
|
221
|
+
if (devTestingStatus) {
|
|
222
|
+
const success = await updateIssueStatus(task.linearId, devTestingStatus, config, localConfig.team);
|
|
223
|
+
if (success) {
|
|
224
|
+
console.log(`Linear: ${task.id} โ ${devTestingStatus}`);
|
|
225
|
+
}
|
|
226
|
+
// Also mark parent to testing if exists
|
|
227
|
+
if (task.parentIssueId && localConfig.qa_pm_teams?.length) {
|
|
228
|
+
const result = await updateParentToTesting(task.parentIssueId, localConfig.qa_pm_teams, config);
|
|
229
|
+
if (result.success) {
|
|
230
|
+
console.log(`Linear: Parent ${task.parentIssueId} โ ${result.testingStatus}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
console.warn("No dev testing status configured, falling back to done");
|
|
236
|
+
const success = await updateIssueStatus(task.linearId, transitions.done, config, localConfig.team);
|
|
237
|
+
if (success) {
|
|
238
|
+
console.log(`Linear: ${task.id} โ ${transitions.done}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "upstream_strict":
|
|
244
|
+
case "upstream_not_strict": {
|
|
245
|
+
// First, mark as done
|
|
246
|
+
const doneSuccess = await updateIssueStatus(task.linearId, transitions.done, config, localConfig.team);
|
|
247
|
+
if (doneSuccess) {
|
|
248
|
+
console.log(`Linear: ${task.id} โ ${transitions.done}`);
|
|
249
|
+
}
|
|
250
|
+
// Try to update parent to testing
|
|
251
|
+
if (task.parentIssueId && localConfig.qa_pm_teams?.length) {
|
|
252
|
+
const result = await updateParentToTesting(task.parentIssueId, localConfig.qa_pm_teams, config);
|
|
253
|
+
parentUpdateSuccess = result.success;
|
|
254
|
+
parentTestingStatus = result.testingStatus;
|
|
255
|
+
if (parentUpdateSuccess) {
|
|
256
|
+
console.log(`Linear: Parent ${task.parentIssueId} โ ${parentTestingStatus}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Fallback logic for upstream_strict
|
|
260
|
+
if (completionMode === "upstream_strict" &&
|
|
261
|
+
!parentUpdateSuccess &&
|
|
262
|
+
devTestingStatus) {
|
|
263
|
+
// No parent or parent update failed, fallback to testing
|
|
264
|
+
const fallbackSuccess = await updateIssueStatus(task.linearId, devTestingStatus, config, localConfig.team);
|
|
265
|
+
if (fallbackSuccess) {
|
|
266
|
+
console.log(`Linear: ${task.id} โ ${devTestingStatus} (fallback, no valid parent)`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
117
271
|
}
|
|
118
272
|
// Add comment with commit info
|
|
119
273
|
if (commit) {
|
|
@@ -123,43 +277,6 @@ Examples:
|
|
|
123
277
|
console.log(`Linear: ๅทฒๆฐๅข commit ็่จ`);
|
|
124
278
|
}
|
|
125
279
|
}
|
|
126
|
-
// Update parent to Testing if exists
|
|
127
|
-
if (task.parentIssueId && transitions.testing) {
|
|
128
|
-
try {
|
|
129
|
-
const client = getLinearClient();
|
|
130
|
-
const searchResult = await client.searchIssues(task.parentIssueId);
|
|
131
|
-
const parentIssue = searchResult.nodes.find((issue) => issue.identifier === task.parentIssueId);
|
|
132
|
-
if (parentIssue) {
|
|
133
|
-
const parentTeam = await parentIssue.team;
|
|
134
|
-
if (parentTeam) {
|
|
135
|
-
// Determine which team key to use for parent's workflow states
|
|
136
|
-
// If qa_pm_team is configured and matches parent's team, use it
|
|
137
|
-
// Otherwise, try to find the team key from config
|
|
138
|
-
let parentTeamKey = localConfig.team;
|
|
139
|
-
const teamEntries = Object.entries(config.teams);
|
|
140
|
-
const matchingTeam = teamEntries.find(([_, t]) => t.id === parentTeam.id);
|
|
141
|
-
if (matchingTeam) {
|
|
142
|
-
parentTeamKey = matchingTeam[0];
|
|
143
|
-
}
|
|
144
|
-
else if (localConfig.qa_pm_team) {
|
|
145
|
-
// Fallback to qa_pm_team if configured
|
|
146
|
-
parentTeamKey = localConfig.qa_pm_team;
|
|
147
|
-
}
|
|
148
|
-
const parentStates = await getWorkflowStates(config, parentTeamKey);
|
|
149
|
-
const testingState = parentStates.find((s) => s.name === transitions.testing);
|
|
150
|
-
if (testingState) {
|
|
151
|
-
await client.updateIssue(parentIssue.id, {
|
|
152
|
-
stateId: testingState.id,
|
|
153
|
-
});
|
|
154
|
-
console.log(`Linear: Parent ${task.parentIssueId} โ ${transitions.testing}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch (parentError) {
|
|
160
|
-
console.error("Failed to update parent issue:", parentError);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
280
|
}
|
|
164
281
|
else if (statusSource === "local") {
|
|
165
282
|
console.log(`Local: ${task.id} marked as completed`);
|
|
@@ -187,9 +304,6 @@ Examples:
|
|
|
187
304
|
if (aiMessage) {
|
|
188
305
|
console.log(`AI: ${aiMessage}`);
|
|
189
306
|
}
|
|
190
|
-
if (task.parentIssueId && config.status_transitions?.testing) {
|
|
191
|
-
console.log(`Parent: ${task.parentIssueId} โ ${config.status_transitions.testing}`);
|
|
192
|
-
}
|
|
193
307
|
console.log(`\n๐ ไปปๅๅฎๆ๏ผ`);
|
|
194
308
|
}
|
|
195
309
|
doneJob().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { displayTaskFull } from "./lib/display.js";
|
|
2
|
+
import { fetchIssueDetail } from "./lib/sync.js";
|
|
3
|
+
import { loadCycleData } from "./utils.js";
|
|
4
|
+
async function getIssue() {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
7
|
+
console.log(`Usage: ttt get-issue <issue-id> [--local]
|
|
8
|
+
|
|
9
|
+
Fetch and display issue details from Linear.
|
|
10
|
+
|
|
11
|
+
Arguments:
|
|
12
|
+
issue-id Issue ID (e.g., MP-624). Required.
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--local Only show from local cycle data, don't fetch from Linear
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
ttt get-issue MP-624 # Fetch from Linear and display
|
|
19
|
+
ttt get-issue MP-624 --local # Show from local data only`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
const localOnly = args.includes("--local");
|
|
23
|
+
const issueId = args.find((arg) => !arg.startsWith("-"));
|
|
24
|
+
if (!issueId) {
|
|
25
|
+
console.error("Issue ID is required.");
|
|
26
|
+
console.error("Usage: ttt get-issue <issue-id>");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
// If local only, get from cycle data
|
|
30
|
+
if (localOnly) {
|
|
31
|
+
const data = await loadCycleData();
|
|
32
|
+
if (!data) {
|
|
33
|
+
console.error("No cycle data found. Run ttt sync first.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const task = data.tasks.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
|
|
37
|
+
if (!task) {
|
|
38
|
+
console.error(`Issue ${issueId} not found in local data.`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
displayTaskFull(task, "๐");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Fetch from Linear
|
|
45
|
+
console.log(`Fetching ${issueId} from Linear...`);
|
|
46
|
+
const task = await fetchIssueDetail(issueId);
|
|
47
|
+
if (!task) {
|
|
48
|
+
console.error(`Issue ${issueId} not found in Linear.`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
// Check local data for local status
|
|
52
|
+
const data = await loadCycleData();
|
|
53
|
+
if (data) {
|
|
54
|
+
const localTask = data.tasks.find((t) => t.id === issueId);
|
|
55
|
+
if (localTask) {
|
|
56
|
+
task.localStatus = localTask.localStatus;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
displayTaskFull(task, "๐");
|
|
60
|
+
}
|
|
61
|
+
getIssue().catch(console.error);
|