team-toon-tack 1.0.4 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Linear task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/init.ts CHANGED
@@ -277,17 +277,14 @@ async function init() {
277
277
  complete: 'Done',
278
278
  need_review: 'In Review'
279
279
  },
280
+ priority_order: ['urgent', 'high', 'medium', 'low', 'none'],
280
281
  current_cycle: currentCycle ? {
281
282
  id: currentCycle.id,
282
283
  name: currentCycle.name || 'Cycle',
283
284
  start_date: currentCycle.startsAt?.toISOString().split('T')[0] || '',
284
285
  end_date: currentCycle.endsAt?.toISOString().split('T')[0] || ''
285
- } : {
286
- id: '',
287
- name: 'No active cycle',
288
- start_date: '',
289
- end_date: ''
290
- }
286
+ } : undefined,
287
+ cycle_history: []
291
288
  };
292
289
 
293
290
  // Find current user key
@@ -309,11 +306,23 @@ async function init() {
309
306
  const existingContent = await fs.readFile(paths.configPath, 'utf-8');
310
307
  const existingConfig = decode(existingContent) as unknown as Config;
311
308
 
312
- // Merge: new data takes precedence but preserve existing custom fields
309
+ // Merge: preserve existing custom fields
313
310
  config.status_transitions = {
314
311
  ...existingConfig.status_transitions,
315
312
  ...config.status_transitions
316
313
  };
314
+ // Preserve cycle history
315
+ if (existingConfig.cycle_history) {
316
+ config.cycle_history = existingConfig.cycle_history;
317
+ }
318
+ // Preserve current_cycle if not fetched fresh
319
+ if (!currentCycle && existingConfig.current_cycle) {
320
+ config.current_cycle = existingConfig.current_cycle;
321
+ }
322
+ // Preserve priority_order if exists
323
+ if (existingConfig.priority_order) {
324
+ config.priority_order = existingConfig.priority_order;
325
+ }
317
326
  } catch {
318
327
  // Ignore merge errors
319
328
  }
package/scripts/sync.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getLinearClient, loadConfig, loadLocalConfig, saveConfig, loadCycleData, saveCycleData, getTeamId, CycleData, Task, Attachment, Comment } from './utils';
1
+ import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, saveConfig, getTeamId, getPrioritySortIndex, CycleData, Task, Attachment, Comment, CycleInfo } from './utils';
2
2
 
3
3
  async function sync() {
4
4
  const args = process.argv.slice(2);
@@ -47,24 +47,46 @@ Examples:
47
47
  }
48
48
 
49
49
  const activeCycle = cycles.nodes[0];
