team-toon-tack 1.0.12 → 1.6.1
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/filters.d.ts +2 -0
- package/dist/scripts/config/filters.js +71 -0
- package/dist/scripts/config/show.d.ts +2 -0
- package/dist/scripts/config/show.js +33 -0
- package/dist/scripts/config/status.d.ts +2 -0
- package/dist/scripts/config/status.js +73 -0
- package/dist/scripts/config/teams.d.ts +2 -0
- package/dist/scripts/config/teams.js +59 -0
- package/dist/scripts/config.js +47 -0
- package/dist/scripts/done-job.js +169 -0
- package/dist/scripts/init.d.ts +2 -0
- package/dist/scripts/init.js +369 -0
- package/dist/scripts/lib/config-builder.d.ts +41 -0
- package/dist/scripts/lib/config-builder.js +116 -0
- package/dist/scripts/lib/display.d.ts +12 -0
- package/dist/scripts/lib/display.js +91 -0
- package/dist/scripts/lib/git.d.ts +10 -0
- package/dist/scripts/lib/git.js +78 -0
- package/dist/scripts/lib/linear.d.ts +11 -0
- package/dist/scripts/lib/linear.js +61 -0
- package/dist/scripts/status.d.ts +2 -0
- package/dist/scripts/status.js +162 -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/dist/scripts/work-on.js +102 -0
- 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.js +0 -230
- 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.js +0 -138
- 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,116 @@
|
|
|
1
|
+
export function buildTeamsConfig(teams) {
|
|
2
|
+
const config = {};
|
|
3
|
+
for (const team of teams) {
|
|
4
|
+
const key = team.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
5
|
+
config[key] = {
|
|
6
|
+
id: team.id,
|
|
7
|
+
name: team.name,
|
|
8
|
+
icon: team.icon || undefined,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
export function buildUsersConfig(users) {
|
|
14
|
+
const config = {};
|
|
15
|
+
for (const user of users) {
|
|
16
|
+
const key = (user.displayName ||
|
|
17
|
+
user.name ||
|
|
18
|
+
user.email?.split("@")[0] ||
|
|
19
|
+
"user")
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]/g, "_");
|
|
22
|
+
config[key] = {
|
|
23
|
+
id: user.id,
|
|
24
|
+
email: user.email || "",
|
|
25
|
+
displayName: user.displayName || user.name || "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
export function buildLabelsConfig(labels) {
|
|
31
|
+
const config = {};
|
|
32
|
+
for (const label of labels) {
|
|
33
|
+
const key = label.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
34
|
+
config[key] = {
|
|
35
|
+
id: label.id,
|
|
36
|
+
name: label.name,
|
|
37
|
+
color: label.color || undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
export function buildStatusesConfig(states) {
|
|
43
|
+
const config = {};
|
|
44
|
+
for (const state of states) {
|
|
45
|
+
const key = state.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
46
|
+
config[key] = {
|
|
47
|
+
name: state.name,
|
|
48
|
+
type: state.type,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return config;
|
|
52
|
+
}
|
|
53
|
+
export function getDefaultStatusTransitions(states) {
|
|
54
|
+
const defaultTodo = states.find((s) => s.type === "unstarted")?.name ||
|
|
55
|
+
states.find((s) => s.name === "Todo")?.name ||
|
|
56
|
+
states[0]?.name ||
|
|
57
|
+
"Todo";
|
|
58
|
+
const defaultInProgress = states.find((s) => s.type === "started")?.name ||
|
|
59
|
+
states.find((s) => s.name === "In Progress")?.name ||
|
|
60
|
+
"In Progress";
|
|
61
|
+
const defaultDone = states.find((s) => s.type === "completed")?.name ||
|
|
62
|
+
states.find((s) => s.name === "Done")?.name ||
|
|
63
|
+
"Done";
|
|
64
|
+
const defaultTesting = states.find((s) => s.name === "Testing")?.name ||
|
|
65
|
+
states.find((s) => s.name === "In Review")?.name;
|
|
66
|
+
return {
|
|
67
|
+
todo: defaultTodo,
|
|
68
|
+
in_progress: defaultInProgress,
|
|
69
|
+
done: defaultDone,
|
|
70
|
+
testing: defaultTesting,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function buildConfig(teams, users, labels, states, statusTransitions, currentCycle) {
|
|
74
|
+
return {
|
|
75
|
+
teams: buildTeamsConfig(teams),
|
|
76
|
+
users: buildUsersConfig(users),
|
|
77
|
+
labels: buildLabelsConfig(labels),
|
|
78
|
+
priorities: {
|
|
79
|
+
urgent: { value: 1, name: "Urgent" },
|
|
80
|
+
high: { value: 2, name: "High" },
|
|
81
|
+
medium: { value: 3, name: "Medium" },
|
|
82
|
+
low: { value: 4, name: "Low" },
|
|
83
|
+
},
|
|
84
|
+
statuses: buildStatusesConfig(states),
|
|
85
|
+
status_transitions: statusTransitions,
|
|
86
|
+
priority_order: ["urgent", "high", "medium", "low", "none"],
|
|
87
|
+
current_cycle: currentCycle
|
|
88
|
+
? {
|
|
89
|
+
id: currentCycle.id,
|
|
90
|
+
name: currentCycle.name || `Cycle #${currentCycle.number}`,
|
|
91
|
+
start_date: currentCycle.startsAt?.toISOString().split("T")[0] || "",
|
|
92
|
+
end_date: currentCycle.endsAt?.toISOString().split("T")[0] || "",
|
|
93
|
+
}
|
|
94
|
+
: undefined,
|
|
95
|
+
cycle_history: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function findUserKey(usersConfig, userId) {
|
|
99
|
+
return (Object.entries(usersConfig).find(([_, u]) => u.id === userId)?.[0] || "user");
|
|
100
|
+
}
|
|
101
|
+
export function findTeamKey(teamsConfig, teamId) {
|
|
102
|
+
return (Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
|
|
103
|
+
Object.keys(teamsConfig)[0]);
|
|
104
|
+
}
|
|
105
|
+
export function buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel, excludeLabels, excludeAssignees) {
|
|
106
|
+
return {
|
|
107
|
+
current_user: currentUserKey,
|
|
108
|
+
team: primaryTeamKey,
|
|
109
|
+
teams: selectedTeamKeys.length > 1 ? selectedTeamKeys : undefined,
|
|
110
|
+
label: defaultLabel,
|
|
111
|
+
exclude_labels: excludeLabels && excludeLabels.length > 0 ? excludeLabels : undefined,
|
|
112
|
+
exclude_assignees: excludeAssignees && excludeAssignees.length > 0
|
|
113
|
+
? excludeAssignees
|
|
114
|
+
: undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Task } from "../utils.js";
|
|
2
|
+
export declare const PRIORITY_LABELS: Record<number, string>;
|
|
3
|
+
export declare function getStatusIcon(localStatus: Task["localStatus"]): string;
|
|
4
|
+
export declare function displayTaskHeader(task: Task, icon?: string): void;
|
|
5
|
+
export declare function displayTaskInfo(task: Task): void;
|
|
6
|
+
export declare function displayTaskStatus(task: Task): void;
|
|
7
|
+
export declare function displayTaskDescription(task: Task): void;
|
|
8
|
+
export declare function displayTaskAttachments(task: Task): void;
|
|
9
|
+
export declare function displayTaskComments(task: Task): void;
|
|
10
|
+
export declare function displayTaskFooter(): void;
|
|
11
|
+
export declare function displayTaskFull(task: Task, icon?: string): void;
|
|
12
|
+
export declare function displayTaskWithStatus(task: Task): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const PRIORITY_LABELS = {
|
|
2
|
+
0: "⚪ None",
|
|
3
|
+
1: "🔴 Urgent",
|
|
4
|
+
2: "🟠 High",
|
|
5
|
+
3: "🟡 Medium",
|
|
6
|
+
4: "🟢 Low",
|
|
7
|
+
};
|
|
8
|
+
export function getStatusIcon(localStatus) {
|
|
9
|
+
switch (localStatus) {
|
|
10
|
+
case "completed":
|
|
11
|
+
return "✅";
|
|
12
|
+
case "in-progress":
|
|
13
|
+
return "🔄";
|
|
14
|
+
case "blocked-backend":
|
|
15
|
+
return "🚫";
|
|
16
|
+
default:
|
|
17
|
+
return "📋";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function displayTaskHeader(task, icon) {
|
|
21
|
+
const separator = "═".repeat(50);
|
|
22
|
+
console.log(`\n${separator}`);
|
|
23
|
+
console.log(`${icon || getStatusIcon(task.localStatus)} ${task.id}: ${task.title}`);
|
|
24
|
+
console.log(separator);
|
|
25
|
+
}
|
|
26
|
+
export function displayTaskInfo(task) {
|
|
27
|
+
console.log(`Priority: ${PRIORITY_LABELS[task.priority] || "None"}`);
|
|
28
|
+
console.log(`Labels: ${task.labels.join(", ")}`);
|
|
29
|
+
if (task.assignee)
|
|
30
|
+
console.log(`Assignee: ${task.assignee}`);
|
|
31
|
+
console.log(`Branch: ${task.branch || "N/A"}`);
|
|
32
|
+
if (task.url)
|
|
33
|
+
console.log(`URL: ${task.url}`);
|
|
34
|
+
}
|
|
35
|
+
export function displayTaskStatus(task) {
|
|
36
|
+
console.log(`\nStatus:`);
|
|
37
|
+
console.log(` Local: ${task.localStatus}`);
|
|
38
|
+
console.log(` Linear: ${task.status}`);
|
|
39
|
+
}
|
|
40
|
+
export function displayTaskDescription(task) {
|
|
41
|
+
if (task.description) {
|
|
42
|
+
console.log(`\n📝 Description:\n${task.description}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function displayTaskAttachments(task) {
|
|
46
|
+
if (task.attachments && task.attachments.length > 0) {
|
|
47
|
+
console.log(`\n📎 Attachments:`);
|
|
48
|
+
for (const att of task.attachments) {
|
|
49
|
+
console.log(` - ${att.title}: ${att.url}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function displayTaskComments(task) {
|
|
54
|
+
if (task.comments && task.comments.length > 0) {
|
|
55
|
+
console.log(`\n💬 Comments (${task.comments.length}):`);
|
|
56
|
+
for (const comment of task.comments) {
|
|
57
|
+
const date = new Date(comment.createdAt).toLocaleDateString();
|
|
58
|
+
console.log(`\n [${comment.user || "Unknown"} - ${date}]`);
|
|
59
|
+
const lines = comment.body.split("\n");
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
console.log(` ${line}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function displayTaskFooter() {
|
|
67
|
+
console.log(`\n${"─".repeat(50)}`);
|
|
68
|
+
}
|
|
69
|
+
export function displayTaskFull(task, icon) {
|
|
70
|
+
displayTaskHeader(task, icon);
|
|
71
|
+
displayTaskInfo(task);
|
|
72
|
+
displayTaskDescription(task);
|
|
73
|
+
displayTaskAttachments(task);
|
|
74
|
+
displayTaskComments(task);
|
|
75
|
+
displayTaskFooter();
|
|
76
|
+
}
|
|
77
|
+
export function displayTaskWithStatus(task) {
|
|
78
|
+
displayTaskHeader(task);
|
|
79
|
+
displayTaskStatus(task);
|
|
80
|
+
console.log(`\nInfo:`);
|
|
81
|
+
console.log(` Priority: ${PRIORITY_LABELS[task.priority] || "None"}`);
|
|
82
|
+
console.log(` Labels: ${task.labels.join(", ")}`);
|
|
83
|
+
console.log(` Assignee: ${task.assignee || "Unassigned"}`);
|
|
84
|
+
console.log(` Branch: ${task.branch || "N/A"}`);
|
|
85
|
+
if (task.url)
|
|
86
|
+
console.log(` URL: ${task.url}`);
|
|
87
|
+
displayTaskDescription(task);
|
|
88
|
+
displayTaskAttachments(task);
|
|
89
|
+
displayTaskComments(task);
|
|
90
|
+
displayTaskFooter();
|
|
91
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CommitInfo {
|
|
2
|
+
shortHash: string;
|
|
3
|
+
fullHash: string;
|
|
4
|
+
message: string;
|
|
5
|
+
diffStat: string;
|
|
6
|
+
commitUrl: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function getLatestCommit(): CommitInfo | null;
|
|
9
|
+
export declare function formatCommitLink(commit: CommitInfo): string;
|
|
10
|
+
export declare function buildCompletionComment(commit: CommitInfo, aiMessage?: string): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
function getRemoteUrl() {
|
|
3
|
+
try {
|
|
4
|
+
return execSync("git remote get-url origin", {
|
|
5
|
+
encoding: "utf-8",
|
|
6
|
+
}).trim();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function buildCommitUrl(remoteUrl, fullHash) {
|
|
13
|
+
// Handle SSH or HTTPS URLs
|
|
14
|
+
// git@gitlab.com:org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
|
|
15
|
+
// https://gitlab.com/org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
|
|
16
|
+
if (remoteUrl.includes("gitlab")) {
|
|
17
|
+
const match = remoteUrl.match(/(?:git@|https:\/\/)([^:/]+)[:\\/](.+?)(?:\.git)?$/);
|
|
18
|
+
if (match) {
|
|
19
|
+
return `https://${match[1]}/${match[2]}/-/commit/${fullHash}`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else if (remoteUrl.includes("github")) {
|
|
23
|
+
const match = remoteUrl.match(/(?:git@|https:\/\/)([^:/]+)[:\\/](.+?)(?:\.git)?$/);
|
|
24
|
+
if (match) {
|
|
25
|
+
return `https://${match[1]}/${match[2]}/commit/${fullHash}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
export function getLatestCommit() {
|
|
31
|
+
try {
|
|
32
|
+
const shortHash = execSync("git rev-parse --short HEAD", {
|
|
33
|
+
encoding: "utf-8",
|
|
34
|
+
}).trim();
|
|
35
|
+
const fullHash = execSync("git rev-parse HEAD", {
|
|
36
|
+
encoding: "utf-8",
|
|
37
|
+
}).trim();
|
|
38
|
+
const message = execSync("git log -1 --format=%s", {
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
}).trim();
|
|
41
|
+
const diffStat = execSync("git diff HEAD~1 --stat --stat-width=60", {
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
}).trim();
|
|
44
|
+
let commitUrl = null;
|
|
45
|
+
const remoteUrl = getRemoteUrl();
|
|
46
|
+
if (remoteUrl) {
|
|
47
|
+
commitUrl = buildCommitUrl(remoteUrl, fullHash);
|
|
48
|
+
}
|
|
49
|
+
return { shortHash, fullHash, message, diffStat, commitUrl };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function formatCommitLink(commit) {
|
|
56
|
+
return commit.commitUrl
|
|
57
|
+
? `[${commit.shortHash}](${commit.commitUrl})`
|
|
58
|
+
: `\`${commit.shortHash}\``;
|
|
59
|
+
}
|
|
60
|
+
export function buildCompletionComment(commit, aiMessage) {
|
|
61
|
+
const commitLink = formatCommitLink(commit);
|
|
62
|
+
const commentParts = [
|
|
63
|
+
"## ✅ 開發完成",
|
|
64
|
+
"",
|
|
65
|
+
"### 🤖 AI 修復說明",
|
|
66
|
+
aiMessage || "_No description provided_",
|
|
67
|
+
"",
|
|
68
|
+
"### 📝 Commit Info",
|
|
69
|
+
`**Commit:** ${commitLink}`,
|
|
70
|
+
`**Message:** ${commit.message}`,
|
|
71
|
+
"",
|
|
72
|
+
"### 📊 Changes",
|
|
73
|
+
"```",
|
|
74
|
+
commit.diffStat,
|
|
75
|
+
"```",
|
|
76
|
+
];
|
|
77
|
+
return commentParts.join("\n");
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Config, type StatusTransitions } from "../utils.js";
|
|
2
|
+
export interface WorkflowStateInfo {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getWorkflowStates(config: Config, teamKey: string): Promise<WorkflowStateInfo[]>;
|
|
8
|
+
export declare function getStatusTransitions(config: Config): StatusTransitions;
|
|
9
|
+
export declare function updateIssueStatus(linearId: string, targetStatusName: string, config: Config, teamKey: string): Promise<boolean>;
|
|
10
|
+
export declare function addComment(issueId: string, body: string): Promise<boolean>;
|
|
11
|
+
export declare function mapLocalStatusToLinear(localStatus: "pending" | "in-progress" | "completed" | "blocked-backend", config: Config): string | undefined;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getLinearClient, getTeamId, } from "../utils.js";
|
|
2
|
+
export async function getWorkflowStates(config, teamKey) {
|
|
3
|
+
const client = getLinearClient();
|
|
4
|
+
const teamId = getTeamId(config, teamKey);
|
|
5
|
+
const statesData = await client.workflowStates({
|
|
6
|
+
filter: { team: { id: { eq: teamId } } },
|
|
7
|
+
});
|
|
8
|
+
return statesData.nodes.map((s) => ({
|
|
9
|
+
id: s.id,
|
|
10
|
+
name: s.name,
|
|
11
|
+
type: s.type,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
export function getStatusTransitions(config) {
|
|
15
|
+
return (config.status_transitions || {
|
|
16
|
+
todo: "Todo",
|
|
17
|
+
in_progress: "In Progress",
|
|
18
|
+
done: "Done",
|
|
19
|
+
testing: "Testing",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function updateIssueStatus(linearId, targetStatusName, config, teamKey) {
|
|
23
|
+
try {
|
|
24
|
+
const client = getLinearClient();
|
|
25
|
+
const states = await getWorkflowStates(config, teamKey);
|
|
26
|
+
const targetState = states.find((s) => s.name === targetStatusName);
|
|
27
|
+
if (targetState) {
|
|
28
|
+
await client.updateIssue(linearId, { stateId: targetState.id });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error("Failed to update Linear:", e);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function addComment(issueId, body) {
|
|
39
|
+
try {
|
|
40
|
+
const client = getLinearClient();
|
|
41
|
+
await client.createComment({ issueId, body });
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.error("Failed to add comment:", e);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function mapLocalStatusToLinear(localStatus, config) {
|
|
50
|
+
const transitions = getStatusTransitions(config);
|
|
51
|
+
switch (localStatus) {
|
|
52
|
+
case "pending":
|
|
53
|
+
return transitions.todo;
|
|
54
|
+
case "in-progress":
|
|
55
|
+
return transitions.in_progress;
|
|
56
|
+
case "completed":
|
|
57
|
+
return transitions.done;
|
|
58
|
+
default:
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { displayTaskWithStatus, getStatusIcon } from "./lib/display.js";
|
|
3
|
+
import { getStatusTransitions, mapLocalStatusToLinear, updateIssueStatus, } from "./lib/linear.js";
|
|
4
|
+
import { loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
|
|
5
|
+
const LOCAL_STATUS_ORDER = [
|
|
6
|
+
"pending",
|
|
7
|
+
"in-progress",
|
|
8
|
+
"completed",
|
|
9
|
+
"blocked-backend",
|
|
10
|
+
];
|
|
11
|
+
function parseArgs(args) {
|
|
12
|
+
let issueId;
|
|
13
|
+
let setStatus;
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg === "--set" || arg === "-s") {
|
|
17
|
+
setStatus = args[++i];
|
|
18
|
+
}
|
|
19
|
+
else if (!arg.startsWith("-")) {
|
|
20
|
+
issueId = arg;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { issueId, setStatus };
|
|
24
|
+
}
|
|
25
|
+
async function status() {
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
28
|
+
console.log(`Usage: ttt status [issue-id] [--set <status>]
|
|
29
|
+
|
|
30
|
+
Show or modify the status of an issue.
|
|
31
|
+
|
|
32
|
+
Arguments:
|
|
33
|
+
issue-id Issue ID (e.g., MP-624). If omitted, shows in-progress task.
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
-s, --set <status> Set local and Linear status. Values:
|
|
37
|
+
+1 Move to next status (pending → in-progress → completed)
|
|
38
|
+
-1 Move to previous status
|
|
39
|
+
+2 Skip two statuses forward
|
|
40
|
+
-2 Skip two statuses backward
|
|
41
|
+
pending Set to pending
|
|
42
|
+
in-progress Set to in-progress
|
|
43
|
+
completed Set to completed
|
|
44
|
+
blocked Set to blocked-backend
|
|
45
|
+
todo Set Linear to Todo status
|
|
46
|
+
done Set Linear to Done status
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
ttt status # Show current in-progress task
|
|
50
|
+
ttt status MP-624 # Show status of specific issue
|
|
51
|
+
ttt status MP-624 --set +1 # Move to next status
|
|
52
|
+
ttt status --set pending # Reset current task to pending`);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
const { issueId: argIssueId, setStatus } = parseArgs(args);
|
|
56
|
+
const config = await loadConfig();
|
|
57
|
+
const localConfig = await loadLocalConfig();
|
|
58
|
+
const data = await loadCycleData();
|
|
59
|
+
if (!data) {
|
|
60
|
+
console.error("No cycle data found. Run ttt sync first.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
// Find task
|
|
64
|
+
let task;
|
|
65
|
+
let issueId = argIssueId;
|
|
66
|
+
if (!issueId) {
|
|
67
|
+
const inProgressTasks = data.tasks.filter((t) => t.localStatus === "in-progress");
|
|
68
|
+
if (inProgressTasks.length === 0) {
|
|
69
|
+
console.log("No in-progress task found.");
|
|
70
|
+
console.log("\nAll tasks:");
|
|
71
|
+
for (const t of data.tasks) {
|
|
72
|
+
console.log(` ${getStatusIcon(t.localStatus)} ${t.id}: ${t.title} [${t.localStatus}]`);
|
|
73
|
+
}
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
task = inProgressTasks[0];
|
|
77
|
+
issueId = task.id;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
task = data.tasks.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
|
|
81
|
+
if (!task) {
|
|
82
|
+
console.error(`Issue ${issueId} not found in local data.`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// If setting status
|
|
87
|
+
if (setStatus) {
|
|
88
|
+
const currentIndex = LOCAL_STATUS_ORDER.indexOf(task.localStatus);
|
|
89
|
+
let newLocalStatus;
|
|
90
|
+
let newLinearStatus;
|
|
91
|
+
// Parse status change
|
|
92
|
+
if (setStatus === "+1") {
|
|
93
|
+
const newIndex = Math.min(currentIndex + 1, LOCAL_STATUS_ORDER.length - 1);
|
|
94
|
+
newLocalStatus = LOCAL_STATUS_ORDER[newIndex];
|
|
95
|
+
}
|
|
96
|
+
else if (setStatus === "-1") {
|
|
97
|
+
const newIndex = Math.max(currentIndex - 1, 0);
|
|
98
|
+
newLocalStatus = LOCAL_STATUS_ORDER[newIndex];
|
|
99
|
+
}
|
|
100
|
+
else if (setStatus === "+2") {
|
|
101
|
+
const newIndex = Math.min(currentIndex + 2, LOCAL_STATUS_ORDER.length - 1);
|
|
102
|
+
newLocalStatus = LOCAL_STATUS_ORDER[newIndex];
|
|
103
|
+
}
|
|
104
|
+
else if (setStatus === "-2") {
|
|
105
|
+
const newIndex = Math.max(currentIndex - 2, 0);
|
|
106
|
+
newLocalStatus = LOCAL_STATUS_ORDER[newIndex];
|
|
107
|
+
}
|
|
108
|
+
else if ([
|
|
109
|
+
"pending",
|
|
110
|
+
"in-progress",
|
|
111
|
+
"completed",
|
|
112
|
+
"blocked-backend",
|
|
113
|
+
"blocked",
|
|
114
|
+
].includes(setStatus)) {
|
|
115
|
+
newLocalStatus =
|
|
116
|
+
setStatus === "blocked"
|
|
117
|
+
? "blocked-backend"
|
|
118
|
+
: setStatus;
|
|
119
|
+
}
|
|
120
|
+
else if (["todo", "in_progress", "done", "testing"].includes(setStatus)) {
|
|
121
|
+
const transitions = getStatusTransitions(config);
|
|
122
|
+
newLinearStatus =
|
|
123
|
+
transitions[setStatus] ?? undefined;
|
|
124
|
+
if (setStatus === "todo") {
|
|
125
|
+
newLocalStatus = "pending";
|
|
126
|
+
}
|
|
127
|
+
else if (setStatus === "in_progress") {
|
|
128
|
+
newLocalStatus = "in-progress";
|
|
129
|
+
}
|
|
130
|
+
else if (setStatus === "done") {
|
|
131
|
+
newLocalStatus = "completed";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.error(`Unknown status: ${setStatus}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
// Update local status
|
|
139
|
+
if (newLocalStatus && newLocalStatus !== task.localStatus) {
|
|
140
|
+
const oldStatus = task.localStatus;
|
|
141
|
+
task.localStatus = newLocalStatus;
|
|
142
|
+
await saveCycleData(data);
|
|
143
|
+
console.log(`Local: ${task.id} ${oldStatus} → ${newLocalStatus}`);
|
|
144
|
+
}
|
|
145
|
+
// Update Linear status
|
|
146
|
+
if (newLinearStatus || newLocalStatus) {
|
|
147
|
+
let targetStateName = newLinearStatus;
|
|
148
|
+
if (!targetStateName && newLocalStatus) {
|
|
149
|
+
targetStateName = mapLocalStatusToLinear(newLocalStatus, config);
|
|
150
|
+
}
|
|
151
|
+
if (targetStateName) {
|
|
152
|
+
const success = await updateIssueStatus(task.linearId, targetStateName, config, localConfig.team);
|
|
153
|
+
if (success) {
|
|
154
|
+
console.log(`Linear: ${task.id} → ${targetStateName}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Display task info using shared function
|
|
160
|
+
displayTaskWithStatus(task);
|
|
161
|
+
}
|
|
162
|
+
status().catch(console.error);
|