team-toon-tack 1.6.2 → 1.7.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.
@@ -1,7 +1,8 @@
1
1
  import prompts from "prompts";
2
2
  import { buildCompletionComment, getLatestCommit } from "./lib/git.js";
3
3
  import { addComment, getStatusTransitions, getWorkflowStates, updateIssueStatus, } from "./lib/linear.js";
4
- import { getLinearClient, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
4
+ import { syncSingleIssue } from "./lib/sync.js";
5
+ import { getLinearClient, loadConfig, loadCycleData, loadLocalConfig, } from "./utils.js";
5
6
  function parseArgs(args) {
6
7
  let issueId;
7
8
  let message;
@@ -144,10 +145,15 @@ Examples:
144
145
  }
145
146
  }
146
147
  }
147
- // Update local status
148
- task.localStatus = "completed";
149
- await saveCycleData(data);
150
- console.log(`Local: ${task.id} → completed`);
148
+ // Sync full issue data from Linear (including new comment)
149
+ const syncedTask = await syncSingleIssue(task.id, {
150
+ config,
151
+ localConfig,
152
+ preserveLocalStatus: false, // Let remote status determine local status
153
+ });
154
+ if (syncedTask) {
155
+ console.log(`Synced: ${syncedTask.id} → ${syncedTask.status} (local: ${syncedTask.localStatus})`);
156
+ }
151
157
  // Summary
152
158
  console.log(`\n${"═".repeat(50)}`);
153
159
  console.log(`✅ ${task.id}: ${task.title}`);
