team-toon-tack 1.0.10 → 1.0.12

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/bin/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/bin/cli.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
8
+ const VERSION = pkg.version;
9
+ const COMMANDS = ['init', 'sync', 'work-on', 'done', 'help', 'version'];
10
+ function printHelp() {
11
+ console.log(`
12
+ team-toon-tack (ttt) - Linear task sync & management CLI
13
+
14
+ USAGE:
15
+ ttt <command> [options]
16
+
17
+ COMMANDS:
18
+ init Initialize config files in current directory
19
+ sync Sync issues from Linear to local cycle.ttt
20
+ work-on Start working on a task (interactive or by ID)
21
+ done Mark current task as completed
22
+ help Show this help message
23
+ version Show version
24
+
25
+ GLOBAL OPTIONS:
26
+ -d, --dir <path> Config directory (default: .ttt)
27
+ Can also set via TOON_DIR environment variable
28
+
29
+ EXAMPLES:
30
+ ttt init # Initialize .ttt directory
31
+ ttt init -d ./custom # Initialize in custom directory
32
+ ttt sync # Sync from Linear
33
+ ttt work-on # Interactive task selection
34
+ ttt work-on MP-123 # Work on specific issue
35
+ ttt work-on next # Auto-select highest priority
36
+ ttt done # Complete current task
37
+ ttt done -m "Fixed the bug" # With completion message
38
+
39
+ ENVIRONMENT:
40
+ LINEAR_API_KEY Required. Your Linear API key
41
+ TOON_DIR Optional. Default config directory
42
+
43
+ More info: https://github.com/wayne930242/team-toon-tack
44
+ `);
45
+ }
46
+ function printVersion() {
47
+ console.log(`team-toon-tack v${VERSION}`);
48
+ }
49
+ function parseGlobalArgs(args) {
50
+ let dir = process.env.TOON_DIR || resolve(process.cwd(), '.ttt');
51
+ const commandArgs = [];
52
+ for (let i = 0; i < args.length; i++) {
53
+ const arg = args[i];
54
+ if (arg === '-d' || arg === '--dir') {
55
+ dir = resolve(args[++i] || '.');
56
+ }
57
+ else {
58
+ commandArgs.push(arg);
59
+ }
60
+ }
61
+ return { dir, commandArgs };
62
+ }
63
+ async function main() {
64
+ const args = process.argv.slice(2);
65
+ if (args.length === 0 || args[0] === 'help' || args[0] === '-h' || args[0] === '--help') {
66
+ printHelp();
67
+ process.exit(0);
68
+ }
69
+ if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
70
+ printVersion();
71
+ process.exit(0);
72
+ }
73
+ const command = args[0];
74
+ const restArgs = args.slice(1);
75
+ const { dir, commandArgs } = parseGlobalArgs(restArgs);
76
+ // Set TOON_DIR for scripts to use
77
+ process.env.TOON_DIR = dir;
78
+ if (!COMMANDS.includes(command)) {
79
+ console.error(`Unknown command: ${command}`);
80
+ console.error(`Run 'ttt help' for usage.`);
81
+ process.exit(1);
82
+ }
83
+ // Import and run the appropriate script
84
+ const scriptDir = new URL('../scripts/', import.meta.url).pathname;
85
+ try {
86
+ switch (command) {
87
+ case 'init':
88
+ process.argv = ['node', 'init.js', ...commandArgs];
89
+ await import(`${scriptDir}init.js`);
90
+ break;
91
+ case 'sync':
92
+ await import(`${scriptDir}sync.js`);
93
+ break;
94
+ case 'work-on':
95
+ process.argv = ['node', 'work-on.js', ...commandArgs];
96
+ await import(`${scriptDir}work-on.js`);
97
+ break;
98
+ case 'done':
99
+ process.argv = ['node', 'done-job.js', ...commandArgs];
100
+ await import(`${scriptDir}done-job.js`);
101
+ break;
102
+ }
103
+ }
104
+ catch (error) {
105
+ if (error instanceof Error) {
106
+ console.error(`Error: ${error.message}`);
107
+ }
108
+ process.exit(1);
109
+ }
110
+ }
111
+ main();
package/bin/cli.ts CHANGED
@@ -1,10 +1,11 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  import { resolve } from 'node:path';
3
3
  import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
4
6
 
5
- // Read version from package.json
6
- const pkgPath = new URL('../package.json', import.meta.url).pathname;
7
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
8
9
  const VERSION = pkg.version;
9
10
 
