team-toon-tack 1.6.1 → 1.6.3

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.
@@ -9,10 +9,6 @@ export async function configureFilters(config, localConfig) {
9
9
  filter: { team: { id: { eq: teamId } } },
10
10
  });
11
11
  const labels = labelsData.nodes;
12
- // Fetch users
13
- const team = await client.team(teamId);
14
- const members = await team.members();
15
- const users = members.nodes;
16
12
  // Label filter (optional)
17
13
  const labelChoices = [
18
14
  { title: "(No filter - sync all labels)", value: "" },
@@ -38,34 +34,13 @@ export async function configureFilters(config, localConfig) {
38
34
  selected: localConfig.exclude_labels?.includes(l.name),
39
35
  })),
40
36
  });
41
- // Exclude users
42
- const excludeUsersResponse = await prompts({
43
- type: "multiselect",
44
- name: "excludeUsers",
45
- message: "Select users to exclude (space to select):",
46
- choices: users.map((u) => {
47
- const key = (u.displayName || u.name || u.email?.split("@")[0] || "user")
48
- .toLowerCase()
49
- .replace(/[^a-z0-9]/g, "_");
50
- return {
51
- title: `${u.displayName || u.name} (${u.email})`,
52
- value: key,
53
- selected: localConfig.exclude_assignees?.includes(key),
54
- };
55
- }),
56
- });
57
37
  localConfig.label = labelResponse.label || undefined;
58
38
  localConfig.exclude_labels =
59
39
  excludeLabelsResponse.excludeLabels?.length > 0
60
40
  ? excludeLabelsResponse.excludeLabels
61
41
  : undefined;
62
- localConfig.exclude_assignees =
63
- excludeUsersResponse.excludeUsers?.length > 0
64
- ? excludeUsersResponse.excludeUsers
65
- : undefined;
66
42
  await saveLocalConfig(localConfig);
67
43
  console.log("\n✅ Filters updated:");
68
44
  console.log(` Label: ${localConfig.label || "(all)"}`);
69
45
  console.log(` Exclude labels: ${localConfig.exclude_labels?.join(", ") || "(none)"}`);
70
- console.log(` Exclude users: ${localConfig.exclude_assignees?.join(", ") || "(none)"}`);
71
46
  }