@@ -66,8 +66,7 @@ export function getDefaultStatusTransitions(states) {
66
66
  const defaultDone = states.find((s) => s.type === "completed")?.name ||
67
67
  findStatusByKeyword(states, ["done", "complete"]) ||
68
68
  "Done";
69
- const defaultTesting = findStatusByKeyword(states, ["testing", "review"]) ||
70
- undefined;
69
+ const defaultTesting = findStatusByKeyword(states, ["testing", "review"]) || undefined;
71
70
  return {
72
71
  todo: defaultTodo,
73
72
  in_progress: defaultInProgress,
@@ -0,0 +1,13 @@
1
+ import type { LinearClient } from "@linear/sdk";
2
+ import { type Config, type LocalConfig, type Task } from "../utils.js";
3
+ export interface SyncIssueOptions {
4
+ config: Config;
5
+ localConfig: LocalConfig;
6
+ client?: LinearClient;
7
+ preserveLocalStatus?: boolean;
8
+ }
9
+ /**
10
+ * Sync a single issue from Linear and update local cycle data
11
+ * Returns the updated task or null if issue not found
12
+ */
13
+ export declare function syncSingleIssue(issueId: string, options: SyncIssueOptions): Promise<Task | null>;
@@ -0,0 +1,97 @@
1
+ import { getLinearClient, loadCycleData, saveCycleData, getPrioritySortIndex, } from "../utils.js";
2
+ import { getStatusTransitions } from "./linear.js";
3
+ /**
4
+ * Sync a single issue from Linear and update local cycle data
5
+ * Returns the updated task or null if issue not found
6
+ */
7
+ export async function syncSingleIssue(issueId, options) {
8
+ const { config, localConfig: _localConfig, preserveLocalStatus = true, } = options;
9
+ const client = options.client ?? getLinearClient();
10
+ // Search for the issue
11
+ const searchResult = await client.searchIssues(issueId);
12
+ const matchingIssue = searchResult.nodes.find((i) => i.identifier === issueId);
13
+ if (!matchingIssue) {
14
+ console.error(`Issue ${issueId} not found in Linear.`);
15
+ return null;
16
+ }
17
+ // Fetch full issue data
18
+ const issue = await client.issue(matchingIssue.id);
19
+ const assignee = await issue.assignee;
20
+ const assigneeEmail = assignee?.email;
21
+ const labels = await issue.labels();
22
+ const labelNames = labels.nodes.map((l) => l.name);
23
+ const state = await issue.state;
24
+ const parent = await issue.parent;
25
+ const attachmentsData = await issue.attachments();
26
+ const commentsData = await issue.comments();
27
+ // Build attachments list
28
+ const attachments = attachmentsData.nodes.map((a) => ({
29
+ id: a.id,
30
+ title: a.title,
31
+ url: a.url,
32
+ sourceType: a.sourceType ?? undefined,
33
+ }));
34
+ // Build comments list
35
+ const comments = await Promise.all(commentsData.nodes.map(async (c) => {
36
+ const user = await c.user;
37
+ return {
38
+ id: c.id,
39
+ body: c.body,
40
+ createdAt: c.createdAt.toISOString(),
41
+ user: user?.displayName ?? user?.email,
42
+ };
43
+ }));
44
+ // Determine local status
45
+ let localStatus = "pending";
46
+ const existingData = await loadCycleData();
47
+ if (preserveLocalStatus && existingData) {
48
+ const existingTask = existingData.tasks.find((t) => t.id === issueId);
49
+ if (existingTask) {
50
+ localStatus = existingTask.localStatus;
51
+ }
52
+ }
53
+ // Map remote status to local status if not preserving
54
+ if (!preserveLocalStatus && state) {
55
+ const transitions = getStatusTransitions(config);
56
+ if (state.name === transitions.done) {
57
+ localStatus = "completed";
58
+ }
59
+ else if (state.name === transitions.in_progress) {
60
+ localStatus = "in-progress";
61
+ }
62
+ else {
63
+ localStatus = "pending";
64
+ }
65
+ }
66
+ const task = {
67
+ id: issue.identifier,
68
+ linearId: issue.id,
69
+ title: issue.title,
70
+ status: state ? state.name : "Unknown",
71
+ localStatus: localStatus,
72
+ assignee: assigneeEmail,
73
+ priority: issue.priority,
74
+ labels: labelNames,
75
+ branch: issue.branchName,
76
+ description: issue.description ?? undefined,
77
+ parentIssueId: parent ? parent.identifier : undefined,
78
+ url: issue.url,
79
+ attachments: attachments.length > 0 ? attachments : undefined,
80
+ comments: comments.length > 0 ? comments : undefined,
81
+ };
82
+ // Update cycle data
83
+ if (existingData) {
84
+ const existingTasks = existingData.tasks.filter((t) => t.id !== issueId);
85
+ const finalTasks = [...existingTasks, task];
86
+ // Sort by priority
87
+ finalTasks.sort((a, b) => {
88
+ const pa = getPrioritySortIndex(a.priority, config.priority_order);
89
+ const pb = getPrioritySortIndex(b.priority, config.priority_order);
90
+ return pa - pb;
91
+ });
92
+ existingData.tasks = finalTasks;
93
+ existingData.updatedAt = new Date().toISOString();
94
+ await saveCycleData(existingData);
95
+ }
96
+ return task;
97
+ }
@@ -135,17 +135,13 @@ Examples:
135
135
  console.error(`Unknown status: ${setStatus}`);
136
136
  process.exit(1);
137
137
  }
138
+ // Track if we need to save
139
+ let needsSave = false;
140
+ const oldLocalStatus = task.localStatus;
138
141
  // Update local status
139
- if (newLocalStatus) {
140
- if (newLocalStatus !== task.localStatus) {
141
- const oldStatus = task.localStatus;
142
- task.localStatus = newLocalStatus;
143
- await saveCycleData(data);
144
- console.log(`Local: ${task.id} ${oldStatus} → ${newLocalStatus}`);
145
- }
146
- else {
147
- console.log(`Local: ${task.id} already ${newLocalStatus}`);
148
- }
142
+ if (newLocalStatus && newLocalStatus !== task.localStatus) {
143
+ task.localStatus = newLocalStatus;
144
+ needsSave = true;
149
145
  }
150
146
  // Update Linear status
151
147
  if (newLinearStatus || newLocalStatus) {
@@ -156,10 +152,22 @@ Examples:
156
152
  if (targetStateName) {
157
153
  const success = await updateIssueStatus(task.linearId, targetStateName, config, localConfig.team);
158
154
  if (success) {
155
+ task.status = targetStateName;
156
+ needsSave = true;
159
157
  console.log(`Linear: ${task.id} → ${targetStateName}`);
160
158
  }
161
159
  }
162
160
  }
161
+ // Save if anything changed
162
+ if (needsSave) {
163
+ await saveCycleData(data);
164
+ if (newLocalStatus && newLocalStatus !== oldLocalStatus) {
165
+ console.log(`Local: ${task.id} ${oldLocalStatus} → ${newLocalStatus}`);
166
+ }
167
+ }
168
+ else if (newLocalStatus) {
169
+ console.log(`Local: ${task.id} already ${newLocalStatus}`);
170
+ }
163
171
  }
164
172
  // Display task info using shared function
165
173
  displayTaskWithStatus(task);
@@ -83,16 +83,17 @@ Examples:
83
83
  // Mark as In Progress
84
84
  if (task.localStatus === "pending") {
85
85
  task.localStatus = "in-progress";
86
- await saveCycleData(data);
87
- console.log(`Local: ${task.id} → in-progress`);
88
86
  // Update Linear
89
87
  if (task.linearId && process.env.LINEAR_API_KEY) {
90
88
  const transitions = getStatusTransitions(config);
91
89
  const success = await updateIssueStatus(task.linearId, transitions.in_progress, config, localConfig.team);
92
90
  if (success) {
91
+ task.status = transitions.in_progress;
93
92
  console.log(`Linear: ${task.id} → ${transitions.in_progress}`);
94
93
  }
95
94
  }
95
+ await saveCycleData(data);
96
+ console.log(`Local: ${task.id} → in-progress`);
96
97
  }
97
98
  // Display task info
98
99
  displayTaskFull(task, "👷");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Linear task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,9 +28,20 @@ Script displays title, description, priority, labels, and attachments.
28
28
  1. Read the issue description carefully
29
29
  2. Explore related code
30
30
  3. Implement the fix/feature
31
- 4. Run `bun type-check && bun lint`
32
- 5. Commit with conventional format
33
- 6. Use `/done-job` to complete
31
+ 4. Commit with conventional format
32
+
33
+ ### 4. Verify
34
+
35
+ Run project-required verification before completing:
36
+
37
+ ```bash
38
+ # Run verification procedure defined in project
39
+ # (e.g., type-check, lint, test, build)
40
+ ```
41
+
42
+ ### 5. Complete
43
+
44
+ Use `/done-job` to mark task as completed
34
45
 
35
46
  ## Example Usage
36
47