10
11
  const COMMANDS = ['init', 'sync', 'work-on', 'done', 'help', 'version'] as const;
@@ -95,23 +96,22 @@ async function main() {
95
96
 
96
97
  // Import and run the appropriate script
97
98
  const scriptDir = new URL('../scripts/', import.meta.url).pathname;
98
-
99
99
  try {
100
100
  switch (command) {
101
101
  case 'init':
102
- process.argv = ['bun', 'init.ts', ...commandArgs];
103
- await import(`${scriptDir}init.ts`);
102
+ process.argv = ['node', 'init.js', ...commandArgs];
103
+ await import(`${scriptDir}init.js`);
104
104
  break;
105
105
  case 'sync':
106
- await import(`${scriptDir}sync.ts`);
106
+ await import(`${scriptDir}sync.js`);
107
107
  break;
108
108
  case 'work-on':
109
- process.argv = ['bun', 'work-on.ts', ...commandArgs];
110
- await import(`${scriptDir}work-on.ts`);
109
+ process.argv = ['node', 'work-on.js', ...commandArgs];
110
+ await import(`${scriptDir}work-on.js`);
111
111
  break;
112
112
  case 'done':
113
- process.argv = ['bun', 'done-job.ts', ...commandArgs];
114
- await import(`${scriptDir}done-job.ts`);
113
+ process.argv = ['node', 'done-job.js', ...commandArgs];
114
+ await import(`${scriptDir}done-job.js`);
115
115
  break;
116
116
  }
117
117
  } catch (error) {
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Linear task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {
7
- "ttt": "./bin/cli.ts",
8
- "team-toon-tack": "./bin/cli.ts"
7
+ "ttt": "./bin/cli.js",
8
+ "team-toon-tack": "./bin/cli.js"
9
9
  },
10
10
  "exports": {
11
- ".": "./scripts/utils.ts",
12
- "./utils": "./scripts/utils.ts"
11
+ ".": "./scripts/utils.js",
12
+ "./utils": "./scripts/utils.js"
13
13
  },
14
14
  "files": [
15
15
  "bin",
@@ -18,9 +18,8 @@
18
18
  "package.json"
19
19
  ],
20
20
  "scripts": {
21
- "start": "bun bin/cli.ts",
22
- "build": "bun build bin/cli.ts --outdir dist --target node",
23
- "prepublishOnly": "bun run build",
21
+ "build": "tsc",
22
+ "prepublishOnly": "npm run build",
24
23
  "release": "npm config set //registry.npmjs.org/:_authToken=$NPM_PUBLISH_KEY && npm publish"
25
24
  },
26
25
  "dependencies": {
@@ -29,8 +28,9 @@
29
28
  "prompts": "^2.4.2"
30
29
  },
31
30
  "devDependencies": {
32
- "@types/bun": "^1.2.0",
33
- "@types/prompts": "^2.4.9"
31
+ "@types/node": "^20.0.0",
32
+ "@types/prompts": "^2.4.9",
33
+ "typescript": "^5.0.0"
34
34
  },
35
35
  "keywords": [
36
36
  "linear",
@@ -47,7 +47,6 @@
47
47
  "url": "git+https://github.com/wayne930242/team-toon-tack.git"
48
48
  },
49
49
  "engines": {
50
- "node": ">=18.0.0",
51
- "bun": ">=1.0.0"
50
+ "node": ">=18.0.0"
52
51
  }
53
52
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,230 @@
1
+ import prompts from 'prompts';
2
+ import { execSync } from 'node:child_process';
3
+ import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, getTeamId } from './utils.js';
4
+ async function getLatestCommit() {
5
+ try {
6
+ const shortHash = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
7
+ const fullHash = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
8
+ const message = execSync('git log -1 --format=%s', { encoding: 'utf-8' }).trim();
9
+ const diffStat = execSync('git diff HEAD~1 --stat --stat-width=60', { encoding: 'utf-8' }).trim();
10
+ // Get remote URL and construct commit link
11
+ let commitUrl = null;
12
+ try {
13
+ const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
14
+ // Handle SSH or HTTPS URLs
15
+ // git@gitlab.com:org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
16
+ // https://gitlab.com/org/repo.git -> https://gitlab.com/org/repo/-/commit/hash
17
+ if (remoteUrl.includes('gitlab')) {
18
+ const match = remoteUrl.match(/(?:git@|https:\/\/)([^:\/]+)[:\\/](.+?)(?:\.git)?$/);
19
+ if (match) {
20
+ commitUrl = `https://${match[1]}/${match[2]}/-/commit/${fullHash}`;
21
+ }
22
+ }
23
+ else if (remoteUrl.includes('github')) {
24
+ const match = remoteUrl.match(/(?:git@|https:\/\/)([^:\/]+)[:\\/](.+?)(?:\.git)?$/);
25
+ if (match) {
26
+ commitUrl = `https://${match[1]}/${match[2]}/commit/${fullHash}`;
27
+ }
28
+ }
29
+ }
30
+ catch {
31
+ // Ignore if can't get remote URL
32
+ }
33
+ return { shortHash, fullHash, message, diffStat, commitUrl };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ function parseArgs(args) {
40
+ let issueId;
41
+ let message;
42
+ for (let i = 0; i < args.length; i++) {
43
+ const arg = args[i];
44
+ if (arg === '-m' || arg === '--message') {
45
+ message = args[++i];
46
+ }
47
+ else if (!arg.startsWith('-')) {
48
+ issueId = arg;
49
+ }
50
+ }
51
+ return { issueId, message };
52
+ }
53
+ async function doneJob() {
54
+ const args = process.argv.slice(2);
55
+ // Handle help flag
56
+ if (args.includes('--help') || args.includes('-h')) {
57
+ console.log(`Usage: ttt done [issue-id] [-m message]
58
+
59
+ Arguments:
60
+ issue-id Issue ID (e.g., MP-624). Optional if only one task is in-progress
61
+
62
+ Options:
63
+ -m, --message AI summary message describing the fix
64
+
65
+ Examples:
66
+ ttt done # Complete current in-progress task
67
+ ttt done MP-624 # Complete specific task
68
+ ttt done -m "Fixed null check" # With completion message
69
+ ttt done MP-624 -m "Refactored" # Specific task with message`);
70
+ process.exit(0);
71
+ }
72
+ const { issueId: argIssueId, message: argMessage } = parseArgs(args);
73
+ let issueId = argIssueId;
74
+ const config = await loadConfig();
75
+ const localConfig = await loadLocalConfig();
76
+ const data = await loadCycleData();
77
+ if (!data) {
78
+ console.error('No cycle data found. Run /sync-linear first.');
79
+ process.exit(1);
80
+ }
81
+ // Find in-progress tasks
82
+ const inProgressTasks = data.tasks.filter(t => t.localStatus === 'in-progress');
83
+ if (inProgressTasks.length === 0) {
84
+ console.log('沒有進行中的任務');
85
+ process.exit(0);
86
+ }
87
+ // Phase 0: Issue Resolution
88
+ if (!issueId) {
89
+ if (inProgressTasks.length === 1) {
90
+ issueId = inProgressTasks[0].id;
91
+ console.log(`Auto-selected: ${issueId}`);
92
+ }
93
+ else if (process.stdin.isTTY) {
94
+ const choices = inProgressTasks.map(task => ({
95
+ title: `${task.id}: ${task.title}`,
96
+ value: task.id,
97
+ description: task.labels.join(', ')
98
+ }));
99
+ const response = await prompts({
100
+ type: 'select',
101
+ name: 'issueId',
102
+ message: '選擇要完成的任務:',
103
+ choices: choices
104
+ });
105
+ if (!response.issueId) {
106
+ console.log('已取消');
107
+ process.exit(0);
108
+ }
109
+ issueId = response.issueId;
110
+ }
111
+ else {
112
+ console.error('多個進行中任務,請指定 issue ID:');
113
+ inProgressTasks.forEach(t => console.log(` - ${t.id}: ${t.title}`));
114
+ process.exit(1);
115
+ }
116
+ }
117
+ // Phase 1: Find task
118
+ const task = data.tasks.find(t => t.id === issueId || t.id === `MP-${issueId}`);
119
+ if (!task) {
120
+ console.error(`Issue ${issueId} not found in current cycle.`);
121
+ process.exit(1);
122
+ }
123
+ if (task.localStatus !== 'in-progress') {
124
+ console.log(`⚠️ 任務 ${task.id} 不在進行中狀態 (目前: ${task.localStatus})`);
125
+ process.exit(1);
126
+ }
127
+ // Get latest commit for comment
128
+ const commit = await getLatestCommit();
129
+ // Phase 2: Get AI summary message
130
+ let aiMessage = argMessage || '';
131
+ if (!aiMessage && process.stdin.isTTY) {
132
+ const aiMsgResponse = await prompts({
133
+ type: 'text',
134
+ name: 'aiMessage',
135
+ message: 'AI 修復說明 (如何解決此問題):',
136
+ });
137
+ aiMessage = aiMsgResponse.aiMessage || '';
138
+ }
139
+ // Phase 3: Update Linear
140
+ if (task.linearId && process.env.LINEAR_API_KEY) {
141
+ try {
142
+ const client = getLinearClient();
143
+ const workflowStates = await client.workflowStates({
144
+ filter: { team: { id: { eq: getTeamId(config, localConfig.team) } } }
145
+ });
146
+ const doneState = workflowStates.nodes.find(s => s.name === 'Done');
147
+ // Update issue to Done
148
+ if (doneState) {
149
+ await client.updateIssue(task.linearId, { stateId: doneState.id });
150
+ console.log(`Linear: ${task.id} → Done`);
151
+ }
152
+ // Add comment with commit info and AI summary
153
+ if (commit) {
154
+ const commitLink = commit.commitUrl
155
+ ? `[${commit.shortHash}](${commit.commitUrl})`
156
+ : `\`${commit.shortHash}\``;
157
+ const commentParts = [
158
+ '## ✅ 開發完成',
159
+ '',
160
+ '### 🤖 AI 修復說明',
161
+ aiMessage || '_No description provided_',
162
+ '',
163
+ '### 📝 Commit Info',
164
+ `**Commit:** ${commitLink}`,
165
+ `**Message:** ${commit.message}`,
166
+ '',
167
+ '### 📊 Changes',
168
+ '```',
169
+ commit.diffStat,
170
+ '```',
171
+ ];
172
+ await client.createComment({
173
+ issueId: task.linearId,
174
+ body: commentParts.join('\n')
175
+ });
176
+ console.log(`Linear: 已新增 commit 留言`);
177
+ }
178
+ // Update parent to Testing if exists
179
+ if (task.parentIssueId) {
180
+ try {
181
+ // Find parent issue by identifier
182
+ const searchResult = await client.searchIssues(task.parentIssueId);
183
+ const parentIssue = searchResult.nodes.find(issue => issue.identifier === task.parentIssueId);
184
+ if (parentIssue) {
185
+ // Get parent's team workflow states
186
+ const parentTeam = await parentIssue.team;
187
+ if (parentTeam) {
188
+ const parentWorkflowStates = await client.workflowStates({
189
+ filter: { team: { id: { eq: parentTeam.id } } }
190
+ });
191
+ const testingState = parentWorkflowStates.nodes.find(s => s.name === 'Testing');
192
+ if (testingState) {
193
+ await client.updateIssue(parentIssue.id, { stateId: testingState.id });
194
+ console.log(`Linear: Parent ${task.parentIssueId} → Testing`);
195
+ }
196
+ }
197
+ }
198
+ }
199
+ catch (parentError) {
200
+ console.error('Failed to update parent issue:', parentError);
201
+ }
202
+ }
203
+ }
204
+ catch (e) {
205
+ console.error('Failed to update Linear:', e);
206
+ }
207
+ }
208
+ // Phase 4: Update local status
209
+ task.localStatus = 'completed';
210
+ await saveCycleData(data);
211
+ console.log(`Local: ${task.id} → completed`);
212
+ // Phase 5: Summary
213
+ console.log(`\n${'═'.repeat(50)}`);
214
+ console.log(`✅ ${task.id}: ${task.title}`);
215
+ console.log(`${'═'.repeat(50)}`);
216
+ if (commit) {
217
+ console.log(`Commit: ${commit.shortHash} - ${commit.message}`);
218
+ if (commit.commitUrl) {
219
+ console.log(`URL: ${commit.commitUrl}`);
220
+ }
221
+ }
222
+ if (aiMessage) {
223
+ console.log(`AI: ${aiMessage}`);
224
+ }
225
+ if (task.parentIssueId) {
226
+ console.log(`Parent: ${task.parentIssueId} → Testing`);
227
+ }
228
+ console.log(`\n🎉 任務完成!`);
229
+ }
230
+ doneJob().catch(console.error);
@@ -1,6 +1,6 @@
1
1
  import prompts from 'prompts';
2
2
  import { execSync } from 'node:child_process';
3
- import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, getTeamId, getDefaultTeamKey } from './utils';
3
+ import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, getTeamId, getDefaultTeamKey } from './utils.js';
4
4
 
5
5
  interface CommitInfo {
6
6
  shortHash: string;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};