team-toon-tack 3.1.0 → 3.2.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/README.md CHANGED
@@ -48,7 +48,7 @@ During init, you'll be prompted to select your task source (Linear or Trello) an
48
48
  **For Trello:**
49
49
  - **Board**: The Trello board to sync
50
50
  - **User**: Your Trello username
51
- - **Status mappings**: Map Trello lists to Todo/In Progress/Done
51
+ - **Status mappings**: Map Trello lists to Todo (supports multiple lists)/In Progress/Done
52
52
  - **Label filter**: Optional label to filter cards
53
53
 
54
54
  ### Completion Modes (Linear only)
package/README.zh-TW.md CHANGED
@@ -48,7 +48,7 @@ ttt init
48
48
  **Trello:**
49
49
  - **看板**:要同步的 Trello 看板
50
50
  - **使用者**:你的 Trello 使用者名稱
51
- - **狀態映射**:將 Trello 列表映射到 Todo/In Progress/Done
51
+ - **狀態映射**:將 Trello 列表映射到 Todo(支援多個列表)/In Progress/Done
52
52
  - **標籤過濾**:可選的卡片過濾標籤
53
53
 
54
54
  ### 完成模式(僅 Linear)
@@ -1,5 +1,6 @@
1
1
  import { select } from "@inquirer/prompts";
2
2
  import { getWorkflowStates } from "../lib/linear.js";
3
+ import { getFirstTodoStatus } from "../lib/status-helpers.js";
3
4
  import { saveConfig, } from "../utils.js";
4
5
  export async function configureStatus(config, localConfig) {
5
6
  console.log("📊 Configure Status Mappings\n");
@@ -14,7 +15,9 @@ export async function configureStatus(config, localConfig) {
14
15
  }));
15
16
  // Get current values or defaults
16
17
  const current = config.status_transitions || {};
17
- const defaultTodo = current.todo || states.find((s) => s.type === "unstarted")?.name || "Todo";
18
+ const defaultTodo = getFirstTodoStatus(current.todo) ||
19
+ states.find((s) => s.type === "unstarted")?.name ||
20
+ "Todo";
18
21
  const defaultInProgress = current.in_progress ||
19
22
  states.find((s) => s.type === "started")?.name ||
20
23
  "In Progress";