@@ -15,7 +15,6 @@ export function showConfig(config, localConfig) {
15
15
  console.log("\nFilters:");
16
16
  console.log(` Label: ${localConfig.label || "(all)"}`);
17
17
  console.log(` Exclude labels: ${localConfig.exclude_labels?.join(", ") || "(none)"}`);
18
- console.log(` Exclude users: ${localConfig.exclude_assignees?.join(", ") || "(none)"}`);
19
18
  // Status Mappings
20
19
  console.log("\nStatus Mappings:");
21
20
  if (config.status_transitions) {
@@ -12,7 +12,7 @@ async function config() {
12
12
  Subcommands:
13
13
  show Show current configuration
14
14
  status Configure status mappings (todo, in_progress, done, testing)
15
- filters Configure filters (label, exclude_labels, exclude_assignees)
15
+ filters Configure filters (label, exclude_labels)
16
16
  teams Configure team selection
17
17
 
18
18
  Examples:
@@ -328,8 +328,6 @@ async function init() {
328
328
  localConfig.teams = existingLocal.teams;
329
329
  if (existingLocal.label)
330
330
  localConfig.label = existingLocal.label;
331
- if (existingLocal.exclude_assignees)
332
- localConfig.exclude_assignees = existingLocal.exclude_assignees;
333
331
  if (existingLocal.exclude_labels)
334
332
  localConfig.exclude_labels = existingLocal.exclude_labels;
335
333
  }
@@ -38,4 +38,4 @@ export declare function getDefaultStatusTransitions(states: LinearState[]): Stat
38
38
  export declare function buildConfig(teams: LinearTeam[], users: LinearUser[], labels: LinearLabel[], states: LinearState[], statusTransitions: StatusTransitions, currentCycle?: LinearCycle): Config;
39
39
  export declare function findUserKey(usersConfig: Record<string, UserConfig>, userId: string): string;
40
40
  export declare function findTeamKey(teamsConfig: Record<string, TeamConfig>, teamId: string): string;
41
- export declare function buildLocalConfig(currentUserKey: string, primaryTeamKey: string, selectedTeamKeys: string[], defaultLabel?: string, excludeLabels?: string[], excludeAssignees?: string[]): LocalConfig;
41
+ export declare function buildLocalConfig(currentUserKey: string, primaryTeamKey: string, selectedTeamKeys: string[], defaultLabel?: string, excludeLabels?: string[]): LocalConfig;
@@ -50,19 +50,24 @@ export function buildStatusesConfig(states) {
50
50
  }
51
51
  return config;
52
52
  }
53
+ // Helper to find status by keyword (case insensitive)
54
+ function findStatusByKeyword(states, keywords) {
55
+ const lowerKeywords = keywords.map((k) => k.toLowerCase());
56
+ return states.find((s) => lowerKeywords.some((k) => s.name.toLowerCase().includes(k)))?.name;
57
+ }
53
58
  export function getDefaultStatusTransitions(states) {
54
59
  const defaultTodo = states.find((s) => s.type === "unstarted")?.name ||
55
- states.find((s) => s.name === "Todo")?.name ||
60
+ findStatusByKeyword(states, ["todo", "pending"]) ||
56
61
  states[0]?.name ||
57
62
  "Todo";
58
63
  const defaultInProgress = states.find((s) => s.type === "started")?.name ||
59
- states.find((s) => s.name === "In Progress")?.name ||
64
+ findStatusByKeyword(states, ["in progress", "progress"]) ||
60
65
  "In Progress";
61
66
  const defaultDone = states.find((s) => s.type === "completed")?.name ||
62
- states.find((s) => s.name === "Done")?.name ||
67
+ findStatusByKeyword(states, ["done", "complete"]) ||
63
68
  "Done";
64
- const defaultTesting = states.find((s) => s.name === "Testing")?.name ||
65
- states.find((s) => s.name === "In Review")?.name;
69
+ const defaultTesting = findStatusByKeyword(states, ["testing", "review"]) ||
70
+ undefined;
66
71
  return {
67
72
  todo: defaultTodo,
68
73
  in_progress: defaultInProgress,
@@ -102,15 +107,12 @@ export function findTeamKey(teamsConfig, teamId) {
102
107
  return (Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
103
108
  Object.keys(teamsConfig)[0]);
104
109
  }
105
- export function buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel, excludeLabels, excludeAssignees) {
110
+ export function buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel, excludeLabels) {
106
111
  return {
107
112
  current_user: currentUserKey,
108
113
  team: primaryTeamKey,
109
114
  teams: selectedTeamKeys.length > 1 ? selectedTeamKeys : undefined,
110
115
  label: defaultLabel,
111
116
  exclude_labels: excludeLabels && excludeLabels.length > 0 ? excludeLabels : undefined,
112
- exclude_assignees: excludeAssignees && excludeAssignees.length > 0
113
- ? excludeAssignees
114
- : undefined,
115
117
  };
116
118
  }
@@ -135,12 +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
142
  if (newLocalStatus && newLocalStatus !== task.localStatus) {
140
- const oldStatus = task.localStatus;
141
143
  task.localStatus = newLocalStatus;
142
- await saveCycleData(data);
143
- console.log(`Local: ${task.id} ${oldStatus} → ${newLocalStatus}`);
144
+ needsSave = true;
144
145
  }
145
146
  // Update Linear status
146
147
  if (newLinearStatus || newLocalStatus) {
@@ -151,10 +152,22 @@ Examples:
151
152
  if (targetStateName) {
152
153
  const success = await updateIssueStatus(task.linearId, targetStateName, config, localConfig.team);
153
154
  if (success) {
155
+ task.status = targetStateName;
156
+ needsSave = true;
154
157
  console.log(`Linear: ${task.id} → ${targetStateName}`);
155
158
  }
156
159
  }
157
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
+ }
158
171
  }
159
172
  // Display task info using shared function
160
173
  displayTaskWithStatus(task);
@@ -28,10 +28,6 @@ Examples:
28
28
  const localConfig = await loadLocalConfig();
29
29
  const client = getLinearClient();
30
30
  const teamId = getTeamId(config, localConfig.team);
31
- // Build excluded emails from local config
32
- const excludedEmails = new Set((localConfig.exclude_assignees ?? [])
33
- .map((key) => config.users[key]?.email)
34
- .filter(Boolean));
35
31
  // Build excluded labels set
36
32
  const excludedLabels = new Set(localConfig.exclude_labels ?? []);
37
33
  // Phase 1: Fetch active cycle directly from team
@@ -139,10 +135,6 @@ Examples:
139
135
  const issue = await client.issue(issueNode.id);
140
136
  const assignee = await issue.assignee;
141
137
  const assigneeEmail = assignee?.email;
142
- // Skip excluded assignees
143
- if (assigneeEmail && excludedEmails.has(assigneeEmail)) {
144
- continue;
145
- }
146
138
  const labels = await issue.labels();
147
139
  const labelNames = labels.nodes.map((l) => l.name);
148
140
  // Skip if any label is in excluded list
@@ -92,7 +92,6 @@ export interface LocalConfig {
92
92
  current_user: string;
93
93
  team: string;
94
94
  teams?: string[];
95
- exclude_assignees?: string[];
96
95
  exclude_labels?: string[];
97
96
  label?: string;
98
97
  }
@@ -25,12 +25,11 @@ Examples:
25
25
  process.exit(1);
26
26
  }
27
27
  const localConfig = await loadLocalConfig();
28
- // Build excluded emails list from user keys
29
- const excludedEmails = new Set((localConfig.exclude_assignees ?? [])
30
- .map((key) => config.users[key]?.email)
31
- .filter(Boolean));
28
+ // Get current user email for filtering
29
+ const currentUserEmail = config.users[localConfig.current_user]?.email;
32
30
  const pendingTasks = data.tasks
33
- .filter((t) => t.localStatus === "pending" && !excludedEmails.has(t.assignee ?? ""))
31
+ .filter((t) => t.localStatus === "pending" &&
32
+ (!currentUserEmail || t.assignee === currentUserEmail))
34
33
  .sort((a, b) => {
35
34
  const pa = getPrioritySortIndex(a.priority, config.priority_order);
36
35
  const pb = getPrioritySortIndex(b.priority, config.priority_order);
@@ -84,16 +83,17 @@ Examples:
84
83
  // Mark as In Progress
85
84
  if (task.localStatus === "pending") {
86
85
  task.localStatus = "in-progress";
87
- await saveCycleData(data);
88
- console.log(`Local: ${task.id} → in-progress`);
89
86
  // Update Linear
90
87
  if (task.linearId && process.env.LINEAR_API_KEY) {
91
88
  const transitions = getStatusTransitions(config);
92
89
  const success = await updateIssueStatus(task.linearId, transitions.in_progress, config, localConfig.team);
93
90
  if (success) {
91
+ task.status = transitions.in_progress;
94
92
  console.log(`Linear: ${task.id} → ${transitions.in_progress}`);
95
93
  }
96
94
  }
95
+ await saveCycleData(data);
96
+ console.log(`Local: ${task.id} → in-progress`);
97
97
  }
98
98
  // Display task info
99
99
  displayTaskFull(task, "👷");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
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
 
@@ -1,6 +1,6 @@
1
1
  # Linear Team Configuration
2
2
  # Copy this file to config.toon and fill in your team's data
3
- # Run `bun run init` to generate this interactively
3
+ # Run `ttt init` to generate this interactively
4
4
 
5
5
  teams:
6
6
  main:
@@ -12,71 +12,26 @@ users:
12
12
  user1:
13
13
  id: USER1_UUID
14
14
  email: user1@example.com
15
- displayName: user1
15
+ displayName: User One
16
16
  role: Developer
17
17
 
18
- labels:
19
- frontend:
20
- id: LABEL_UUID
21
- name: Frontend
22
- color: "#f2c94c"
23
- backend:
24
- id: LABEL_UUID
25
- name: Backend
26
- color: "#5e6ad2"
27
- bug:
28
- id: LABEL_UUID
29
- name: Bug
30
- color: "#EB5757"
31
- feature:
32
- id: LABEL_UUID
33
- name: Feature
34
- color: "#BB87FC"
35
-
36
- priorities:
37
- urgent:
38
- value: 1
39
- name: Urgent
40
- high:
41
- value: 2
42
- name: High
43
- medium:
44
- value: 3
45
- name: Medium
46
- low:
47
- value: 4
48
- name: Low
49
-
50
- statuses:
51
- backlog:
52
- name: Backlog
53
- type: backlog
54
- todo:
55
- name: Todo
56
- type: unstarted
57
- in_progress:
58
- name: In Progress
59
- type: started
60
- in_review:
61
- name: In Review
62
- type: started
63
- testing:
64
- name: Testing
65
- type: started
66
- done:
67
- name: Done
68
- type: completed
69
- canceled:
70
- name: Canceled
71
- type: canceled
72
-
18
+ # Status mapping for Linear integration
73
19
  status_transitions:
74
- start_work: In Progress
75
- complete: Done
76
- need_review: In Review
20
+ todo: Todo
21
+ in_progress: In Progress
22
+ done: Done
23
+ testing: Testing
24
+
25
+ # Priority ordering (optional, defaults shown)
26
+ priority_order[1]: urgent
27
+ priority_order[2]: high
28
+ priority_order[3]: medium
29
+ priority_order[4]: low
30
+ priority_order[5]: none
77
31
 
78
- current_cycle:
79
- id: CYCLE_UUID
80
- name: Cycle #1
81
- start_date: 2026-01-01
82
- end_date: 2026-01-07
32
+ # Current cycle info (auto-managed by sync)
33
+ # current_cycle:
34
+ # id: CYCLE_UUID
35
+ # name: Cycle #1
36
+ # start_date: 2026-01-01
37
+ # end_date: 2026-01-07
@@ -3,11 +3,14 @@
3
3
  # This file is gitignored and contains personal settings
4
4
 
5
5
  # Your user key (must match a key in config.toon users section)
6
+ # Only tasks assigned to this user will be synced
6
7
  current_user: user1
7
8
 
8
- # Filter issues by label (optional, defaults to first label in config)
9
+ # Primary team (required)
10
+ team: main
11
+
12
+ # Filter issues by label (optional)
9
13
  label: Frontend
10
14
 
11
- # Exclude issues assigned to these users (optional)
12
- # Use user keys from config.toon
13
- # exclude_assignees[1]: backend_dev
15
+ # Exclude issues with these labels (optional)
16
+ # exclude_labels[1]: Backend