50
- const cycleChanged = config.current_cycle.id !== activeCycle.id;
51
-
52
- if (cycleChanged) {
53
- console.log(`Cycle changed: ${config.current_cycle.name} → ${activeCycle.name}`);
54
- config.current_cycle = {
55
- id: activeCycle.id,
56
- name: activeCycle.name ?? `Cycle`,
57
- start_date: activeCycle.startsAt?.toISOString().split('T')[0] ?? '',
58
- end_date: activeCycle.endsAt?.toISOString().split('T')[0] ?? ''
59
- };
50
+ const cycleId = activeCycle.id;
51
+ const cycleName = activeCycle.name ?? 'Cycle';
52
+ const newCycleInfo: CycleInfo = {
53
+ id: cycleId,
54
+ name: cycleName,
55
+ start_date: activeCycle.startsAt?.toISOString().split('T')[0] ?? '',
56
+ end_date: activeCycle.endsAt?.toISOString().split('T')[0] ?? ''
57
+ };
58
+
59
+ // Check if cycle changed and update config with history
60
+ const existingData = await loadCycleData();
61
+ const oldCycleId = config.current_cycle?.id ?? existingData?.cycleId;
62
+
63
+ if (oldCycleId && oldCycleId !== cycleId) {
64
+ const oldCycleName = config.current_cycle?.name ?? existingData?.cycleName ?? 'Unknown';
65
+ console.log(`Cycle changed: ${oldCycleName} → ${cycleName}`);
66
+
67
+ // Move old cycle to history
68
+ if (config.current_cycle) {
69
+ config.cycle_history = config.cycle_history ?? [];
70
+ config.cycle_history.unshift(config.current_cycle);
71
+ // Keep only last 10 cycles
72
+ if (config.cycle_history.length > 10) {
73
+ config.cycle_history = config.cycle_history.slice(0, 10);
74
+ }
75
+ }
76
+
77
+ // Update current cycle
78
+ config.current_cycle = newCycleInfo;
60
79
  await saveConfig(config);
61
- console.log('Config updated with new cycle.');
80
+ console.log('Config updated with new cycle (old cycle saved to history).');
62
81
  } else {
63
- console.log(`Current cycle: ${config.current_cycle.name}`);
82
+ // Update current cycle info even if ID unchanged (dates might change)
83
+ if (!config.current_cycle || config.current_cycle.id !== cycleId) {
84
+ config.current_cycle = newCycleInfo;
85
+ await saveConfig(config);
86
+ }
87
+ console.log(`Current cycle: ${cycleName}`);
64
88
  }
65
89
 
66
- const cycleId = config.current_cycle.id;
67
-
68
90
  // Phase 2: Fetch workflow states
69
91
  const workflowStates = await client.workflowStates({
70
92
  filter: { team: { id: { eq: teamId } } }
@@ -72,8 +94,7 @@ Examples:
72
94
  const stateMap = new Map(workflowStates.nodes.map(s => [s.name, s.id]));
73
95
  const testingStateId = stateMap.get('Testing');
74
96
 
75
- // Phase 3: Read existing local state
76
- const existingData = await loadCycleData();
97
+ // Phase 3: Build existing tasks map for preserving local status
77
98
  const existingTasksMap = new Map(existingData?.tasks.map(t => [t.id, t]));
78
99
 
79
100
  // Phase 4: Fetch current issues with full content
@@ -168,22 +189,22 @@ Examples:
168
189
  tasks.push(task);
169
190
  }
170
191
 
171
- // Sort by priority (urgent first)
192
+ // Sort by priority using config order
172
193
  tasks.sort((a, b) => {
173
- const pa = a.priority === 0 ? 5 : a.priority;
174
- const pb = b.priority === 0 ? 5 : b.priority;
194
+ const pa = getPrioritySortIndex(a.priority, config.priority_order);
195
+ const pb = getPrioritySortIndex(b.priority, config.priority_order);
175
196
  return pa - pb;
176
197
  });
177
198
 
178
199
  const newData: CycleData = {
179
200
  cycleId: cycleId,
180
- cycleName: config.current_cycle.name,
201
+ cycleName: cycleName,
181
202
  updatedAt: new Date().toISOString(),
182
203
  tasks: tasks
183
204
  };
184
205
 
185
206
  await saveCycleData(newData);
186
- console.log(`\n✅ Synced ${tasks.length} tasks for ${config.current_cycle.name}.`);
207
+ console.log(`\n✅ Synced ${tasks.length} tasks for ${cycleName}.`);
187
208
  if (updatedCount > 0) {
188
209
  console.log(` Updated ${updatedCount} issues to Testing in Linear.`);
189
210
  }
package/scripts/utils.ts CHANGED
@@ -52,14 +52,41 @@ export interface LabelConfig {
52
52
  color?: string;
53
53
  }
54
54
 
55
+ export interface CycleInfo {
56
+ id: string;
57
+ name: string;
58
+ start_date: string;
59
+ end_date: string;
60
+ }
61
+
55
62
  export interface Config {
56
63
  teams: Record<string, TeamConfig>;
57
64
  users: Record<string, UserConfig>;
58
- labels: Record<string, LabelConfig>;
59
- priorities: Record<string, { value: number; name: string }>;
60
- statuses: Record<string, { name: string; type: string }>;
61
- status_transitions: Record<string, string>;
62
- current_cycle: { id: string; name: string; start_date: string; end_date: string };
65
+ labels?: Record<string, LabelConfig>;
66
+ priorities?: Record<string, { value: number; name: string }>;
67
+ statuses?: Record<string, { name: string; type: string }>;
68
+ status_transitions?: Record<string, string>;
69
+ priority_order?: string[]; // e.g., ['urgent', 'high', 'medium', 'low', 'none']
70
+ current_cycle?: CycleInfo;
71
+ cycle_history?: CycleInfo[];
72
+ }
73
+
74
+ // Linear priority value to name mapping (fixed by Linear API)
75
+ export const PRIORITY_NAMES: Record<number, string> = {
76
+ 0: 'none',
77
+ 1: 'urgent',
78
+ 2: 'high',
79
+ 3: 'medium',
80
+ 4: 'low'
81
+ };
82
+
83
+ export const DEFAULT_PRIORITY_ORDER = ['urgent', 'high', 'medium', 'low', 'none'];
84
+
85
+ export function getPrioritySortIndex(priority: number, priorityOrder?: string[]): number {
86
+ const order = priorityOrder ?? DEFAULT_PRIORITY_ORDER;
87
+ const name = PRIORITY_NAMES[priority] ?? 'none';
88
+ const index = order.indexOf(name);
89
+ return index === -1 ? order.length : index;
63
90
  }
64
91
 
65
92
  export interface Attachment {
@@ -1,8 +1,8 @@
1
1
  import prompts from 'prompts';
2
- import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, getUserEmail, getTeamId } from './utils';
2
+ import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, getUserEmail, getTeamId, getPrioritySortIndex } from './utils';
3
3
 
4
4
  const PRIORITY_LABELS: Record<number, string> = {
5
- 0: '⚪',
5
+ 0: '⚪ None',
6
6
  1: '🔴 Urgent',
7
7
  2: '🟠 High',
8
8
  3: '🟡 Medium',
@@ -53,8 +53,8 @@ Examples:
53
53
  !excludedEmails.has(t.assignee ?? '')
54
54
  )
55
55
  .sort((a, b) => {
56
- const pa = a.priority === 0 ? 5 : a.priority;
57
- const pb = b.priority === 0 ? 5 : b.priority;
56
+ const pa = getPrioritySortIndex(a.priority, config.priority_order);
57
+ const pb = getPrioritySortIndex(b.priority, config.priority_order);
58
58
  return pa - pb;
59
59
  });
60
60