@@ -7,3 +7,7 @@ export declare function downloadLinearFile(url: string, issueId: string, attachm
7
7
  export declare const downloadLinearImage: typeof downloadLinearFile;
8
8
  export declare function clearIssueImages(outputDir: string, issueId: string): Promise<void>;
9
9
  export declare function ensureOutputDir(outputDir: string): Promise<void>;
10
+ /**
11
+ * Clear all files in output directory (for fresh sync)
12
+ */
13
+ export declare function clearAllOutput(outputDir: string): Promise<void>;
@@ -134,3 +134,21 @@ export async function clearIssueImages(outputDir, issueId) {
134
134
  export async function ensureOutputDir(outputDir) {
135
135
  await fs.mkdir(outputDir, { recursive: true });
136
136
  }
137
+ /**
138
+ * Clear all files in output directory (for fresh sync)
139
+ */
140
+ export async function clearAllOutput(outputDir) {
141
+ try {
142
+ const files = await fs.readdir(outputDir);
143
+ for (const file of files) {
144
+ const filepath = path.join(outputDir, file);
145
+ const stat = await fs.stat(filepath);
146
+ if (stat.isFile()) {
147
+ await fs.unlink(filepath);
148
+ }
149
+ }
150
+ }
151
+ catch {
152
+ // Directory doesn't exist or other error, ignore
153
+ }
154
+ }
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
5
5
  import { decode, encode } from "@toon-format/toon";
6
6
  import { fileExists, getLinearClient, } from "../../utils.js";
7
7
  import { buildConfig, buildLocalConfig, buildTeamsConfig, findTeamKey, findUserKey, } from "../config-builder.js";
8
+ import { formatTodoStatus } from "../status-helpers.js";
8
9
  import { showPluginInstallInstructions, updateGitignore } from "./file-ops.js";
9
10
  import { promptForApiKey, selectCompletionMode, selectDevTeam, selectDevTestingStatus, selectQaPmTeams, selectStatusMappings, } from "./linear-prompts.js";
10
11
  import { selectLabelFilter, selectStatusSource, selectUser, } from "./prompts.js";
@@ -195,7 +196,7 @@ export async function initLinear(options, paths) {
195
196
  console.log(` Cycle: ${currentCycle.name || `Cycle #${currentCycle.number}`}`);
196
197
  }
197
198
  console.log(` Status mappings:`);
198
- console.log(` Todo: ${statusTransitions.todo}`);
199
+ console.log(` Todo: ${formatTodoStatus(statusTransitions.todo)}`);
199
200
  console.log(` In Progress: ${statusTransitions.in_progress}`);
200
201
  console.log(` Done: ${statusTransitions.done}`);
201
202
  if (statusTransitions.blocked) {
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { checkbox, password, select } from "@inquirer/prompts";
5
5
  import { getDefaultStatusTransitions, } from "../config-builder.js";
6
+ import { getFirstTodoStatus } from "../status-helpers.js";
6
7
  export async function promptForApiKey(options) {
7
8
  let apiKey = options.apiKey || process.env.LINEAR_API_KEY;
8
9
  if (!apiKey && options.interactive) {
@@ -165,7 +166,7 @@ export async function selectStatusMappings(devStates, options) {
165
166
  const todo = await select({
166
167
  message: 'Select status for "Todo" (pending tasks):',
167
168
  choices: devStateChoices,
168
- default: devDefaults.todo,
169
+ default: getFirstTodoStatus(devDefaults.todo),
169
170
  });
170
171
  const in_progress = await select({
171
172
  message: 'Select status for "In Progress" (working tasks):',
@@ -2,8 +2,9 @@
2
2
  * Trello initialization flow
3
3
  */
4
4
  import fs from "node:fs/promises";
5
- import { select } from "@inquirer/prompts";
5
+ import { checkbox, select } from "@inquirer/prompts";
6
6
  import { encode } from "@toon-format/toon";
7
+ import { formatTodoStatus } from "../status-helpers.js";
7
8
  import { TrelloClient } from "../trello.js";
8
9
  import { updateGitignore } from "./file-ops.js";
9
10
  import { selectLabelFilter, selectStatusSource } from "./prompts.js";
@@ -71,16 +72,23 @@ export async function initTrello(options, paths) {
71
72
  let statusTransitions;
72
73
  if (options.interactive && lists.length > 0) {
73
74
  console.log("\n📊 Configure status mappings (lists):");
74
- const defaultTodo = lists.find((l) => /todo|backlog|to do/i.test(l.name))?.name ||
75
- lists[0]?.name;
76
- const defaultInProgress = lists.find((l) => /progress|doing|working/i.test(l.name))?.name ||
77
- lists[1]?.name;
78
- const defaultDone = lists.find((l) => /done|complete|finished/i.test(l.name))?.name ||
75
+ // Smart defaults for checkbox - pre-check matching lists
76
+ const todoPattern = /todo|backlog|to do|proposed/i;
77
+ const inProgressPattern = /progress|doing|working|active/i;
78
+ const donePattern = /done|complete|finished|closed/i;
79
+ const defaultInProgress = lists.find((l) => inProgressPattern.test(l.name))?.name || lists[1]?.name;
80
+ const defaultDone = lists.find((l) => donePattern.test(l.name))?.name ||
79
81
  lists[lists.length - 1]?.name;
80
- const todo = await select({
81
- message: 'Select list for "Todo" (pending tasks):',
82
- choices: listChoices,
83
- default: defaultTodo,
82
+ // Todo uses checkbox for multi-select
83
+ const todoChoices = lists.map((l) => ({
84
+ name: l.name,
85
+ value: l.name,
86
+ checked: todoPattern.test(l.name),
87
+ }));
88
+ const todo = await checkbox({
89
+ message: 'Select list(s) for "Todo" (pending tasks - multi-select):',
90
+ choices: todoChoices,
91
+ required: true,
84
92
  });
85
93
  const in_progress = await select({
86
94
  message: 'Select list for "In Progress":',
@@ -93,7 +101,7 @@ export async function initTrello(options, paths) {
93
101
  default: defaultDone,
94
102
  });
95
103
  statusTransitions = {
96
- todo: todo || lists[0]?.name || "Todo",
104
+ todo: todo.length === 1 ? todo[0] : todo,
97
105
  in_progress: in_progress || lists[1]?.name || "In Progress",
98
106
  done: done || lists[lists.length - 1]?.name || "Done",
99
107
  };
@@ -192,7 +200,7 @@ export async function initTrello(options, paths) {
192
200
  console.log(` Label filters: ${defaultLabel && defaultLabel.length > 0 ? defaultLabel.join(", ") : "(none)"}`);
193
201
  console.log(` Status source: ${statusSource === "local" ? "local" : "remote"}`);
194
202
  console.log(` Status mappings:`);
195
- console.log(` Todo: ${statusTransitions.todo}`);
203
+ console.log(` Todo: ${formatTodoStatus(statusTransitions.todo)}`);
196
204
  console.log(` In Progress: ${statusTransitions.in_progress}`);
197
205
  console.log(` Done: ${statusTransitions.done}`);
198
206
  console.log("\nNext steps:");
@@ -1,4 +1,5 @@
1
1
  import { getLinearClient, getTeamId, } from "../utils.js";
2
+ import { getFirstTodoStatus } from "./status-helpers.js";
2
3
  export async function getWorkflowStates(config, teamKey) {
3
4
  const client = getLinearClient();
4
5
  const teamId = getTeamId(config, teamKey);
@@ -50,7 +51,7 @@ export function mapLocalStatusToLinear(localStatus, config) {
50
51
  const transitions = getStatusTransitions(config);
51
52
  switch (localStatus) {
52
53
  case "pending":
53
- return transitions.todo;
54
+ return getFirstTodoStatus(transitions.todo);
54
55
  case "in-progress":
55
56
  return transitions.in_progress;
56
57
  case "in-review":
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Helper functions for status transitions
3
+ * Handles the case where `todo` can be string | string[]
4
+ */
5
+ import type { StatusTransitions } from "../utils.js";
6
+ /**
7
+ * Normalize todo status value to array
8
+ * Only `todo` supports multiple values for sync filtering
9
+ */
10
+ export declare function normalizeTodoStatuses(todo: string | string[] | undefined): string[];
11
+ /**
12
+ * Check if a status name matches the todo transition (supports string | string[])
13
+ */
14
+ export declare function isTodoStatus(statusName: string, transitions: StatusTransitions): boolean;
15
+ /**
16
+ * Get all statuses to sync (flattens todo array + in_progress)
17
+ */
18
+ export declare function getSyncStatuses(transitions: StatusTransitions): string[];
19
+ /**
20
+ * Map remote status to local status using transitions
21
+ */
22
+ export declare function mapRemoteToLocalStatus(remoteStatus: string, transitions: StatusTransitions): "pending" | "in-progress" | "completed" | "in-review" | "blocked";
23
+ /**
24
+ * Format todo status for display (handles string | string[])
25
+ */
26
+ export declare function formatTodoStatus(todo: string | string[]): string;
27
+ /**
28
+ * Get first todo status (for defaults and pushing to remote)
29
+ */
30
+ export declare function getFirstTodoStatus(todo: string | string[] | undefined): string | undefined;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Helper functions for status transitions
3
+ * Handles the case where `todo` can be string | string[]
4
+ */
5
+ /**
6
+ * Normalize todo status value to array
7
+ * Only `todo` supports multiple values for sync filtering
8
+ */
9
+ export function normalizeTodoStatuses(todo) {
10
+ if (!todo)
11
+ return [];
12
+ return Array.isArray(todo) ? todo : [todo];
13
+ }
14
+ /**
15
+ * Check if a status name matches the todo transition (supports string | string[])
16
+ */
17
+ export function isTodoStatus(statusName, transitions) {
18
+ const todoStatuses = normalizeTodoStatuses(transitions.todo);
19
+ return todoStatuses.includes(statusName);
20
+ }
21
+ /**
22
+ * Get all statuses to sync (flattens todo array + in_progress)
23
+ */
24
+ export function getSyncStatuses(transitions) {
25
+ return [...normalizeTodoStatuses(transitions.todo), transitions.in_progress];
26
+ }
27
+ /**
28
+ * Map remote status to local status using transitions
29
+ */
30
+ export function mapRemoteToLocalStatus(remoteStatus, transitions) {
31
+ if (remoteStatus === transitions.done) {
32
+ return "completed";
33
+ }
34
+ if (remoteStatus === transitions.in_progress) {
35
+ return "in-progress";
36
+ }
37
+ if (transitions.testing && remoteStatus === transitions.testing) {
38
+ return "in-review";
39
+ }
40
+ if (transitions.blocked && remoteStatus === transitions.blocked) {
41
+ return "blocked";
42
+ }
43
+ return "pending";
44
+ }
45
+ /**
46
+ * Format todo status for display (handles string | string[])
47
+ */
48
+ export function formatTodoStatus(todo) {
49
+ return Array.isArray(todo) ? todo.join(", ") : todo;
50
+ }
51
+ /**
52
+ * Get first todo status (for defaults and pushing to remote)
53
+ */
54
+ export function getFirstTodoStatus(todo) {
55
+ if (!todo)
56
+ return undefined;
57
+ return Array.isArray(todo) ? todo[0] : todo;
58
+ }
@@ -2,6 +2,7 @@
2
2
  import { createAdapter } from "./lib/adapters/index.js";
3
3
  import { displayTaskWithStatus, getStatusIcon } from "./lib/display.js";
4
4
  import { getStatusTransitions, mapLocalStatusToLinear } from "./lib/linear.js";
5
+ import { getFirstTodoStatus } from "./lib/status-helpers.js";
5
6
  import { getSourceType, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
6
7
  const LOCAL_STATUS_ORDER = [
7
8
  "pending",
@@ -111,17 +112,21 @@ Examples:
111
112
  }
112
113
  else if (["todo", "in_progress", "done", "testing"].includes(setStatus)) {
113
114
  const transitions = getStatusTransitions(config);
114
- newLinearStatus =
115
- transitions[setStatus] ?? undefined;
116
115
  if (setStatus === "todo") {
116
+ newLinearStatus = getFirstTodoStatus(transitions.todo);
117
117
  newLocalStatus = "pending";
118
118
  }
119
119
  else if (setStatus === "in_progress") {
120
+ newLinearStatus = transitions.in_progress;
120
121
  newLocalStatus = "in-progress";
121
122
  }
122
123
  else if (setStatus === "done") {
124
+ newLinearStatus = transitions.done;
123
125
  newLocalStatus = "completed";
124
126
  }
127
+ else if (setStatus === "testing") {
128
+ newLinearStatus = transitions.testing;
129
+ }
125
130
  }
126
131
  else {
127
132
  console.error(`Unknown status: ${setStatus}`);
@@ -1,5 +1,6 @@
1
1
  import { createAdapter } from "./lib/adapters/index.js";
2
- import { clearIssueImages, downloadLinearImage, ensureOutputDir, extractLinearImageUrls, isLinearImageUrl, } from "./lib/files.js";
2
+ import { clearAllOutput, clearIssueImages, downloadLinearImage, ensureOutputDir, extractLinearImageUrls, isLinearImageUrl, } from "./lib/files.js";
3
+ import { getSyncStatuses } from "./lib/status-helpers.js";
3
4
  import { getLinearClient, getPaths, getPrioritySortIndex, getSourceType, getTeamId, loadConfig, loadCycleData, loadLocalConfig, saveConfig, saveCycleData, } from "./utils.js";
4
5
  async function sync() {
5
6
  const args = process.argv.slice(2);
@@ -55,6 +56,10 @@ Examples:
55
56
  const teamId = getTeamId(config, localConfig.team);
56
57
  // Ensure output directory exists
57
58
  await ensureOutputDir(outputPath);
59
+ // Clear previous output for full sync (not single-issue)
60
+ if (!singleIssueId) {
61
+ await clearAllOutput(outputPath);
62
+ }
58
63
  // Build excluded labels set
59
64
  const excludedLabels = new Set(localConfig.exclude_labels ?? []);
60
65
  // Phase 1: Fetch active cycle directly from team
@@ -166,7 +171,7 @@ Examples:
166
171
  const existingTasksMap = new Map(existingData?.tasks.map((t) => [t.id, t]));
167
172
  // Phase 4: Fetch current issues with full content
168
173
  const filterLabels = localConfig.labels;
169
- const syncStatuses = [statusTransitions.todo, statusTransitions.in_progress];
174
+ const syncStatuses = getSyncStatuses(statusTransitions);
170
175
  const labelDesc = filterLabels && filterLabels.length > 0
171
176
  ? ` with labels: ${filterLabels.join(", ")}`
172
177
  : "";
@@ -391,6 +396,10 @@ async function syncTrello(config, localConfig, options) {
391
396
  }
392
397
  // Ensure output directory exists
393
398
  await ensureOutputDir(outputPath);
399
+ // Clear previous output for full sync (not single-issue)
400
+ if (!singleIssueId) {
401
+ await clearAllOutput(outputPath);
402
+ }
394
403
  // Build excluded labels set
395
404
  const excludedLabels = new Set(localConfig.exclude_labels ?? []);
396
405
  // Get team (board) ID
@@ -405,7 +414,7 @@ async function syncTrello(config, localConfig, options) {
405
414
  in_progress: "In Progress",
406
415
  done: "Done",
407
416
  };
408
- const syncStatuses = [statusTransitions.todo, statusTransitions.in_progress];
417
+ const syncStatuses = getSyncStatuses(statusTransitions);
409
418
  // Load existing data
410
419
  const existingData = await loadCycleData();
411
420
  const existingTasksMap = new Map(existingData?.tasks.map((t) => [t.id, t]));
@@ -29,7 +29,7 @@ export interface CycleInfo {
29
29
  end_date: string;
30
30
  }
31
31
  export interface StatusTransitions {
32
- todo: string;
32
+ todo: string | string[];
33
33
  in_progress: string;
34
34
  done: string;
35
35
  testing?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Linear & Trello task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {