team-toon-tack 1.0.12 → 1.6.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.
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env bun
2
+ import { getLinearClient, getTeamId, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
3
+ const PRIORITY_LABELS = {
4
+ 0: "⚪ None",
5
+ 1: "🔴 Urgent",
6
+ 2: "🟠 High",
7
+ 3: "🟡 Medium",
8
+ 4: "🟢 Low",
9
+ };
10
+ const LOCAL_STATUS_ORDER = [
11
+ "pending",
12
+ "in-progress",
13
+ "completed",
14
+ "blocked-backend",
15
+ ];
16
+ function parseArgs(args) {
17
+ let issueId;
18
+ let setStatus;
19
+ for (let i = 0; i < args.length; i++) {
20
+ const arg = args[i];
21
+ if (arg === "--set" || arg === "-s") {
22
+ setStatus = args[++i];
23
+ }
24
+ else if (!arg.startsWith("-")) {
25
+ issueId = arg;
26
+ }
27
+ }
28
+ return { issueId, setStatus };
29
+ }
30
+ async function status() {
31
+ const args = process.argv.slice(2);
32
+ // Handle help flag
33
+ if (args.includes("--help") || args.includes("-h")) {
34
+ console.log(`Usage: ttt status [issue-id] [--set <status>]
35
+
36
+ Show or modify the status of an issue.
37
+
38
+ Arguments:
39
+ issue-id Issue ID (e.g., MP-624). If omitted, shows in-progress task.
40
+
41
+ Options:
42
+ -s, --set <status> Set local and Linear status. Values:
43
+ +1 Move to next status (pending → in-progress → completed)
44
+ -1 Move to previous status
45
+ +2 Skip two statuses forward
46
+ -2 Skip two statuses backward
47
+ pending Set to pending
48
+ in-progress Set to in-progress
49
+ completed Set to completed
50
+ blocked Set to blocked-backend
51
+ todo Set Linear to Todo status
52
+ done Set Linear to Done status
53
+
54
+ Examples:
55
+ ttt status # Show current in-progress task
56
+ ttt status MP-624 # Show status of specific issue
57
+ ttt status MP-624 --set +1 # Move to next status
58
+ ttt status MP-624 --set done # Mark as done
59
+ ttt status --set pending # Reset current task to pending`);
60
+ process.exit(0);
61
+ }
62
+ const { issueId: argIssueId, setStatus } = parseArgs(args);
63
+ const config = await loadConfig();
64
+ const localConfig = await loadLocalConfig();
65
+ const data = await loadCycleData();
66
+ if (!data) {
67
+ console.error("No cycle data found. Run ttt sync first.");
68
+ process.exit(1);
69
+ }
70
+ // Find task
71
+ let task;
72
+ let issueId = argIssueId;
73
+ if (!issueId) {
74
+ // Find current in-progress task
75
+ const inProgressTasks = data.tasks.filter((t) => t.localStatus === "in-progress");
76
+ if (inProgressTasks.length === 0) {
77
+ console.log("No in-progress task found.");
78
+ console.log("\nAll tasks:");
79
+ for (const t of data.tasks) {
80
+ const statusIcon = t.localStatus === "completed"
81
+ ? "✅"
82
+ : t.localStatus === "in-progress"
83
+ ? "🔄"
84
+ : t.localStatus === "blocked-backend"
85
+ ? "🚫"
86
+ : "📋";
87
+ console.log(` ${statusIcon} ${t.id}: ${t.title} [${t.localStatus}]`);
88
+ }
89
+ process.exit(0);
90
+ }
91
+ task = inProgressTasks[0];
92
+ issueId = task.id;
93
+ }
94
+ else {
95
+ task = data.tasks.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
96
+ if (!task) {
97
+ console.error(`Issue ${issueId} not found in local data.`);
98
+ process.exit(1);
99
+ }
100
+ }
101
+ // If setting status
102
+ if (setStatus) {
103
+ const currentIndex = LOCAL_STATUS_ORDER.indexOf(task.localStatus);
104
+ let newLocalStatus;
105
+ let newLinearStatus;
106
+ // Parse status change
107
+ if (setStatus === "+1") {
108
+ const newIndex = Math.min(currentIndex + 1, LOCAL_STATUS_ORDER.length - 1);
109
+ newLocalStatus = LOCAL_STATUS_ORDER[newIndex];
110
+ }
111
+ else if (setStatus === "-1") {
112
+ const newIndex = Math.max(currentIndex - 1, 0);
113
+ newLocalStatus = LOCAL_STATUS_ORDER[newIndex];
114
+ }
115
+ else if (setStatus === "+2") {
116
+ const newIndex = Math.min(currentIndex + 2, LOCAL_STATUS_ORDER.length - 1);
117
+ newLocalStatus = LOCAL_STATUS_ORDER[newIndex];
118
+ }
119
+ else if (setStatus === "-2") {
120
+ const newIndex = Math.max(currentIndex - 2, 0);
121
+ newLocalStatus = LOCAL_STATUS_ORDER[newIndex];
122
+ }
123
+ else if ([
124
+ "pending",
125
+ "in-progress",
126
+ "completed",
127
+ "blocked-backend",
128
+ "blocked",
129
+ ].includes(setStatus)) {
130
+ newLocalStatus =
131
+ setStatus === "blocked"
132
+ ? "blocked-backend"
133
+ : setStatus;
134
+ }
135
+ else if (["todo", "in_progress", "done", "testing"].includes(setStatus)) {
136
+ // Map to Linear status
137
+ const statusTransitions = config.status_transitions || {
138
+ todo: "Todo",
139
+ in_progress: "In Progress",
140
+ done: "Done",
141
+ testing: "Testing",
142
+ };
143
+ newLinearStatus =
144
+ statusTransitions[setStatus];
145
+ // Also update local status accordingly
146
+ if (setStatus === "todo") {
147
+ newLocalStatus = "pending";
148
+ }
149
+ else if (setStatus === "in_progress") {
150
+ newLocalStatus = "in-progress";
151
+ }
152
+ else if (setStatus === "done") {
153
+ newLocalStatus = "completed";
154
+ }
155
+ }
156
+ else {
157
+ console.error(`Unknown status: ${setStatus}`);
158
+ process.exit(1);
159
+ }
160
+ // Update local status
161
+ if (newLocalStatus && newLocalStatus !== task.localStatus) {
162
+ const oldStatus = task.localStatus;
163
+ task.localStatus = newLocalStatus;
164
+ await saveCycleData(data);
165
+ console.log(`Local: ${task.id} ${oldStatus} → ${newLocalStatus}`);
166
+ }
167
+ // Update Linear status
168
+ if (newLinearStatus || newLocalStatus) {
169
+ try {
170
+ const client = getLinearClient();
171
+ const teamId = getTeamId(config, localConfig.team);
172
+ const workflowStates = await client.workflowStates({
173
+ filter: { team: { id: { eq: teamId } } },
174
+ });
175
+ let targetStateName = newLinearStatus;
176
+ if (!targetStateName && newLocalStatus) {
177
+ // Map local status to Linear status
178
+ const statusTransitions = config.status_transitions || {
179
+ todo: "Todo",
180
+ in_progress: "In Progress",
181
+ done: "Done",
182
+ };
183
+ if (newLocalStatus === "pending") {
184
+ targetStateName = statusTransitions.todo;
185
+ }
186
+ else if (newLocalStatus === "in-progress") {
187
+ targetStateName = statusTransitions.in_progress;
188
+ }
189
+ else if (newLocalStatus === "completed") {
190
+ targetStateName = statusTransitions.done;
191
+ }
192
+ }
193
+ if (targetStateName) {
194
+ const targetState = workflowStates.nodes.find((s) => s.name === targetStateName);
195
+ if (targetState) {
196
+ await client.updateIssue(task.linearId, {
197
+ stateId: targetState.id,
198
+ });
199
+ console.log(`Linear: ${task.id} → ${targetStateName}`);
200
+ }
201
+ }
202
+ }
203
+ catch (e) {
204
+ console.error("Failed to update Linear:", e);
205
+ }
206
+ }
207
+ }
208
+ // Display task info
209
+ console.log(`\n${"═".repeat(50)}`);
210
+ const statusIcon = task.localStatus === "completed"
211
+ ? "✅"
212
+ : task.localStatus === "in-progress"
213
+ ? "🔄"
214
+ : task.localStatus === "blocked-backend"
215
+ ? "🚫"
216
+ : "📋";
217
+ console.log(`${statusIcon} ${task.id}: ${task.title}`);
218
+ console.log(`${"═".repeat(50)}`);
219
+ console.log(`\nStatus:`);
220
+ console.log(` Local: ${task.localStatus}`);
221
+ console.log(` Linear: ${task.status}`);
222
+ console.log(`\nInfo:`);
223
+ console.log(` Priority: ${PRIORITY_LABELS[task.priority] || "None"}`);
224
+ console.log(` Labels: ${task.labels.join(", ")}`);
225
+ console.log(` Assignee: ${task.assignee || "Unassigned"}`);
226
+ console.log(` Branch: ${task.branch || "N/A"}`);
227
+ if (task.url)
228
+ console.log(` URL: ${task.url}`);
229
+ if (task.description) {
230
+ console.log(`\n📝 Description:\n${task.description}`);
231
+ }
232
+ if (task.attachments && task.attachments.length > 0) {
233
+ console.log(`\n📎 Attachments:`);
234
+ for (const att of task.attachments) {
235
+ console.log(` - ${att.title}: ${att.url}`);
236
+ }
237
+ }
238
+ if (task.comments && task.comments.length > 0) {
239
+ console.log(`\n💬 Comments (${task.comments.length}):`);
240
+ for (const comment of task.comments) {
241
+ const date = new Date(comment.createdAt).toLocaleDateString();
242
+ console.log(`\n [${comment.user || "Unknown"} - ${date}]`);
243
+ const lines = comment.body.split("\n");
244
+ for (const line of lines) {
245
+ console.log(` ${line}`);
246
+ }
247
+ }
248
+ }
249
+ console.log(`\n${"─".repeat(50)}`);
250
+ }
251
+ status().catch(console.error);
@@ -0,0 +1,247 @@
1
+ import { getLinearClient, getPrioritySortIndex, getTeamId, loadConfig, loadCycleData, loadLocalConfig, saveConfig, saveCycleData, } from "./utils.js";
2
+ async function sync() {
3
+ const args = process.argv.slice(2);
4
+ // Handle help flag
5
+ if (args.includes("--help") || args.includes("-h")) {
6
+ console.log(`Usage: ttt sync [issue-id]
7
+
8
+ Sync issues from Linear to local cycle.ttt file.
9
+
10
+ Arguments:
11
+ issue-id Optional. Sync only this specific issue (e.g., MP-624)
12
+
13
+ What it does:
14
+ - Fetches active cycle from Linear
15
+ - Downloads all issues matching configured filters
16
+ - Preserves local status for existing tasks
17
+ - Updates config with new cycle info
18
+
19
+ Examples:
20
+ ttt sync # Sync all matching issues
21
+ ttt sync MP-624 # Sync only this specific issue
22
+ ttt sync -d .ttt # Sync using .ttt directory`);
23
+ process.exit(0);
24
+ }
25
+ // Parse issue ID argument (if provided)
26
+ const singleIssueId = args.find((arg) => !arg.startsWith("-") && arg.match(/^[A-Z]+-\d+$/i));
27
+ const config = await loadConfig();
28
+ const localConfig = await loadLocalConfig();
29
+ const client = getLinearClient();
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
+ // Build excluded labels set
36
+ const excludedLabels = new Set(localConfig.exclude_labels ?? []);
37
+ // Phase 1: Fetch active cycle directly from team
38
+ console.log("Fetching latest cycle...");
39
+ const team = await client.team(teamId);
40
+ const activeCycle = await team.activeCycle;
41
+ if (!activeCycle) {
42
+ console.error("No active cycle found.");
43
+ process.exit(1);
44
+ }
45
+ const cycleId = activeCycle.id;
46
+ const cycleName = activeCycle.name ?? `Cycle #${activeCycle.number}`;
47
+ const newCycleInfo = {
48
+ id: cycleId,
49
+ name: cycleName,
50
+ start_date: activeCycle.startsAt?.toISOString().split("T")[0] ?? "",
51
+ end_date: activeCycle.endsAt?.toISOString().split("T")[0] ?? "",
52
+ };
53
+ // Check if cycle changed and update config with history
54
+ const existingData = await loadCycleData();
55
+ const oldCycleId = config.current_cycle?.id ?? existingData?.cycleId;
56
+ if (oldCycleId && oldCycleId !== cycleId) {
57
+ const oldCycleName = config.current_cycle?.name ?? existingData?.cycleName ?? "Unknown";
58
+ console.log(`Cycle changed: ${oldCycleName} → ${cycleName}`);
59
+ // Move old cycle to history (avoid duplicates)
60
+ if (config.current_cycle) {
61
+ config.cycle_history = config.cycle_history ?? [];
62
+ // Remove if already exists in history
63
+ config.cycle_history = config.cycle_history.filter((c) => c.id !== config.current_cycle?.id);
64
+ config.cycle_history.unshift(config.current_cycle);
65
+ // Keep only last 10 cycles
66
+ if (config.cycle_history.length > 10) {
67
+ config.cycle_history = config.cycle_history.slice(0, 10);
68
+ }
69
+ }
70
+ // Update current cycle
71
+ config.current_cycle = newCycleInfo;
72
+ await saveConfig(config);
73
+ console.log("Config updated with new cycle (old cycle saved to history).");
74
+ }
75
+ else {
76
+ // Update current cycle info even if ID unchanged (dates might change)
77
+ if (!config.current_cycle || config.current_cycle.id !== cycleId) {
78
+ config.current_cycle = newCycleInfo;
79
+ await saveConfig(config);
80
+ }
81
+ console.log(`Current cycle: ${cycleName}`);
82
+ }
83
+ // Phase 2: Fetch workflow states and get status mappings
84
+ const workflowStates = await client.workflowStates({
85
+ filter: { team: { id: { eq: teamId } } },
86
+ });
87
+ const stateMap = new Map(workflowStates.nodes.map((s) => [s.name, s.id]));
88
+ // Get status names from config or use defaults
89
+ const statusTransitions = config.status_transitions || {
90
+ todo: "Todo",
91
+ in_progress: "In Progress",
92
+ done: "Done",
93
+ testing: "Testing",
94
+ };
95
+ const testingStateId = statusTransitions.testing
96
+ ? stateMap.get(statusTransitions.testing)
97
+ : undefined;
98
+ // Phase 3: Build existing tasks map for preserving local status
99
+ const existingTasksMap = new Map(existingData?.tasks.map((t) => [t.id, t]));
100
+ // Phase 4: Fetch current issues with full content
101
+ const filterLabel = localConfig.label;
102
+ const syncStatuses = [statusTransitions.todo, statusTransitions.in_progress];
103
+ let issues;
104
+ if (singleIssueId) {
105
+ // Sync single issue by ID
106
+ console.log(`Fetching issue ${singleIssueId}...`);
107
+ const searchResult = await client.searchIssues(singleIssueId);
108
+ const matchingIssue = searchResult.nodes.find((i) => i.identifier === singleIssueId);
109
+ if (!matchingIssue) {
110
+ console.error(`Issue ${singleIssueId} not found.`);
111
+ process.exit(1);
112
+ }
113
+ issues = { nodes: [matchingIssue] };
114
+ }
115
+ else {
116
+ // Sync all matching issues
117
+ console.log(`Fetching issues with status: ${syncStatuses.join(", ")}${filterLabel ? ` and label: ${filterLabel}` : ""}...`);
118
+ // Build filter - label is optional
119
+ const issueFilter = {
120
+ team: { id: { eq: teamId } },
121
+ cycle: { id: { eq: cycleId } },
122
+ state: { name: { in: syncStatuses } },
123
+ };
124
+ if (filterLabel) {
125
+ issueFilter.labels = { name: { eq: filterLabel } };
126
+ }
127
+ issues = await client.issues({
128
+ filter: issueFilter,
129
+ first: 50,
130
+ });
131
+ }
132
+ if (issues.nodes.length === 0) {
133
+ console.log(`No issues found in current cycle with ${syncStatuses.join("/")} status${filterLabel ? ` and label: ${filterLabel}` : ""}.`);
134
+ }
135
+ const tasks = [];
136
+ let updatedCount = 0;
137
+ for (const issueNode of issues.nodes) {
138
+ // Fetch full issue to get all relations (searchIssues returns IssueSearchResult which lacks some methods)
139
+ const issue = await client.issue(issueNode.id);
140
+ const assignee = await issue.assignee;
141
+ const assigneeEmail = assignee?.email;
142
+ // Skip excluded assignees
143
+ if (assigneeEmail && excludedEmails.has(assigneeEmail)) {
144
+ continue;
145
+ }
146
+ const labels = await issue.labels();
147
+ const labelNames = labels.nodes.map((l) => l.name);
148
+ // Skip if any label is in excluded list
149
+ if (labelNames.some((name) => excludedLabels.has(name))) {
150
+ continue;
151
+ }
152
+ const state = await issue.state;
153
+ const parent = await issue.parent;
154
+ const attachmentsData = await issue.attachments();
155
+ const commentsData = await issue.comments();
156
+ // Build attachments list
157
+ const attachments = attachmentsData.nodes.map((a) => ({
158
+ id: a.id,
159
+ title: a.title,
160
+ url: a.url,
161
+ sourceType: a.sourceType ?? undefined,
162
+ }));
163
+ // Build comments list
164
+ const comments = await Promise.all(commentsData.nodes.map(async (c) => {
165
+ const user = await c.user;
166
+ return {
167
+ id: c.id,
168
+ body: c.body,
169
+ createdAt: c.createdAt.toISOString(),
170
+ user: user?.displayName ?? user?.email,
171
+ };
172
+ }));
173
+ let localStatus = "pending";
174
+ // Preserve local status & sync completed tasks to Linear
175
+ if (existingTasksMap.has(issue.identifier)) {
176
+ const existing = existingTasksMap.get(issue.identifier);
177
+ localStatus = existing?.localStatus ?? "pending";
178
+ if (localStatus === "completed" && state && testingStateId) {
179
+ // Skip if already in terminal states (done, testing, or cancelled type)
180
+ const terminalStates = [statusTransitions.done];
181
+ if (statusTransitions.testing)
182
+ terminalStates.push(statusTransitions.testing);
183
+ const isTerminal = terminalStates.includes(state.name) || state.type === "cancelled";
184
+ if (!isTerminal) {
185
+ console.log(`Updating ${issue.identifier} to ${statusTransitions.testing} in Linear...`);
186
+ await client.updateIssue(issue.id, { stateId: testingStateId });
187
+ updatedCount++;
188
+ }
189
+ }
190
+ }
191
+ const task = {
192
+ id: issue.identifier,
193
+ linearId: issue.id,
194
+ title: issue.title,
195
+ status: state ? state.name : "Unknown",
196
+ localStatus: localStatus,
197
+ assignee: assigneeEmail,
198
+ priority: issue.priority,
199
+ labels: labelNames,
200
+ branch: issue.branchName,
201
+ description: issue.description ?? undefined,
202
+ parentIssueId: parent ? parent.identifier : undefined,
203
+ url: issue.url,
204
+ attachments: attachments.length > 0 ? attachments : undefined,
205
+ comments: comments.length > 0 ? comments : undefined,
206
+ };
207
+ tasks.push(task);
208
+ }
209
+ // Sort by priority using config order
210
+ tasks.sort((a, b) => {
211
+ const pa = getPrioritySortIndex(a.priority, config.priority_order);
212
+ const pb = getPrioritySortIndex(b.priority, config.priority_order);
213
+ return pa - pb;
214
+ });
215
+ let finalTasks;
216
+ if (singleIssueId && existingData) {
217
+ // Merge single issue into existing tasks
218
+ const existingTasks = existingData.tasks.filter((t) => t.id !== singleIssueId);
219
+ finalTasks = [...existingTasks, ...tasks];
220
+ // Re-sort after merge
221
+ finalTasks.sort((a, b) => {
222
+ const pa = getPrioritySortIndex(a.priority, config.priority_order);
223
+ const pb = getPrioritySortIndex(b.priority, config.priority_order);
224
+ return pa - pb;
225
+ });
226
+ }
227
+ else {
228
+ finalTasks = tasks;
229
+ }
230
+ const newData = {
231
+ cycleId: cycleId,
232
+ cycleName: cycleName,
233
+ updatedAt: new Date().toISOString(),
234
+ tasks: finalTasks,
235
+ };
236
+ await saveCycleData(newData);
237
+ if (singleIssueId) {
238
+ console.log(`\n✅ Synced issue ${singleIssueId}.`);
239
+ }
240
+ else {
241
+ console.log(`\n✅ Synced ${tasks.length} tasks for ${cycleName}.`);
242
+ }
243
+ if (updatedCount > 0) {
244
+ console.log(` Updated ${updatedCount} issues to ${statusTransitions.testing} in Linear.`);
245
+ }
246
+ }
247
+ sync().catch(console.error);
@@ -1,4 +1,4 @@
1
- import { LinearClient } from '@linear/sdk';
1
+ import { LinearClient } from "@linear/sdk";
2
2
  export declare function getPaths(): {
3
3
  baseDir: string;
4
4
  configPath: string;
@@ -27,6 +27,12 @@ export interface CycleInfo {
27
27
  start_date: string;
28
28
  end_date: string;
29
29
  }
30
+ export interface StatusTransitions {
31
+ todo: string;
32
+ in_progress: string;
33
+ done: string;
34
+ testing?: string;
35
+ }
30
36
  export interface Config {
31
37
  teams: Record<string, TeamConfig>;
32
38
  users: Record<string, UserConfig>;
@@ -39,7 +45,7 @@ export interface Config {
39
45
  name: string;
40
46
  type: string;
41
47
  }>;
42
- status_transitions?: Record<string, string>;
48
+ status_transitions?: StatusTransitions;
43
49
  priority_order?: string[];
44
50
  current_cycle?: CycleInfo;
45
51
  cycle_history?: CycleInfo[];
@@ -64,7 +70,7 @@ export interface Task {
64
70
  linearId: string;
65
71
  title: string;
66
72
  status: string;
67
- localStatus: 'pending' | 'in-progress' | 'completed' | 'blocked-backend';
73
+ localStatus: "pending" | "in-progress" | "completed" | "blocked-backend";
68
74
  assignee?: string;
69
75
  priority: number;
70
76
  labels: string[];
@@ -85,7 +91,9 @@ export interface CycleData {
85
91
  export interface LocalConfig {
86
92
  current_user: string;
87
93
  team: string;
94
+ teams?: string[];
88
95
  exclude_assignees?: string[];
96
+ exclude_labels?: string[];
89
97
  label?: string;
90
98
  }
91
99
  export declare function fileExists(filePath: string): Promise<boolean>;
@@ -1,7 +1,7 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { LinearClient } from '@linear/sdk';
4
- import { decode, encode } from '@toon-format/toon';
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { LinearClient } from "@linear/sdk";
4
+ import { decode, encode } from "@toon-format/toon";
5
5
  // Resolve base directory - supports multiple configuration methods
6
6
  function getBaseDir() {
7
7
  // 1. Check for TOON_DIR environment variable (set by CLI or user)
@@ -13,12 +13,12 @@ function getBaseDir() {
13
13
  return path.resolve(process.env.LINEAR_TOON_DIR);
14
14
  }
15
15
  // 3. Default: .ttt directory in current working directory
16
- return path.join(process.cwd(), '.ttt');
16
+ return path.join(process.cwd(), ".ttt");
17
17
  }
18
18
  const BASE_DIR = getBaseDir();
19
- const CONFIG_PATH = path.join(BASE_DIR, 'config.toon');
20
- const CYCLE_PATH = path.join(BASE_DIR, 'cycle.toon');
21
- const LOCAL_PATH = path.join(BASE_DIR, 'local.toon');
19
+ const CONFIG_PATH = path.join(BASE_DIR, "config.toon");
20
+ const CYCLE_PATH = path.join(BASE_DIR, "cycle.toon");
21
+ const LOCAL_PATH = path.join(BASE_DIR, "local.toon");
22
22
  export function getPaths() {
23
23
  return {
24
24
  baseDir: BASE_DIR,
@@ -29,16 +29,22 @@ export function getPaths() {
29
29
  }
30
30
  // Linear priority value to name mapping (fixed by Linear API)
31
31
  export const PRIORITY_NAMES = {
32
- 0: 'none',
33
- 1: 'urgent',
34
- 2: 'high',
35
- 3: 'medium',
36
- 4: 'low'
32
+ 0: "none",
33
+ 1: "urgent",
34
+ 2: "high",
35
+ 3: "medium",
36
+ 4: "low",
37
37
  };
38
- export const DEFAULT_PRIORITY_ORDER = ['urgent', 'high', 'medium', 'low', 'none'];
38
+ export const DEFAULT_PRIORITY_ORDER = [
39
+ "urgent",
40
+ "high",
41
+ "medium",
42
+ "low",
43
+ "none",
44
+ ];
39
45
  export function getPrioritySortIndex(priority, priorityOrder) {
40
46
  const order = priorityOrder ?? DEFAULT_PRIORITY_ORDER;
41
- const name = PRIORITY_NAMES[priority] ?? 'none';
47
+ const name = PRIORITY_NAMES[priority] ?? "none";
42
48
  const index = order.indexOf(name);
43
49
  return index === -1 ? order.length : index;
44
50
  }
@@ -53,23 +59,23 @@ export async function fileExists(filePath) {
53
59
  }
54
60
  export async function loadConfig() {
55
61
  try {
56
- const fileContent = await fs.readFile(CONFIG_PATH, 'utf-8');
62
+ const fileContent = await fs.readFile(CONFIG_PATH, "utf-8");
57
63
  return decode(fileContent);
58
64
  }
59
65
  catch (error) {
60
66
  console.error(`Error loading config from ${CONFIG_PATH}:`, error);
61
- console.error('Run `bun run init` to create configuration files.');
67
+ console.error("Run `bun run init` to create configuration files.");
62
68
  process.exit(1);
63
69
  }
64
70
  }
65
71
  export async function loadLocalConfig() {
66
72
  try {
67
- const fileContent = await fs.readFile(LOCAL_PATH, 'utf-8');
73
+ const fileContent = await fs.readFile(LOCAL_PATH, "utf-8");
68
74
  return decode(fileContent);
69
75
  }
70
76
  catch {
71
77
  console.error(`Error: ${LOCAL_PATH} not found.`);
72
- console.error('Run `bun run init` to create local configuration.');
78
+ console.error("Run `bun run init` to create local configuration.");
73
79
  process.exit(1);
74
80
  }
75
81
  }
@@ -79,7 +85,7 @@ export async function getUserEmail() {
79
85
  const user = config.users[localConfig.current_user];
80
86
  if (!user) {
81
87
  console.error(`Error: User "${localConfig.current_user}" not found in config.toon`);
82
- console.error(`Available users: ${Object.keys(config.users).join(', ')}`);
88
+ console.error(`Available users: ${Object.keys(config.users).join(", ")}`);
83
89
  process.exit(1);
84
90
  }
85
91
  return user.email;
@@ -87,7 +93,7 @@ export async function getUserEmail() {
87
93
  export function getLinearClient() {
88
94
  const apiKey = process.env.LINEAR_API_KEY;
89
95
  if (!apiKey) {
90
- console.error('Error: LINEAR_API_KEY environment variable is not set.');
96
+ console.error("Error: LINEAR_API_KEY environment variable is not set.");
91
97
  console.error('Set it in your shell: export LINEAR_API_KEY="lin_api_xxxxx"');
92
98
  process.exit(1);
93
99
  }
@@ -96,7 +102,7 @@ export function getLinearClient() {
96
102
  export async function loadCycleData() {
97
103
  try {
98
104
  await fs.access(CYCLE_PATH);
99
- const fileContent = await fs.readFile(CYCLE_PATH, 'utf-8');
105
+ const fileContent = await fs.readFile(CYCLE_PATH, "utf-8");
100
106
  return decode(fileContent);
101
107
  }
102
108
  catch {
@@ -105,21 +111,21 @@ export async function loadCycleData() {
105
111
  }
106
112
  export async function saveCycleData(data) {
107
113
  const toonString = encode(data);
108
- await fs.writeFile(CYCLE_PATH, toonString, 'utf-8');
114
+ await fs.writeFile(CYCLE_PATH, toonString, "utf-8");
109
115
  }
110
116
  export async function saveConfig(config) {
111
117
  const toonString = encode(config);
112
- await fs.writeFile(CONFIG_PATH, toonString, 'utf-8');
118
+ await fs.writeFile(CONFIG_PATH, toonString, "utf-8");
113
119
  }
114
120
  export async function saveLocalConfig(config) {
115
121
  const toonString = encode(config);
116
- await fs.writeFile(LOCAL_PATH, toonString, 'utf-8');
122
+ await fs.writeFile(LOCAL_PATH, toonString, "utf-8");
117
123
  }
118
124
  // Get first team key from config
119
125
  export function getDefaultTeamKey(config) {
120
126
  const keys = Object.keys(config.teams);
121
127
  if (keys.length === 0) {
122
- console.error('Error: No teams defined in config.toon');
128
+ console.error("Error: No teams defined in config.toon");
123
129
  process.exit(1);
124
130
  }
125
131
  return keys[0];
@@ -130,7 +136,7 @@ export function getTeamId(config, teamKey) {
130
136
  const team = config.teams[key];
131
137
  if (!team) {
132
138
  console.error(`Error: Team "${key}" not found in config.toon`);
133
- console.error(`Available teams: ${Object.keys(config.teams).join(', ')}`);
139
+ console.error(`Available teams: ${Object.keys(config.teams).join(", ")}`);
134
140
  process.exit(1);
135
141
  }
136
142
  return team.id;