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/done-job.ts
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import prompts from 'prompts';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, getTeamId, getDefaultTeamKey } from './utils.js';
|
|
4
|
-
|
|
5
|
-
interface CommitInfo {
|
|
6
|
-
shortHash: string;
|
|
7
|
-
fullHash: string;
|
|
8
|
-
message: string;
|
|
9
|
-
diffStat: string;
|
|
10
|
-
commitUrl: string | null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function getLatestCommit(): Promise<CommitInfo | null> {
|
|
14
|
-
try {
|
|
15
|
-
const shortHash = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
|
|
16
|
-
const fullHash = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
|
17
|
-
const message = execSync('git log -1 --format=%s', { encoding: 'utf-8' }).trim();
|
|
18
|
-
const diffStat = execSync('git diff HEAD~1 --stat --stat-width=60', { encoding: 'utf-8' }).trim();
|
|
19
|
-
|
|
20
|
-
// Get remote URL and construct commit link
|
|
21
|
-
let commitUrl: string | null = null;
|
|
22
|
-
try {
|
|
23
|
-
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
24
|
-
// Handle SSH or HTTPS URLs
|
|
25
|
-
// git@gitlab.com:org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
|
|
26
|
-
// https://gitlab.com/org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
|
|
27
|
-
if (remoteUrl.includes('gitlab')) {
|
|
28
|
-
const match = remoteUrl.match(/(?:git@|https:\/\/)([^:\/]+)[:\\/](.+?)(?:\.git)?$/);
|
|
29
|
-
if (match) {
|
|
30
|
-
commitUrl = `https://${match[1]}/${match[2]}/-/commit/${fullHash}`;
|
|
31
|
-
}
|
|
32
|
-
} else if (remoteUrl.includes('github')) {
|
|
33
|
-
const match = remoteUrl.match(/(?:git@|https:\/\/)([^:\/]+)[:\\/](.+?)(?:\.git)?$/);
|
|
34
|
-
if (match) {
|
|
35
|
-
commitUrl = `https://${match[1]}/${match[2]}/commit/${fullHash}`;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
// Ignore if can't get remote URL
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return { shortHash, fullHash, message, diffStat, commitUrl };
|
|
43
|
-
} catch {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function parseArgs(args: string[]): { issueId?: string; message?: string } {
|
|
49
|
-
let issueId: string | undefined;
|
|
50
|
-
let message: string | undefined;
|
|
51
|
-
|
|
52
|
-
for (let i = 0; i < args.length; i++) {
|
|
53
|
-
const arg = args[i];
|
|
54
|
-
if (arg === '-m' || arg === '--message') {
|
|
55
|
-
message = args[++i];
|
|
56
|
-
} else if (!arg.startsWith('-')) {
|
|
57
|
-
issueId = arg;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return { issueId, message };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function doneJob() {
|
|
65
|
-
const args = process.argv.slice(2);
|
|
66
|
-
|
|
67
|
-
// Handle help flag
|
|
68
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
69
|
-
console.log(`Usage: ttt done [issue-id] [-m message]
|
|
70
|
-
|
|
71
|
-
Arguments:
|
|
72
|
-
issue-id Issue ID (e.g., MP-624). Optional if only one task is in-progress
|
|
73
|
-
|
|
74
|
-
Options:
|
|
75
|
-
-m, --message AI summary message describing the fix
|
|
76
|
-
|
|
77
|
-
Examples:
|
|
78
|
-
ttt done # Complete current in-progress task
|
|
79
|
-
ttt done MP-624 # Complete specific task
|
|
80
|
-
ttt done -m "Fixed null check" # With completion message
|
|
81
|
-
ttt done MP-624 -m "Refactored" # Specific task with message`);
|
|
82
|
-
process.exit(0);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const { issueId: argIssueId, message: argMessage } = parseArgs(args);
|
|
86
|
-
let issueId = argIssueId;
|
|
87
|
-
|
|
88
|
-
const config = await loadConfig();
|
|
89
|
-
const localConfig = await loadLocalConfig();
|
|
90
|
-
const data = await loadCycleData();
|
|
91
|
-
|
|
92
|
-
if (!data) {
|
|
93
|
-
console.error('No cycle data found. Run /sync-linear first.');
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Find in-progress tasks
|
|
98
|
-
const inProgressTasks = data.tasks.filter(t => t.localStatus === 'in-progress');
|
|
99
|
-
|
|
100
|
-
if (inProgressTasks.length === 0) {
|
|
101
|
-
console.log('沒有進行中的任務');
|
|
102
|
-
process.exit(0);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Phase 0: Issue Resolution
|
|
106
|
-
if (!issueId) {
|
|
107
|
-
if (inProgressTasks.length === 1) {
|
|
108
|
-
issueId = inProgressTasks[0].id;
|
|
109
|
-
console.log(`Auto-selected: ${issueId}`);
|
|
110
|
-
} else if (process.stdin.isTTY) {
|
|
111
|
-
const choices = inProgressTasks.map(task => ({
|
|
112
|
-
title: `${task.id}: ${task.title}`,
|
|
113
|
-
value: task.id,
|
|
114
|
-
description: task.labels.join(', ')
|
|
115
|
-
}));
|
|
116
|
-
|
|
117
|
-
const response = await prompts({
|
|
118
|
-
type: 'select',
|
|
119
|
-
name: 'issueId',
|
|
120
|
-
message: '選擇要完成的任務:',
|
|
121
|
-
choices: choices
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
if (!response.issueId) {
|
|
125
|
-
console.log('已取消');
|
|
126
|
-
process.exit(0);
|
|
127
|
-
}
|
|
128
|
-
issueId = response.issueId;
|
|
129
|
-
} else {
|
|
130
|
-
console.error('多個進行中任務,請指定 issue ID:');
|
|
131
|
-
inProgressTasks.forEach(t => console.log(` - ${t.id}: ${t.title}`));
|
|
132
|
-
process.exit(1);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Phase 1: Find task
|
|
137
|
-
const task = data.tasks.find(t => t.id === issueId || t.id === `MP-${issueId}`);
|
|
138
|
-
if (!task) {
|
|
139
|
-
console.error(`Issue ${issueId} not found in current cycle.`);
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (task.localStatus !== 'in-progress') {
|
|
144
|
-
console.log(`⚠️ 任務 ${task.id} 不在進行中狀態 (目前: ${task.localStatus})`);
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Get latest commit for comment
|
|
149
|
-
const commit = await getLatestCommit();
|
|
150
|
-
|
|
151
|
-
// Phase 2: Get AI summary message
|
|
152
|
-
let aiMessage = argMessage || '';
|
|
153
|
-
if (!aiMessage && process.stdin.isTTY) {
|
|
154
|
-
const aiMsgResponse = await prompts({
|
|
155
|
-
type: 'text',
|
|
156
|
-
name: 'aiMessage',
|
|
157
|
-
message: 'AI 修復說明 (如何解決此問題):',
|
|
158
|
-
});
|
|
159
|
-
aiMessage = aiMsgResponse.aiMessage || '';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Phase 3: Update Linear
|
|
163
|
-
if (task.linearId && process.env.LINEAR_API_KEY) {
|
|
164
|
-
try {
|
|
165
|
-
const client = getLinearClient();
|
|
166
|
-
const workflowStates = await client.workflowStates({
|
|
167
|
-
filter: { team: { id: { eq: getTeamId(config, localConfig.team) } } }
|
|
168
|
-
});
|
|
169
|
-
const doneState = workflowStates.nodes.find(s => s.name === 'Done');
|
|
170
|
-
|
|
171
|
-
// Update issue to Done
|
|
172
|
-
if (doneState) {
|
|
173
|
-
await client.updateIssue(task.linearId, { stateId: doneState.id });
|
|
174
|
-
console.log(`Linear: ${task.id} → Done`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Add comment with commit info and AI summary
|
|
178
|
-
if (commit) {
|
|
179
|
-
const commitLink = commit.commitUrl
|
|
180
|
-
? `[${commit.shortHash}](${commit.commitUrl})`
|
|
181
|
-
: `\`${commit.shortHash}\``;
|
|
182
|
-
|
|
183
|
-
const commentParts = [
|
|
184
|
-
'## ✅ 開發完成',
|
|
185
|
-
'',
|
|
186
|
-
'### 🤖 AI 修復說明',
|
|
187
|
-
aiMessage || '_No description provided_',
|
|
188
|
-
'',
|
|
189
|
-
'### 📝 Commit Info',
|
|
190
|
-
`**Commit:** ${commitLink}`,
|
|
191
|
-
`**Message:** ${commit.message}`,
|
|
192
|
-
'',
|
|
193
|
-
'### 📊 Changes',
|
|
194
|
-
'```',
|
|
195
|
-
commit.diffStat,
|
|
196
|
-
'```',
|
|
197
|
-
];
|
|
198
|
-
|
|
199
|
-
await client.createComment({
|
|
200
|
-
issueId: task.linearId,
|
|
201
|
-
body: commentParts.join('\n')
|
|
202
|
-
});
|
|
203
|
-
console.log(`Linear: 已新增 commit 留言`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Update parent to Testing if exists
|
|
207
|
-
if (task.parentIssueId) {
|
|
208
|
-
try {
|
|
209
|
-
// Find parent issue by identifier
|
|
210
|
-
const searchResult = await client.searchIssues(task.parentIssueId);
|
|
211
|
-
const parentIssue = searchResult.nodes.find(
|
|
212
|
-
issue => issue.identifier === task.parentIssueId
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
if (parentIssue) {
|
|
216
|
-
// Get parent's team workflow states
|
|
217
|
-
const parentTeam = await parentIssue.team;
|
|
218
|
-
if (parentTeam) {
|
|
219
|
-
const parentWorkflowStates = await client.workflowStates({
|
|
220
|
-
filter: { team: { id: { eq: parentTeam.id } } }
|
|
221
|
-
});
|
|
222
|
-
const testingState = parentWorkflowStates.nodes.find(s => s.name === 'Testing');
|
|
223
|
-
|
|
224
|
-
if (testingState) {
|
|
225
|
-
await client.updateIssue(parentIssue.id, { stateId: testingState.id });
|
|
226
|
-
console.log(`Linear: Parent ${task.parentIssueId} → Testing`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
} catch (parentError) {
|
|
231
|
-
console.error('Failed to update parent issue:', parentError);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
} catch (e) {
|
|
235
|
-
console.error('Failed to update Linear:', e);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Phase 4: Update local status
|
|
240
|
-
task.localStatus = 'completed';
|
|
241
|
-
await saveCycleData(data);
|
|
242
|
-
console.log(`Local: ${task.id} → completed`);
|
|
243
|
-
|
|
244
|
-
// Phase 5: Summary
|
|
245
|
-
console.log(`\n${'═'.repeat(50)}`);
|
|
246
|
-
console.log(`✅ ${task.id}: ${task.title}`);
|
|
247
|
-
console.log(`${'═'.repeat(50)}`);
|
|
248
|
-
if (commit) {
|
|
249
|
-
console.log(`Commit: ${commit.shortHash} - ${commit.message}`);
|
|
250
|
-
if (commit.commitUrl) {
|
|
251
|
-
console.log(`URL: ${commit.commitUrl}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (aiMessage) {
|
|
255
|
-
console.log(`AI: ${aiMessage}`);
|
|
256
|
-
}
|
|
257
|
-
if (task.parentIssueId) {
|
|
258
|
-
console.log(`Parent: ${task.parentIssueId} → Testing`);
|
|
259
|
-
}
|
|
260
|
-
console.log(`\n🎉 任務完成!`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
doneJob().catch(console.error);
|
package/scripts/init.js
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import prompts from 'prompts';
|
|
4
|
-
import { encode, decode } from '@toon-format/toon';
|
|
5
|
-
import { getLinearClient, getPaths, fileExists } from './utils.js';
|
|
6
|
-
function parseArgs(args) {
|
|
7
|
-
const options = { interactive: true };
|
|
8
|
-
for (let i = 0; i < args.length; i++) {
|
|
9
|
-
const arg = args[i];
|
|
10
|
-
switch (arg) {
|
|
11
|
-
case '--api-key':
|
|
12
|
-
case '-k':
|
|
13
|
-
options.apiKey = args[++i];
|
|
14
|
-
break;
|
|
15
|
-
case '--user':
|
|
16
|
-
case '-u':
|
|
17
|
-
options.user = args[++i];
|
|
18
|
-
break;
|
|
19
|
-
case '--team':
|
|
20
|
-
case '-t':
|
|
21
|
-
options.team = args[++i];
|
|
22
|
-
break;
|
|
23
|
-
case '--label':
|
|
24
|
-
case '-l':
|
|
25
|
-
options.label = args[++i];
|
|
26
|
-
break;
|
|
27
|
-
case '--force':
|
|
28
|
-
case '-f':
|
|
29
|
-
options.force = true;
|
|
30
|
-
break;
|
|
31
|
-
case '--yes':
|
|
32
|
-
case '-y':
|
|
33
|
-
options.interactive = false;
|
|
34
|
-
break;
|
|
35
|
-
case '--help':
|
|
36
|
-
case '-h':
|
|
37
|
-
printHelp();
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return options;
|
|
42
|
-
}
|
|
43
|
-
function printHelp() {
|
|
44
|
-
console.log(`
|
|
45
|
-
linear-toon init - Initialize configuration files
|
|
46
|
-
|
|
47
|
-
USAGE:
|
|
48
|
-
bun run init [OPTIONS]
|
|
49
|
-
|
|
50
|
-
OPTIONS:
|
|
51
|
-
-k, --api-key <key> Linear API key (or set LINEAR_API_KEY env)
|
|
52
|
-
-u, --user <email> Your email address in Linear
|
|
53
|
-
-t, --team <name> Team name to sync (optional, fetches from Linear)
|
|
54
|
-
-l, --label <name> Default label filter (e.g., Frontend, Backend)
|
|
55
|
-
-f, --force Overwrite existing config files
|
|
56
|
-
-y, --yes Non-interactive mode (use defaults/provided args)
|
|
57
|
-
-h, --help Show this help message
|
|
58
|
-
|
|
59
|
-
EXAMPLES:
|
|
60
|
-
bun run init
|
|
61
|
-
bun run init --user alice@example.com --label Frontend
|
|
62
|
-
bun run init -k lin_api_xxx -y
|
|
63
|
-
`);
|
|
64
|
-
}
|
|
65
|
-
async function init() {
|
|
66
|
-
const args = process.argv.slice(2);
|
|
67
|
-
const options = parseArgs(args);
|
|
68
|
-
const paths = getPaths();
|
|
69
|
-
console.log('🚀 Linear-TOON Initialization\n');
|
|
70
|
-
// Check existing files
|
|
71
|
-
const configExists = await fileExists(paths.configPath);
|
|
72
|
-
const localExists = await fileExists(paths.localPath);
|
|
73
|
-
if ((configExists || localExists) && !options.force) {
|
|
74
|
-
console.log('Existing configuration found:');
|
|
75
|
-
if (configExists)
|
|
76
|
-
console.log(` ✓ ${paths.configPath}`);
|
|
77
|
-
if (localExists)
|
|
78
|
-
console.log(` ✓ ${paths.localPath}`);
|
|
79
|
-
if (options.interactive) {
|
|
80
|
-
const { proceed } = await prompts({
|
|
81
|
-
type: 'confirm',
|
|
82
|
-
name: 'proceed',
|
|
83
|
-
message: 'Update existing configuration?',
|
|
84
|
-
initial: true
|
|
85
|
-
});
|
|
86
|
-
if (!proceed) {
|
|
87
|
-
console.log('Cancelled.');
|
|
88
|
-
process.exit(0);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
console.log('Use --force to overwrite existing files.');
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Get API key
|
|
97
|
-
let apiKey = options.apiKey || process.env.LINEAR_API_KEY;
|
|
98
|
-
if (!apiKey && options.interactive) {
|
|
99
|
-
const response = await prompts({
|
|
100
|
-
type: 'password',
|
|
101
|
-
name: 'apiKey',
|
|
102
|
-
message: 'Enter your Linear API key:',
|
|
103
|
-
validate: v => v.startsWith('lin_api_') ? true : 'API key should start with "lin_api_"'
|
|
104
|
-
});
|
|
105
|
-
apiKey = response.apiKey;
|
|
106
|
-
}
|
|
107
|
-
if (!apiKey) {
|
|
108
|
-
console.error('Error: LINEAR_API_KEY is required.');
|
|
109
|
-
console.error('Get your API key from: https://linear.app/settings/api');
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
// Create Linear client
|
|
113
|
-
const client = getLinearClient();
|
|
114
|
-
console.log('\n📡 Fetching data from Linear...');
|
|
115
|
-
// Fetch teams
|
|
116
|
-
const teamsData = await client.teams();
|
|
117
|
-
const teams = teamsData.nodes;
|
|
118
|
-
if (teams.length === 0) {
|
|
119
|
-
console.error('Error: No teams found in your Linear workspace.');
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
// Select team
|
|
123
|
-
let selectedTeam = teams[0];
|
|
124
|
-
if (options.team) {
|
|
125
|
-
const found = teams.find(t => t.name.toLowerCase() === options.team.toLowerCase());
|
|
126
|
-
if (found)
|
|
127
|
-
selectedTeam = found;
|
|
128
|
-
}
|
|
129
|
-
else if (options.interactive && teams.length > 1) {
|
|
130
|
-
const response = await prompts({
|
|
131
|
-
type: 'select',
|
|
132
|
-
name: 'teamId',
|
|
133
|
-
message: 'Select your primary team:',
|
|
134
|
-
choices: teams.map(t => ({ title: t.name, value: t.id }))
|
|
135
|
-
});
|
|
136
|
-
selectedTeam = teams.find(t => t.id === response.teamId) || teams[0];
|
|
137
|
-
}
|
|
138
|
-
console.log(` Team: ${selectedTeam.name}`);
|
|
139
|
-
// Fetch team members
|
|
140
|
-
const members = await selectedTeam.members();
|
|
141
|
-
const users = members.nodes;
|
|
142
|
-
console.log(` Users: ${users.length}`);
|
|
143
|
-
// Fetch labels
|
|
144
|
-
const labelsData = await client.issueLabels({
|
|
145
|
-
filter: { team: { id: { eq: selectedTeam.id } } }
|
|
146
|
-
});
|
|
147
|
-
const labels = labelsData.nodes;
|
|
148
|
-
console.log(` Labels: ${labels.length}`);
|
|
149
|
-
// Fetch workflow states
|
|
150
|
-
const statesData = await client.workflowStates({
|
|
151
|
-
filter: { team: { id: { eq: selectedTeam.id } } }
|
|
152
|
-
});
|
|
153
|
-
const states = statesData.nodes;
|
|
154
|
-
// Fetch current cycle using activeCycle (direct and accurate)
|
|
155
|
-
const currentCycle = await selectedTeam.activeCycle;
|
|
156
|
-
// Select current user
|
|
157
|
-
let currentUser = users[0];
|
|
158
|
-
if (options.user) {
|
|
159
|
-
const found = users.find(u => u.email?.toLowerCase() === options.user.toLowerCase() ||
|
|
160
|
-
u.displayName?.toLowerCase() === options.user.toLowerCase());
|
|
161
|
-
if (found)
|
|
162
|
-
currentUser = found;
|
|
163
|
-
}
|
|
164
|
-
else if (options.interactive) {
|
|
165
|
-
const response = await prompts({
|
|
166
|
-
type: 'select',
|
|
167
|
-
name: 'userId',
|
|
168
|
-
message: 'Select yourself:',
|
|
169
|
-
choices: users.map(u => ({
|
|
170
|
-
title: `${u.displayName || u.name} (${u.email})`,
|
|
171
|
-
value: u.id
|
|
172
|
-
}))
|
|
173
|
-
});
|
|
174
|
-
currentUser = users.find(u => u.id === response.userId) || users[0];
|
|
175
|
-
}
|
|
176
|
-
// Select default label
|
|
177
|
-
let defaultLabel = labels[0]?.name || 'Frontend';
|
|
178
|
-
if (options.label) {
|
|
179
|
-
defaultLabel = options.label;
|
|
180
|
-
}
|
|
181
|
-
else if (options.interactive && labels.length > 0) {
|
|
182
|
-
const response = await prompts({
|
|
183
|
-
type: 'select',
|
|
184
|
-
name: 'label',
|
|
185
|
-
message: 'Select default label filter:',
|
|
186
|
-
choices: labels.map(l => ({ title: l.name, value: l.name }))
|
|
187
|
-
});
|
|
188
|
-
defaultLabel = response.label || defaultLabel;
|
|
189
|
-
}
|
|
190
|
-
// Build config
|
|
191
|
-
const teamsConfig = {};
|
|
192
|
-
for (const team of teams) {
|
|
193
|
-
const key = team.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
194
|
-
teamsConfig[key] = {
|
|
195
|
-
id: team.id,
|
|
196
|
-
name: team.name,
|
|
197
|
-
icon: team.icon || undefined
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
const usersConfig = {};
|
|
201
|
-
for (const user of users) {
|
|
202
|
-
const key = (user.displayName || user.name || user.email?.split('@')[0] || 'user')
|
|
203
|
-
.toLowerCase()
|
|
204
|
-
.replace(/[^a-z0-9]/g, '_');
|
|
205
|
-
usersConfig[key] = {
|
|
206
|
-
id: user.id,
|
|
207
|
-
email: user.email || '',
|
|
208
|
-
displayName: user.displayName || user.name || ''
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
const labelsConfig = {};
|
|
212
|
-
for (const label of labels) {
|
|
213
|
-
const key = label.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
214
|
-
labelsConfig[key] = {
|
|
215
|
-
id: label.id,
|
|
216
|
-
name: label.name,
|
|
217
|
-
color: label.color || undefined
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
const statusesConfig = {};
|
|
221
|
-
for (const state of states) {
|
|
222
|
-
const key = state.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
223
|
-
statusesConfig[key] = {
|
|
224
|
-
name: state.name,
|
|
225
|
-
type: state.type
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
const config = {
|
|
229
|
-
teams: teamsConfig,
|
|
230
|
-
users: usersConfig,
|
|
231
|
-
labels: labelsConfig,
|
|
232
|
-
priorities: {
|
|
233
|
-
urgent: { value: 1, name: 'Urgent' },
|
|
234
|
-
high: { value: 2, name: 'High' },
|
|
235
|
-
medium: { value: 3, name: 'Medium' },
|
|
236
|
-
low: { value: 4, name: 'Low' }
|
|
237
|
-
},
|
|
238
|
-
statuses: statusesConfig,
|
|
239
|
-
status_transitions: {
|
|
240
|
-
start_work: 'In Progress',
|
|
241
|
-
complete: 'Done',
|
|
242
|
-
need_review: 'In Review'
|
|
243
|
-
},
|
|
244
|
-
priority_order: ['urgent', 'high', 'medium', 'low', 'none'],
|
|
245
|
-
current_cycle: currentCycle ? {
|
|
246
|
-
id: currentCycle.id,
|
|
247
|
-
name: currentCycle.name || `Cycle #${currentCycle.number}`,
|
|
248
|
-
start_date: currentCycle.startsAt?.toISOString().split('T')[0] || '',
|
|
249
|
-
end_date: currentCycle.endsAt?.toISOString().split('T')[0] || ''
|
|
250
|
-
} : undefined,
|
|
251
|
-
cycle_history: []
|
|
252
|
-
};
|
|
253
|
-
// Find current user key
|
|
254
|
-
const currentUserKey = Object.entries(usersConfig).find(([_, u]) => u.id === currentUser.id)?.[0] || 'user';
|
|
255
|
-
// Find selected team key
|
|
256
|
-
const selectedTeamKey = Object.entries(teamsConfig).find(([_, t]) => t.id === selectedTeam.id)?.[0] || Object.keys(teamsConfig)[0];
|
|
257
|
-
const localConfig = {
|
|
258
|
-
current_user: currentUserKey,
|
|
259
|
-
team: selectedTeamKey,
|
|
260
|
-
label: defaultLabel
|
|
261
|
-
};
|
|
262
|
-
// Write config files
|
|
263
|
-
console.log('\n📝 Writing configuration files...');
|
|
264
|
-
// Ensure directory exists
|
|
265
|
-
await fs.mkdir(paths.baseDir, { recursive: true });
|
|
266
|
-
// Merge with existing config if exists
|
|
267
|
-
if (configExists && !options.force) {
|
|
268
|
-
try {
|
|
269
|
-
const existingContent = await fs.readFile(paths.configPath, 'utf-8');
|
|
270
|
-
const existingConfig = decode(existingContent);
|
|
271
|
-
// Merge: preserve existing custom fields
|
|
272
|
-
config.status_transitions = {
|
|
273
|
-
...existingConfig.status_transitions,
|
|
274
|
-
...config.status_transitions
|
|
275
|
-
};
|
|
276
|
-
// Preserve cycle history
|
|
277
|
-
if (existingConfig.cycle_history) {
|
|
278
|
-
config.cycle_history = existingConfig.cycle_history;
|
|
279
|
-
}
|
|
280
|
-
// Preserve current_cycle if not fetched fresh
|
|
281
|
-
if (!currentCycle && existingConfig.current_cycle) {
|
|
282
|
-
config.current_cycle = existingConfig.current_cycle;
|
|
283
|
-
}
|
|
284
|
-
// Preserve priority_order if exists
|
|
285
|
-
if (existingConfig.priority_order) {
|
|
286
|
-
config.priority_order = existingConfig.priority_order;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// Ignore merge errors
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
await fs.writeFile(paths.configPath, encode(config), 'utf-8');
|
|
294
|
-
console.log(` ✓ ${paths.configPath}`);
|
|
295
|
-
// Merge local config
|
|
296
|
-
if (localExists && !options.force) {
|
|
297
|
-
try {
|
|
298
|
-
const existingContent = await fs.readFile(paths.localPath, 'utf-8');
|
|
299
|
-
const existingLocal = decode(existingContent);
|
|
300
|
-
// Preserve existing values
|
|
301
|
-
if (existingLocal.current_user)
|
|
302
|
-
localConfig.current_user = existingLocal.current_user;
|
|
303
|
-
if (existingLocal.team)
|
|
304
|
-
localConfig.team = existingLocal.team;
|
|
305
|
-
if (existingLocal.label)
|
|
306
|
-
localConfig.label = existingLocal.label;
|
|
307
|
-
if (existingLocal.exclude_assignees)
|
|
308
|
-
localConfig.exclude_assignees = existingLocal.exclude_assignees;
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
// Ignore merge errors
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
await fs.writeFile(paths.localPath, encode(localConfig), 'utf-8');
|
|
315
|
-
console.log(` ✓ ${paths.localPath}`);
|
|
316
|
-
// Summary
|
|
317
|
-
console.log('\n✅ Initialization complete!\n');
|
|
318
|
-
console.log('Configuration summary:');
|
|
319
|
-
console.log(` Team: ${selectedTeam.name}`);
|
|
320
|
-
console.log(` User: ${currentUser.displayName || currentUser.name} (${currentUser.email})`);
|
|
321
|
-
console.log(` Label: ${defaultLabel}`);
|
|
322
|
-
if (currentCycle) {
|
|
323
|
-
console.log(` Cycle: ${currentCycle.name || `Cycle #${currentCycle.number}`}`);
|
|
324
|
-
}
|
|
325
|
-
console.log('\nNext steps:');
|
|
326
|
-
console.log(' 1. Set LINEAR_API_KEY in your shell profile:');
|
|
327
|
-
console.log(` export LINEAR_API_KEY="${apiKey}"`);
|
|
328
|
-
console.log(' 2. Run sync: bun run sync');
|
|
329
|
-
console.log(' 3. Start working: bun run work-on');
|
|
330
|
-
}
|
|
331
|
-
init().catch(console.error);
|