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.
- package/LICENSE +21 -0
- package/README.md +44 -145
- package/README.zh-TW.md +65 -451
- package/dist/scripts/done-job.js +23 -3
- package/dist/scripts/init.js +346 -15
- package/dist/scripts/lib/config-builder.d.ts +1 -1
- package/dist/scripts/lib/config-builder.js +5 -1
- package/dist/scripts/lib/display.js +14 -3
- package/dist/scripts/lib/images.d.ts +9 -0
- package/dist/scripts/lib/images.js +107 -0
- package/dist/scripts/lib/linear.d.ts +1 -1
- package/dist/scripts/lib/linear.js +2 -0
- package/dist/scripts/status.js +15 -15
- package/dist/scripts/sync.js +100 -9
- package/dist/scripts/utils.d.ts +6 -1
- package/dist/scripts/utils.js +2 -0
- package/dist/scripts/work-on.js +8 -2
- package/package.json +1 -1
- package/templates/claude-code-commands/work-on.md +12 -2
package/dist/scripts/status.js
CHANGED
|
@@ -6,7 +6,7 @@ const LOCAL_STATUS_ORDER = [
|
|
|
6
6
|
"pending",
|
|
7
7
|
"in-progress",
|
|
8
8
|
"completed",
|
|
9
|
-
"blocked
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}`);
|
package/dist/scripts/sync.js
CHANGED
|
@@ -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
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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;
|
package/dist/scripts/utils.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/scripts/utils.js
CHANGED
|
@@ -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)
|
package/dist/scripts/work-on.js
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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.
|
|
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
|
-
###
|
|
52
|
+
### 6. Complete
|
|
43
53
|
|
|
44
54
|
Use `/done-job` to mark task as completed
|
|
45
55
|
|