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.
Files changed (48) hide show
  1. package/README.md +59 -8
  2. package/README.zh-TW.md +111 -22
  3. package/{bin → dist/bin}/cli.js +41 -19
  4. package/dist/scripts/config/filters.d.ts +2 -0
  5. package/dist/scripts/config/filters.js +71 -0
  6. package/dist/scripts/config/show.d.ts +2 -0
  7. package/dist/scripts/config/show.js +33 -0
  8. package/dist/scripts/config/status.d.ts +2 -0
  9. package/dist/scripts/config/status.js +73 -0
  10. package/dist/scripts/config/teams.d.ts +2 -0
  11. package/dist/scripts/config/teams.js +59 -0
  12. package/dist/scripts/config.js +47 -0
  13. package/dist/scripts/done-job.js +169 -0
  14. package/dist/scripts/init.d.ts +2 -0
  15. package/dist/scripts/init.js +369 -0
  16. package/dist/scripts/lib/config-builder.d.ts +41 -0
  17. package/dist/scripts/lib/config-builder.js +116 -0
  18. package/dist/scripts/lib/display.d.ts +12 -0
  19. package/dist/scripts/lib/display.js +91 -0
  20. package/dist/scripts/lib/git.d.ts +10 -0
  21. package/dist/scripts/lib/git.js +78 -0
  22. package/dist/scripts/lib/linear.d.ts +11 -0
  23. package/dist/scripts/lib/linear.js +61 -0
  24. package/dist/scripts/status.d.ts +2 -0
  25. package/dist/scripts/status.js +162 -0
  26. package/dist/scripts/sync.js +247 -0
  27. package/{scripts → dist/scripts}/utils.d.ts +11 -3
  28. package/{scripts → dist/scripts}/utils.js +33 -27
  29. package/dist/scripts/work-on.js +102 -0
  30. package/package.json +52 -50
  31. package/templates/claude-code-commands/done-job.md +45 -0
  32. package/templates/claude-code-commands/sync-linear.md +32 -0
  33. package/templates/claude-code-commands/work-on.md +41 -0
  34. package/bin/cli.ts +0 -125
  35. package/scripts/done-job.js +0 -230
  36. package/scripts/done-job.ts +0 -263
  37. package/scripts/init.js +0 -331
  38. package/scripts/init.ts +0 -375
  39. package/scripts/sync.js +0 -178
  40. package/scripts/sync.ts +0 -211
  41. package/scripts/utils.ts +0 -236
  42. package/scripts/work-on.js +0 -138
  43. package/scripts/work-on.ts +0 -161
  44. /package/{bin → dist/bin}/cli.d.ts +0 -0
  45. /package/{scripts/init.d.ts → dist/scripts/config.d.ts} +0 -0
  46. /package/{scripts → dist/scripts}/done-job.d.ts +0 -0
  47. /package/{scripts → dist/scripts}/sync.d.ts +0 -0
  48. /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,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
@@ -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);