team-toon-tack 1.0.12 → 1.6.1
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 +59 -8
- package/README.zh-TW.md +111 -22
- package/{bin → dist/bin}/cli.js +41 -19
- package/dist/scripts/config/filters.d.ts +2 -0
- package/dist/scripts/config/filters.js +71 -0
- package/dist/scripts/config/show.d.ts +2 -0
- package/dist/scripts/config/show.js +33 -0
- package/dist/scripts/config/status.d.ts +2 -0
- package/dist/scripts/config/status.js +73 -0
- package/dist/scripts/config/teams.d.ts +2 -0
- package/dist/scripts/config/teams.js +59 -0
- package/dist/scripts/config.js +47 -0
- package/dist/scripts/done-job.js +169 -0
- package/dist/scripts/init.d.ts +2 -0
- package/dist/scripts/init.js +369 -0
- package/dist/scripts/lib/config-builder.d.ts +41 -0
- package/dist/scripts/lib/config-builder.js +116 -0
- package/dist/scripts/lib/display.d.ts +12 -0
- package/dist/scripts/lib/display.js +91 -0
- package/dist/scripts/lib/git.d.ts +10 -0
- package/dist/scripts/lib/git.js +78 -0
- package/dist/scripts/lib/linear.d.ts +11 -0
- package/dist/scripts/lib/linear.js +61 -0
- package/dist/scripts/status.d.ts +2 -0
- package/dist/scripts/status.js +162 -0
- package/dist/scripts/sync.js +247 -0
- package/{scripts → dist/scripts}/utils.d.ts +11 -3
- package/{scripts → dist/scripts}/utils.js +33 -27
- package/dist/scripts/work-on.js +102 -0
- package/package.json +52 -50
- package/templates/claude-code-commands/done-job.md +45 -0
- package/templates/claude-code-commands/sync-linear.md +32 -0
- package/templates/claude-code-commands/work-on.md +41 -0
- package/bin/cli.ts +0 -125
- package/scripts/done-job.js +0 -230
- package/scripts/done-job.ts +0 -263
- package/scripts/init.js +0 -331
- package/scripts/init.ts +0 -375
- package/scripts/sync.js +0 -178
- package/scripts/sync.ts +0 -211
- package/scripts/utils.ts +0 -236
- package/scripts/work-on.js +0 -138
- package/scripts/work-on.ts +0 -161
- /package/{bin → dist/bin}/cli.d.ts +0 -0
- /package/{scripts/init.d.ts → dist/scripts/config.d.ts} +0 -0
- /package/{scripts → dist/scripts}/done-job.d.ts +0 -0
- /package/{scripts → dist/scripts}/sync.d.ts +0 -0
- /package/{scripts → dist/scripts}/work-on.d.ts +0 -0
package/scripts/sync.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, saveConfig, getTeamId, getPrioritySortIndex, CycleData, Task, Attachment, Comment, CycleInfo } from './utils.js';
|
|
2
|
-
|
|
3
|
-
async function sync() {
|
|
4
|
-
const args = process.argv.slice(2);
|
|
5
|
-
|
|
6
|
-
// Handle help flag
|
|
7
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
8
|
-
console.log(`Usage: ttt sync
|
|
9
|
-
|
|
10
|
-
Sync issues from Linear to local cycle.ttt file.
|
|
11
|
-
|
|
12
|
-
What it does:
|
|
13
|
-
- Fetches active cycle from Linear
|
|
14
|
-
- Downloads all issues matching configured label
|
|
15
|
-
- Preserves local status for existing tasks
|
|
16
|
-
- Updates config with new cycle info
|
|
17
|
-
|
|
18
|
-
Examples:
|
|
19
|
-
ttt sync # Sync in current directory
|
|
20
|
-
ttt sync -d .ttt # Sync using .ttt directory`);
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const config = await loadConfig();
|
|
25
|
-
const localConfig = await loadLocalConfig();
|
|
26
|
-
const client = getLinearClient();
|
|
27
|
-
const teamId = getTeamId(config, localConfig.team);
|
|
28
|
-
|
|
29
|
-
// Build excluded emails from local config
|
|
30
|
-
const excludedEmails = new Set(
|
|
31
|
-
(localConfig.exclude_assignees ?? [])
|
|
32
|
-
.map(key => config.users[key]?.email)
|
|
33
|
-
.filter(Boolean)
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
// Phase 1: Fetch active cycle directly from team
|
|
37
|
-
console.log('Fetching latest cycle...');
|
|
38
|
-
const team = await client.team(teamId);
|
|
39
|
-
const activeCycle = await team.activeCycle;
|
|
40
|
-
|
|
41
|
-
if (!activeCycle) {
|
|
42
|
-
console.error('No active cycle found.');
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const cycleId = activeCycle.id;
|
|
47
|
-
const cycleName = activeCycle.name ?? `Cycle #${activeCycle.number}`;
|
|
48
|
-
const newCycleInfo: CycleInfo = {
|
|
49
|
-
id: cycleId,
|
|
50
|
-
name: cycleName,
|
|
51
|
-
start_date: activeCycle.startsAt?.toISOString().split('T')[0] ?? '',
|
|
52
|
-
end_date: activeCycle.endsAt?.toISOString().split('T')[0] ?? ''
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// Check if cycle changed and update config with history
|
|
56
|
-
const existingData = await loadCycleData();
|
|
57
|
-
const oldCycleId = config.current_cycle?.id ?? existingData?.cycleId;
|
|
58
|
-
|
|
59
|
-
if (oldCycleId && oldCycleId !== cycleId) {
|
|
60
|
-
const oldCycleName = config.current_cycle?.name ?? existingData?.cycleName ?? 'Unknown';
|
|
61
|
-
console.log(`Cycle changed: ${oldCycleName} → ${cycleName}`);
|
|
62
|
-
|
|
63
|
-
// Move old cycle to history (avoid duplicates)
|
|
64
|
-
if (config.current_cycle) {
|
|
65
|
-
config.cycle_history = config.cycle_history ?? [];
|
|
66
|
-
// Remove if already exists in history
|
|
67
|
-
config.cycle_history = config.cycle_history.filter(c => c.id !== config.current_cycle!.id);
|
|
68
|
-
config.cycle_history.unshift(config.current_cycle);
|
|
69
|
-
// Keep only last 10 cycles
|
|
70
|
-
if (config.cycle_history.length > 10) {
|
|
71
|
-
config.cycle_history = config.cycle_history.slice(0, 10);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Update current cycle
|
|
76
|
-
config.current_cycle = newCycleInfo;
|
|
77
|
-
await saveConfig(config);
|
|
78
|
-
console.log('Config updated with new cycle (old cycle saved to history).');
|
|
79
|
-
} else {
|
|
80
|
-
// Update current cycle info even if ID unchanged (dates might change)
|
|
81
|
-
if (!config.current_cycle || config.current_cycle.id !== cycleId) {
|
|
82
|
-
config.current_cycle = newCycleInfo;
|
|
83
|
-
await saveConfig(config);
|
|
84
|
-
}
|
|
85
|
-
console.log(`Current cycle: ${cycleName}`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Phase 2: Fetch workflow states
|
|
89
|
-
const workflowStates = await client.workflowStates({
|
|
90
|
-
filter: { team: { id: { eq: teamId } } }
|
|
91
|
-
});
|
|
92
|
-
const stateMap = new Map(workflowStates.nodes.map(s => [s.name, s.id]));
|
|
93
|
-
const testingStateId = stateMap.get('Testing');
|
|
94
|
-
|
|
95
|
-
// Phase 3: Build existing tasks map for preserving local status
|
|
96
|
-
const existingTasksMap = new Map(existingData?.tasks.map(t => [t.id, t]));
|
|
97
|
-
|
|
98
|
-
// Phase 4: Fetch current issues with full content
|
|
99
|
-
const filterLabel = localConfig.label ?? 'Frontend';
|
|
100
|
-
console.log(`Fetching issues with label: ${filterLabel}...`);
|
|
101
|
-
const issues = await client.issues({
|
|
102
|
-
filter: {
|
|
103
|
-
team: { id: { eq: teamId } },
|
|
104
|
-
cycle: { id: { eq: cycleId } },
|
|
105
|
-
labels: { name: { eq: filterLabel } },
|
|
106
|
-
state: { name: { in: ["Todo", "In Progress"] } }
|
|
107
|
-
},
|
|
108
|
-
first: 50
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (issues.nodes.length === 0) {
|
|
112
|
-
console.log(`No ${filterLabel} issues found in current cycle with Todo/In Progress status.`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const tasks: Task[] = [];
|
|
116
|
-
let updatedCount = 0;
|
|
117
|
-
|
|
118
|
-
for (const issue of issues.nodes) {
|
|
119
|
-
const assignee = await issue.assignee;
|
|
120
|
-
const assigneeEmail = assignee?.email;
|
|
121
|
-
|
|
122
|
-
// Skip excluded assignees
|
|
123
|
-
if (assigneeEmail && excludedEmails.has(assigneeEmail)) {
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const labels = await issue.labels();
|
|
128
|
-
const state = await issue.state;
|
|
129
|
-
const parent = await issue.parent;
|
|
130
|
-
const attachmentsData = await issue.attachments();
|
|
131
|
-
const commentsData = await issue.comments();
|
|
132
|
-
|
|
133
|
-
// Build attachments list
|
|
134
|
-
const attachments: Attachment[] = attachmentsData.nodes.map(a => ({
|
|
135
|
-
id: a.id,
|
|
136
|
-
title: a.title,
|
|
137
|
-
url: a.url,
|
|
138
|
-
sourceType: a.sourceType ?? undefined
|
|
139
|
-
}));
|
|
140
|
-
|
|
141
|
-
// Build comments list
|
|
142
|
-
const comments: Comment[] = await Promise.all(
|
|
143
|
-
commentsData.nodes.map(async c => {
|
|
144
|
-
const user = await c.user;
|
|
145
|
-
return {
|
|
146
|
-
id: c.id,
|
|
147
|
-
body: c.body,
|
|
148
|
-
createdAt: c.createdAt.toISOString(),
|
|
149
|
-
user: user?.displayName ?? user?.email
|
|
150
|
-
};
|
|
151
|
-
})
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
let localStatus: Task['localStatus'] = 'pending';
|
|
155
|
-
|
|
156
|
-
// Preserve local status & sync completed tasks to Linear
|
|
157
|
-
if (existingTasksMap.has(issue.identifier)) {
|
|
158
|
-
const existing = existingTasksMap.get(issue.identifier)!;
|
|
159
|
-
localStatus = existing.localStatus;
|
|
160
|
-
|
|
161
|
-
if (localStatus === 'completed' && state && testingStateId) {
|
|
162
|
-
if (!['Testing', 'Done', 'In Review', 'Canceled'].includes(state.name)) {
|
|
163
|
-
console.log(`Updating ${issue.identifier} to Testing in Linear...`);
|
|
164
|
-
await client.updateIssue(issue.id, { stateId: testingStateId });
|
|
165
|
-
updatedCount++;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const task: Task = {
|
|
171
|
-
id: issue.identifier,
|
|
172
|
-
linearId: issue.id,
|
|
173
|
-
title: issue.title,
|
|
174
|
-
status: state ? state.name : 'Unknown',
|
|
175
|
-
localStatus: localStatus,
|
|
176
|
-
assignee: assigneeEmail,
|
|
177
|
-
priority: issue.priority,
|
|
178
|
-
labels: labels.nodes.map(l => l.name),
|
|
179
|
-
branch: issue.branchName,
|
|
180
|
-
description: issue.description,
|
|
181
|
-
parentIssueId: parent ? parent.identifier : undefined,
|
|
182
|
-
url: issue.url,
|
|
183
|
-
attachments: attachments.length > 0 ? attachments : undefined,
|
|
184
|
-
comments: comments.length > 0 ? comments : undefined
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
tasks.push(task);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Sort by priority using config order
|
|
191
|
-
tasks.sort((a, b) => {
|
|
192
|
-
const pa = getPrioritySortIndex(a.priority, config.priority_order);
|
|
193
|
-
const pb = getPrioritySortIndex(b.priority, config.priority_order);
|
|
194
|
-
return pa - pb;
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const newData: CycleData = {
|
|
198
|
-
cycleId: cycleId,
|
|
199
|
-
cycleName: cycleName,
|
|
200
|
-
updatedAt: new Date().toISOString(),
|
|
201
|
-
tasks: tasks
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
await saveCycleData(newData);
|
|
205
|
-
console.log(`\n✅ Synced ${tasks.length} tasks for ${cycleName}.`);
|
|
206
|
-
if (updatedCount > 0) {
|
|
207
|
-
console.log(` Updated ${updatedCount} issues to Testing in Linear.`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
sync().catch(console.error);
|
package/scripts/utils.ts
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
// Resolve base directory - supports multiple configuration methods
|
|
7
|
-
function getBaseDir(): string {
|
|
8
|
-
// 1. Check for TOON_DIR environment variable (set by CLI or user)
|
|
9
|
-
if (process.env.TOON_DIR) {
|
|
10
|
-
return path.resolve(process.env.TOON_DIR);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// 2. Check for legacy LINEAR_TOON_DIR environment variable
|
|
14
|
-
if (process.env.LINEAR_TOON_DIR) {
|
|
15
|
-
return path.resolve(process.env.LINEAR_TOON_DIR);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// 3. Default: .ttt directory in current working directory
|
|
19
|
-
return path.join(process.cwd(), '.ttt');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const BASE_DIR = getBaseDir();
|
|
23
|
-
const CONFIG_PATH = path.join(BASE_DIR, 'config.toon');
|
|
24
|
-
const CYCLE_PATH = path.join(BASE_DIR, 'cycle.toon');
|
|
25
|
-
const LOCAL_PATH = path.join(BASE_DIR, 'local.toon');
|
|
26
|
-
|
|
27
|
-
export function getPaths() {
|
|
28
|
-
return {
|
|
29
|
-
baseDir: BASE_DIR,
|
|
30
|
-
configPath: CONFIG_PATH,
|
|
31
|
-
cyclePath: CYCLE_PATH,
|
|
32
|
-
localPath: LOCAL_PATH,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface TeamConfig {
|
|
37
|
-
id: string;
|
|
38
|
-
name: string;
|
|
39
|
-
icon?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface UserConfig {
|
|
43
|
-
id: string;
|
|
44
|
-
email: string;
|
|
45
|
-
displayName: string;
|
|
46
|
-
role?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface LabelConfig {
|
|
50
|
-
id: string;
|
|
51
|
-
name: string;
|
|
52
|
-
color?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface CycleInfo {
|
|
56
|
-
id: string;
|
|
57
|
-
name: string;
|
|
58
|
-
start_date: string;
|
|
59
|
-
end_date: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface Config {
|
|
63
|
-
teams: Record<string, TeamConfig>;
|
|
64
|
-
users: Record<string, UserConfig>;
|
|
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;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface Attachment {
|
|
93
|
-
id: string;
|
|
94
|
-
title: string;
|
|
95
|
-
url: string;
|
|
96
|
-
sourceType?: string;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface Comment {
|
|
100
|
-
id: string;
|
|
101
|
-
body: string;
|
|
102
|
-
createdAt: string;
|
|
103
|
-
user?: string;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface Task {
|
|
107
|
-
id: string;
|
|
108
|
-
linearId: string;
|
|
109
|
-
title: string;
|
|
110
|
-
status: string;
|
|
111
|
-
localStatus: 'pending' | 'in-progress' | 'completed' | 'blocked-backend';
|
|
112
|
-
assignee?: string;
|
|
113
|
-
priority: number;
|
|
114
|
-
labels: string[];
|
|
115
|
-
branch?: string;
|
|
116
|
-
description?: string;
|
|
117
|
-
parentIssueId?: string;
|
|
118
|
-
subIssues?: Task[];
|
|
119
|
-
url?: string;
|
|
120
|
-
attachments?: Attachment[];
|
|
121
|
-
comments?: Comment[];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface CycleData {
|
|
125
|
-
cycleId: string;
|
|
126
|
-
cycleName: string;
|
|
127
|
-
updatedAt: string;
|
|
128
|
-
tasks: Task[];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export interface LocalConfig {
|
|
132
|
-
current_user: string;
|
|
133
|
-
team: string; // team key from config.teams
|
|
134
|
-
exclude_assignees?: string[];
|
|
135
|
-
label?: string;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export async function fileExists(filePath: string): Promise<boolean> {
|
|
139
|
-
try {
|
|
140
|
-
await fs.access(filePath);
|
|
141
|
-
return true;
|
|
142
|
-
} catch {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function loadConfig(): Promise<Config> {
|
|
148
|
-
try {
|
|
149
|
-
const fileContent = await fs.readFile(CONFIG_PATH, 'utf-8');
|
|
150
|
-
return decode(fileContent) as unknown as Config;
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error(`Error loading config from ${CONFIG_PATH}:`, error);
|
|
153
|
-
console.error('Run `bun run init` to create configuration files.');
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function loadLocalConfig(): Promise<LocalConfig> {
|
|
159
|
-
try {
|
|
160
|
-
const fileContent = await fs.readFile(LOCAL_PATH, 'utf-8');
|
|
161
|
-
return decode(fileContent) as unknown as LocalConfig;
|
|
162
|
-
} catch {
|
|
163
|
-
console.error(`Error: ${LOCAL_PATH} not found.`);
|
|
164
|
-
console.error('Run `bun run init` to create local configuration.');
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export async function getUserEmail(): Promise<string> {
|
|
170
|
-
const localConfig = await loadLocalConfig();
|
|
171
|
-
const config = await loadConfig();
|
|
172
|
-
const user = config.users[localConfig.current_user];
|
|
173
|
-
if (!user) {
|
|
174
|
-
console.error(`Error: User "${localConfig.current_user}" not found in config.toon`);
|
|
175
|
-
console.error(`Available users: ${Object.keys(config.users).join(', ')}`);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
return user.email;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function getLinearClient(): LinearClient {
|
|
182
|
-
const apiKey = process.env.LINEAR_API_KEY;
|
|
183
|
-
if (!apiKey) {
|
|
184
|
-
console.error('Error: LINEAR_API_KEY environment variable is not set.');
|
|
185
|
-
console.error('Set it in your shell: export LINEAR_API_KEY="lin_api_xxxxx"');
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
return new LinearClient({ apiKey });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export async function loadCycleData(): Promise<CycleData | null> {
|
|
192
|
-
try {
|
|
193
|
-
await fs.access(CYCLE_PATH);
|
|
194
|
-
const fileContent = await fs.readFile(CYCLE_PATH, 'utf-8');
|
|
195
|
-
return decode(fileContent) as unknown as CycleData;
|
|
196
|
-
} catch {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export async function saveCycleData(data: CycleData): Promise<void> {
|
|
202
|
-
const toonString = encode(data);
|
|
203
|
-
await fs.writeFile(CYCLE_PATH, toonString, 'utf-8');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export async function saveConfig(config: Config): Promise<void> {
|
|
207
|
-
const toonString = encode(config);
|
|
208
|
-
await fs.writeFile(CONFIG_PATH, toonString, 'utf-8');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export async function saveLocalConfig(config: LocalConfig): Promise<void> {
|
|
212
|
-
const toonString = encode(config);
|
|
213
|
-
await fs.writeFile(LOCAL_PATH, toonString, 'utf-8');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Get first team key from config
|
|
217
|
-
export function getDefaultTeamKey(config: Config): string {
|
|
218
|
-
const keys = Object.keys(config.teams);
|
|
219
|
-
if (keys.length === 0) {
|
|
220
|
-
console.error('Error: No teams defined in config.toon');
|
|
221
|
-
process.exit(1);
|
|
222
|
-
}
|
|
223
|
-
return keys[0];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Get team ID by key or return first team
|
|
227
|
-
export function getTeamId(config: Config, teamKey?: string): string {
|
|
228
|
-
const key = teamKey || getDefaultTeamKey(config);
|
|
229
|
-
const team = config.teams[key];
|
|
230
|
-
if (!team) {
|
|
231
|
-
console.error(`Error: Team "${key}" not found in config.toon`);
|
|
232
|
-
console.error(`Available teams: ${Object.keys(config.teams).join(', ')}`);
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
return team.id;
|
|
236
|
-
}
|
package/scripts/work-on.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import prompts from 'prompts';
|
|
2
|
-
import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, getUserEmail, getTeamId, getPrioritySortIndex } from './utils.js';
|
|
3
|
-
const PRIORITY_LABELS = {
|
|
4
|
-
0: '⚪ None',
|
|
5
|
-
1: '🔴 Urgent',
|
|
6
|
-
2: '🟠 High',
|
|
7
|
-
3: '🟡 Medium',
|
|
8
|
-
4: '🟢 Low'
|
|
9
|
-
};
|
|
10
|
-
async function workOn() {
|
|
11
|
-
const args = process.argv.slice(2);
|
|
12
|
-
// Handle help flag
|
|
13
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
14
|
-
console.log(`Usage: ttt work-on [issue-id]
|
|
15
|
-
|
|
16
|
-
Arguments:
|
|
17
|
-
issue-id Issue ID (e.g., MP-624) or 'next' for auto-select
|
|
18
|
-
If omitted, shows interactive selection
|
|
19
|
-
|
|
20
|
-
Examples:
|
|
21
|
-
ttt work-on # Interactive selection
|
|
22
|
-
ttt work-on MP-624 # Work on specific issue
|
|
23
|
-
ttt work-on next # Auto-select highest priority`);
|
|
24
|
-
process.exit(0);
|
|
25
|
-
}
|
|
26
|
-
let issueId = args[0];
|
|
27
|
-
const config = await loadConfig();
|
|
28
|
-
const data = await loadCycleData();
|
|
29
|
-
if (!data) {
|
|
30
|
-
console.error('No cycle data found. Run /sync-linear first.');
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
const userEmail = await getUserEmail();
|
|
34
|
-
const localConfig = await loadLocalConfig();
|
|
35
|
-
// Build excluded emails list from user keys
|
|
36
|
-
const excludedEmails = new Set((localConfig.exclude_assignees ?? [])
|
|
37
|
-
.map(key => config.users[key]?.email)
|
|
38
|
-
.filter(Boolean));
|
|
39
|
-
const pendingTasks = data.tasks
|
|
40
|
-
.filter(t => t.localStatus === 'pending' &&
|
|
41
|
-
!excludedEmails.has(t.assignee ?? ''))
|
|
42
|
-
.sort((a, b) => {
|
|
43
|
-
const pa = getPrioritySortIndex(a.priority, config.priority_order);
|
|
44
|
-
const pb = getPrioritySortIndex(b.priority, config.priority_order);
|
|
45
|
-
return pa - pb;
|
|
46
|
-
});
|
|
47
|
-
// Phase 0: Issue Resolution
|
|
48
|
-
if (!issueId) {
|
|
49
|
-
// Interactive selection
|
|
50
|
-
if (pendingTasks.length === 0) {
|
|
51
|
-
console.log('✅ 沒有待處理的任務,所有工作已完成或進行中');
|
|
52
|
-
process.exit(0);
|
|
53
|
-
}
|
|
54
|
-
const choices = pendingTasks.map(task => ({
|
|
55
|
-
title: `${PRIORITY_LABELS[task.priority] || '⚪'} ${task.id}: ${task.title}`,
|
|
56
|
-
value: task.id,
|
|
57
|
-
description: task.labels.join(', ')
|
|
58
|
-
}));
|
|
59
|
-
const response = await prompts({
|
|
60
|
-
type: 'select',
|
|
61
|
-
name: 'issueId',
|
|
62
|
-
message: '選擇要處理的任務:',
|
|
63
|
-
choices: choices
|
|
64
|
-
});
|
|
65
|
-
if (!response.issueId) {
|
|
66
|
-
console.log('已取消');
|
|
67
|
-
process.exit(0);
|
|
68
|
-
}
|
|
69
|
-
issueId = response.issueId;
|
|
70
|
-
}
|
|
71
|
-
else if (['next', '下一個', '下一個工作'].includes(issueId)) {
|
|
72
|
-
// Auto-select highest priority
|
|
73
|
-
if (pendingTasks.length === 0) {
|
|
74
|
-
console.log('✅ 沒有待處理的任務,所有工作已完成或進行中');
|
|
75
|
-
process.exit(0);
|
|
76
|
-
}
|
|
77
|
-
issueId = pendingTasks[0].id;
|
|
78
|
-
console.log(`Auto-selected: ${issueId}`);
|
|
79
|
-
}
|
|
80
|
-
// Phase 1: Find task
|
|
81
|
-
const task = data.tasks.find(t => t.id === issueId || t.id === `MP-${issueId}`);
|
|
82
|
-
if (!task) {
|
|
83
|
-
console.error(`Issue ${issueId} not found in current cycle.`);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
// Phase 2: Availability Check
|
|
87
|
-
if (task.localStatus === 'in-progress') {
|
|
88
|
-
console.log(`⚠️ 此任務 ${task.id} 已在進行中`);
|
|
89
|
-
}
|
|
90
|
-
else if (task.localStatus === 'completed') {
|
|
91
|
-
console.log(`⚠️ 此任務 ${task.id} 已完成`);
|
|
92
|
-
process.exit(0);
|
|
93
|
-
}
|
|
94
|
-
// Phase 3: Mark as In Progress
|
|
95
|
-
if (task.localStatus === 'pending') {
|
|
96
|
-
task.localStatus = 'in-progress';
|
|
97
|
-
await saveCycleData(data);
|
|
98
|
-
console.log(`Local: ${task.id} → in-progress`);
|
|
99
|
-
// Update Linear using stored linearId
|
|
100
|
-
if (task.linearId && process.env.LINEAR_API_KEY) {
|
|
101
|
-
try {
|
|
102
|
-
const client = getLinearClient();
|
|
103
|
-
const workflowStates = await client.workflowStates({
|
|
104
|
-
filter: { team: { id: { eq: getTeamId(config, localConfig.team) } } }
|
|
105
|
-
});
|
|
106
|
-
const inProgressState = workflowStates.nodes.find(s => s.name === 'In Progress');
|
|
107
|
-
if (inProgressState) {
|
|
108
|
-
await client.updateIssue(task.linearId, { stateId: inProgressState.id });
|
|
109
|
-
console.log(`Linear: ${task.id} → In Progress`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
catch (e) {
|
|
113
|
-
console.error('Failed to update Linear:', e);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// Phase 4: Display task info
|
|
118
|
-
console.log(`\n${'═'.repeat(50)}`);
|
|
119
|
-
console.log(`👷 ${task.id}: ${task.title}`);
|
|
120
|
-
console.log(`${'═'.repeat(50)}`);
|
|
121
|
-
console.log(`Priority: ${PRIORITY_LABELS[task.priority] || 'None'}`);
|
|
122
|
-
console.log(`Labels: ${task.labels.join(', ')}`);
|
|
123
|
-
console.log(`Branch: ${task.branch || 'N/A'}`);
|
|
124
|
-
if (task.url)
|
|
125
|
-
console.log(`URL: ${task.url}`);
|
|
126
|
-
if (task.description) {
|
|
127
|
-
console.log(`\n📝 Description:\n${task.description}`);
|
|
128
|
-
}
|
|
129
|
-
if (task.attachments && task.attachments.length > 0) {
|
|
130
|
-
console.log(`\n📎 Attachments:`);
|
|
131
|
-
for (const att of task.attachments) {
|
|
132
|
-
console.log(` - ${att.title}: ${att.url}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
console.log(`\n${'─'.repeat(50)}`);
|
|
136
|
-
console.log('Next: bun type-check && bun lint');
|
|
137
|
-
}
|
|
138
|
-
workOn().catch(console.error);
|