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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/scripts/config/status.js +4 -1
- package/dist/scripts/lib/files.d.ts +4 -0
- package/dist/scripts/lib/files.js +18 -0
- package/dist/scripts/lib/init/linear-init.js +2 -1
- package/dist/scripts/lib/init/linear-prompts.js +2 -1
- package/dist/scripts/lib/init/trello-init.js +20 -12
- package/dist/scripts/lib/linear.js +2 -1
- package/dist/scripts/lib/status-helpers.d.ts +30 -0
- package/dist/scripts/lib/status-helpers.js +58 -0
- package/dist/scripts/status.js +7 -2
- package/dist/scripts/sync.js +12 -3
- package/dist/scripts/utils.d.ts +1 -1
- package/package.json +1 -1
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
|
@@ -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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/scripts/status.js
CHANGED
|
@@ -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}`);
|
package/dist/scripts/sync.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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]));
|
package/dist/scripts/utils.d.ts
CHANGED