team-toon-tack 1.7.0 → 2.0.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.
@@ -6,7 +6,7 @@ const LOCAL_STATUS_ORDER = [
6
6
  "pending",
7
7
  "in-progress",
8
8
  "completed",
9
- "blocked-backend",
9
+ "blocked",
10
10
  ];
11
11
  function parseArgs(args) {
12
12
  let issueId;
@@ -41,7 +41,7 @@ Options:
41
41
  pending Set to pending
42
42
  in-progress Set to in-progress
43
43
  completed Set to completed
44
- blocked Set to blocked-backend
44
+ blocked Set to blocked (syncs to Linear if configured)
45
45
  todo Set Linear to Todo status
46
46
  done Set Linear to Done status
47
47
 
@@ -105,17 +105,8 @@ Examples:
105
105
  const newIndex = Math.max(currentIndex - 2, 0);
106
106
  newLocalStatus = LOCAL_STATUS_ORDER[newIndex];
107
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;
108
+ else if (["pending", "in-progress", "completed", "blocked"].includes(setStatus)) {
109
+ newLocalStatus = setStatus;
119
110
  }
120
111
  else if (["todo", "in_progress", "done", "testing"].includes(setStatus)) {
121
112
  const transitions = getStatusTransitions(config);
@@ -143,8 +134,9 @@ Examples:
143
134
  task.localStatus = newLocalStatus;
144
135
  needsSave = true;
145
136
  }
146
- // Update Linear status
147
- if (newLinearStatus || newLocalStatus) {
137
+ // Update Linear status (only if status_source is 'remote' or not set)
138
+ const statusSource = localConfig.status_source || "remote";
139
+ if (statusSource === "remote" && (newLinearStatus || newLocalStatus)) {
148
140
  let targetStateName = newLinearStatus;
149
141
  if (!targetStateName && newLocalStatus) {
150
142
  targetStateName = mapLocalStatusToLinear(newLocalStatus, config);
@@ -158,12 +150,20 @@ Examples:
158
150
  }
159
151
  }
160
152
  }
153
+ else if (statusSource === "local" &&
154
+ (newLinearStatus || newLocalStatus)) {
155
+ // Local mode: just note that Linear wasn't updated
156
+ needsSave = true;
157
+ }
161
158
  // Save if anything changed
162
159
  if (needsSave) {
163
160
  await saveCycleData(data);
164
161
  if (newLocalStatus && newLocalStatus !== oldLocalStatus) {
165
162
  console.log(`Local: ${task.id} ${oldLocalStatus} → ${newLocalStatus}`);
166
163
  }
164
+ if (statusSource === "local") {
165
+ console.log(`(Linear status not updated - use 'sync --update' to push)`);
166
+ }
167
167
  }
168
168
  else if (newLocalStatus) {
169
169
  console.log(`Local: ${task.id} already ${newLocalStatus}`);
@@ -1,15 +1,20 @@
1
- import { getLinearClient, getPrioritySortIndex, getTeamId, loadConfig, loadCycleData, loadLocalConfig, saveConfig, saveCycleData, } from "./utils.js";
1
+ import { getLinearClient, getPaths, getPrioritySortIndex, getTeamId, loadConfig, loadCycleData, loadLocalConfig, saveConfig, saveCycleData, } from "./utils.js";
2
+ import { clearIssueImages, downloadLinearImage, ensureOutputDir, extractLinearImageUrls, isLinearImageUrl, } from "./lib/images.js";
2
3
  async function sync() {
3
4
  const args = process.argv.slice(2);
4
5
  // Handle help flag
5
6
  if (args.includes("--help") || args.includes("-h")) {
6
- console.log(`Usage: ttt sync [issue-id]
7
+ console.log(`Usage: ttt sync [issue-id] [--update]
7
8
 
8
9
  Sync issues from Linear to local cycle.ttt file.
9
10
 
10
11
  Arguments:
11
12
  issue-id Optional. Sync only this specific issue (e.g., MP-624)
12
13
 
14
+ Options:
15
+ --update Push local status changes to Linear (for local mode users)
16
+ This updates Linear with your local in-progress/completed statuses
17
+
13
18
  What it does:
14
19
  - Fetches active cycle from Linear
15
20
  - Downloads all issues matching configured filters
@@ -19,15 +24,21 @@ What it does:
19
24
  Examples:
20
25
  ttt sync # Sync all matching issues
21
26
  ttt sync MP-624 # Sync only this specific issue
27
+ ttt sync --update # Push local changes to Linear, then sync
22
28
  ttt sync -d .ttt # Sync using .ttt directory`);
23
29
  process.exit(0);
24
30
  }
31
+ // Check for --update flag
32
+ const shouldUpdate = args.includes("--update");
25
33
  // Parse issue ID argument (if provided)
26
34
  const singleIssueId = args.find((arg) => !arg.startsWith("-") && arg.match(/^[A-Z]+-\d+$/i));
27
35
  const config = await loadConfig();
28
36
  const localConfig = await loadLocalConfig();
29
37
  const client = getLinearClient();
30
38
  const teamId = getTeamId(config, localConfig.team);
39
+ const { outputPath } = getPaths();
40
+ // Ensure output directory exists
41
+ await ensureOutputDir(outputPath);
31
42
  // Build excluded labels set
32
43
  const excludedLabels = new Set(localConfig.exclude_labels ?? []);
33
44
  // Phase 1: Fetch active cycle directly from team
@@ -91,6 +102,50 @@ Examples:
91
102
  const testingStateId = statusTransitions.testing
92
103
  ? stateMap.get(statusTransitions.testing)
93
104
  : undefined;
105
+ const inProgressStateId = stateMap.get(statusTransitions.in_progress);
106
+ // Phase 2.5: Push local status changes to Linear (if --update flag)
107
+ if (shouldUpdate && existingData) {
108
+ console.log("Pushing local status changes to Linear...");
109
+ let pushCount = 0;
110
+ for (const task of existingData.tasks) {
111
+ // Map local status to Linear status
112
+ let targetStateId;
113
+ if (task.localStatus === "in-progress" && inProgressStateId) {
114
+ // Check if Linear status is not already in-progress
115
+ if (task.status !== statusTransitions.in_progress) {
116
+ targetStateId = inProgressStateId;
117
+ }
118
+ }
119
+ else if (task.localStatus === "completed" && testingStateId) {
120
+ // Check if Linear status is not already testing/done
121
+ const terminalStatuses = [statusTransitions.done];
122
+ if (statusTransitions.testing)
123
+ terminalStatuses.push(statusTransitions.testing);
124
+ if (!terminalStatuses.includes(task.status)) {
125
+ targetStateId = testingStateId;
126
+ }
127
+ }
128
+ if (targetStateId) {
129
+ try {
130
+ await client.updateIssue(task.linearId, { stateId: targetStateId });
131
+ const targetName = targetStateId === inProgressStateId
132
+ ? statusTransitions.in_progress
133
+ : statusTransitions.testing;
134
+ console.log(` ${task.id} → ${targetName}`);
135
+ pushCount++;
136
+ }
137
+ catch (e) {
138
+ console.error(` Failed to update ${task.id}:`, e);
139
+ }
140
+ }
141
+ }
142
+ if (pushCount > 0) {
143
+ console.log(`Pushed ${pushCount} status updates to Linear.`);
144
+ }
145
+ else {
146
+ console.log("No local changes to push.");
147
+ }
148
+ }
94
149
  // Phase 3: Build existing tasks map for preserving local status
95
150
  const existingTasksMap = new Map(existingData?.tasks.map((t) => [t.id, t]));
96
151
  // Phase 4: Fetch current issues with full content
@@ -145,13 +200,49 @@ Examples:
145
200
  const parent = await issue.parent;
146
201
  const attachmentsData = await issue.attachments();
147
202
  const commentsData = await issue.comments();
148
- // Build attachments list
149
- const attachments = attachmentsData.nodes.map((a) => ({
150
- id: a.id,
151
- title: a.title,
152
- url: a.url,
153
- sourceType: a.sourceType ?? undefined,
154
- }));
203
+ // Clear old images for this issue before downloading new ones
204
+ await clearIssueImages(outputPath, issue.identifier);
205
+ // Build attachments list and download Linear images
206
+ const attachments = [];
207
+ for (const a of attachmentsData.nodes) {
208
+ const attachment = {
209
+ id: a.id,
210
+ title: a.title,
211
+ url: a.url,
212
+ sourceType: a.sourceType ?? undefined,
213
+ };
214
+ // Download Linear domain images
215
+ if (isLinearImageUrl(a.url)) {
216
+ const localPath = await downloadLinearImage(a.url, issue.identifier, a.id, outputPath);
217
+ if (localPath) {
218
+ attachment.localPath = localPath;
219
+ }
220
+ }
221
+ attachments.push(attachment);
222
+ }
223
+ // Extract and download images from description
224
+ if (issue.description) {
225
+ const descriptionImageUrls = extractLinearImageUrls(issue.description);
226
+ for (const url of descriptionImageUrls) {
227
+ // Generate a short ID from URL (last segment of path)
228
+ const urlPath = new URL(url).pathname;
229
+ const segments = urlPath.split("/").filter(Boolean);
230
+ const imageId = segments[segments.length - 1] || `desc_${Date.now()}`;
231
+ // Skip if already in attachments
232
+ if (attachments.some((a) => a.url === url))
233
+ continue;
234
+ const localPath = await downloadLinearImage(url, issue.identifier, imageId, outputPath);
235
+ if (localPath) {
236
+ attachments.push({
237
+ id: imageId,
238
+ title: `Description Image`,
239
+ url: url,
240
+ sourceType: "description",
241
+ localPath: localPath,
242
+ });
243
+ }
244
+ }
245
+ }
155
246
  // Build comments list
156
247
  const comments = await Promise.all(commentsData.nodes.map(async (c) => {
157
248
  const user = await c.user;
@@ -4,6 +4,7 @@ export declare function getPaths(): {
4
4
  configPath: string;
5
5
  cyclePath: string;
6
6
  localPath: string;
7
+ outputPath: string;
7
8
  };
8
9
  export interface TeamConfig {
9
10
  id: string;
@@ -32,6 +33,7 @@ export interface StatusTransitions {
32
33
  in_progress: string;
33
34
  done: string;
34
35
  testing?: string;
36
+ blocked?: string;
35
37
  }
36
38
  export interface Config {
37
39
  teams: Record<string, TeamConfig>;
@@ -58,6 +60,7 @@ export interface Attachment {
58
60
  title: string;
59
61
  url: string;
60
62
  sourceType?: string;
63
+ localPath?: string;
61
64
  }
62
65
  export interface Comment {
63
66
  id: string;
@@ -70,7 +73,7 @@ export interface Task {
70
73
  linearId: string;
71
74
  title: string;
72
75
  status: string;
73
- localStatus: "pending" | "in-progress" | "completed" | "blocked-backend";
76
+ localStatus: "pending" | "in-progress" | "completed" | "blocked";
74
77
  assignee?: string;
75
78
  priority: number;
76
79
  labels: string[];
@@ -92,8 +95,10 @@ export interface LocalConfig {
92
95
  current_user: string;
93
96
  team: string;
94
97
  teams?: string[];
98
+ qa_pm_team?: string;
95
99
  exclude_labels?: string[];
96
100
  label?: string;
101
+ status_source?: "remote" | "local";
97
102
  }
98
103
  export declare function fileExists(filePath: string): Promise<boolean>;
99
104
  export declare function loadConfig(): Promise<Config>;
@@ -19,12 +19,14 @@ const BASE_DIR = getBaseDir();
19
19
  const CONFIG_PATH = path.join(BASE_DIR, "config.toon");
20
20
  const CYCLE_PATH = path.join(BASE_DIR, "cycle.toon");
21
21
  const LOCAL_PATH = path.join(BASE_DIR, "local.toon");
22
+ const OUTPUT_PATH = path.join(BASE_DIR, "output");
22
23
  export function getPaths() {
23
24
  return {
24
25
  baseDir: BASE_DIR,
25
26
  configPath: CONFIG_PATH,
26
27
  cyclePath: CYCLE_PATH,
27
28
  localPath: LOCAL_PATH,
29
+ outputPath: OUTPUT_PATH,
28
30
  };
29
31
  }
30
32
  // Linear priority value to name mapping (fixed by Linear API)
@@ -83,8 +83,11 @@ Examples:
83
83
  // Mark as In Progress
84
84
  if (task.localStatus === "pending") {
85
85
  task.localStatus = "in-progress";
86
- // Update Linear
87
- if (task.linearId && process.env.LINEAR_API_KEY) {
86
+ // Update Linear (only if status_source is 'remote' or not set)
87
+ const statusSource = localConfig.status_source || "remote";
88
+ if (statusSource === "remote" &&
89
+ task.linearId &&
90
+ process.env.LINEAR_API_KEY) {
88
91
  const transitions = getStatusTransitions(config);
89
92
  const success = await updateIssueStatus(task.linearId, transitions.in_progress, config, localConfig.team);
90
93
  if (success) {
@@ -94,6 +97,9 @@ Examples:
94
97
  }
95
98
  await saveCycleData(data);
96
99
  console.log(`Local: ${task.id} → in-progress`);
100
+ if (statusSource === "local") {
101
+ console.log(`(Linear status not updated - use 'sync --update' to push)`);
102
+ }
97
103
  }
98
104
  // Display task info
99
105
  displayTaskFull(task, "👷");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "1.7.0",
3
+ "version": "2.0.0",
4
4
  "description": "Linear task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,17 @@ Script displays title, description, priority, labels, and attachments.
30
30
  3. Implement the fix/feature
31
31
  4. Commit with conventional format
32
32
 
33
- ### 4. Verify
33
+ ### 4. Handle Blockers (if any)
34
+
35
+ If you encounter a blocker (waiting for backend, design, external dependency):
36
+
37
+ ```bash
38
+ ttt status --set blocked
39
+ ```
40
+
41
+ Add a comment explaining the blocker, then move to another task.
42
+
43
+ ### 5. Verify
34
44
 
35
45
  Run project-required verification before completing:
36
46
 
@@ -39,7 +49,7 @@ Run project-required verification before completing:
39
49
  # (e.g., type-check, lint, test, build)
40
50
  ```
41
51
 
42
- ### 5. Complete
52
+ ### 6. Complete
43
53
 
44
54
  Use `/done-job` to mark task as completed
45
55