team-toon-tack 1.0.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 +187 -0
- package/bin/cli.ts +119 -0
- package/package.json +51 -0
- package/scripts/done-job.ts +242 -0
- package/scripts/init.ts +360 -0
- package/scripts/sync.ts +172 -0
- package/scripts/utils.ts +208 -0
- package/scripts/work-on.ts +145 -0
- package/templates/config.example.toon +82 -0
- package/templates/local.example.toon +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# team-toon-tack (ttt)
|
|
2
|
+
|
|
3
|
+
CLI tool for syncing and managing Linear issues with local TOON format.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# npm (recommended)
|
|
9
|
+
npm install -g team-toon-tack
|
|
10
|
+
|
|
11
|
+
# Or with bun
|
|
12
|
+
bun add -g team-toon-tack
|
|
13
|
+
|
|
14
|
+
# Or use npx/bunx without installing
|
|
15
|
+
npx team-toon-tack <command>
|
|
16
|
+
bunx team-toon-tack <command>
|
|
17
|
+
|
|
18
|
+
# Short alias after global install
|
|
19
|
+
ttt <command>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 1. Set your Linear API key
|
|
26
|
+
export LINEAR_API_KEY="lin_api_xxxxx"
|
|
27
|
+
|
|
28
|
+
# 2. Initialize in your project
|
|
29
|
+
cd your-project
|
|
30
|
+
ttt init
|
|
31
|
+
|
|
32
|
+
# 3. Sync issues from Linear
|
|
33
|
+
ttt sync
|
|
34
|
+
|
|
35
|
+
# 4. Start working
|
|
36
|
+
ttt work-on
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
### `ttt init`
|
|
42
|
+
|
|
43
|
+
Initialize configuration files in current directory.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
ttt init # Interactive mode
|
|
47
|
+
ttt init --user alice@example.com # Pre-select user
|
|
48
|
+
ttt init --label Frontend # Set default label
|
|
49
|
+
ttt init --force # Overwrite existing config
|
|
50
|
+
ttt init -y # Non-interactive mode
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `ttt sync`
|
|
54
|
+
|
|
55
|
+
Sync current cycle issues from Linear.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ttt sync
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `ttt work-on`
|
|
62
|
+
|
|
63
|
+
Start working on a task.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
ttt work-on # Interactive selection
|
|
67
|
+
ttt work-on MP-123 # Specific issue
|
|
68
|
+
ttt work-on next # Auto-select highest priority
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `ttt done`
|
|
72
|
+
|
|
73
|
+
Mark task as completed.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
ttt done # Auto-select if only one in-progress
|
|
77
|
+
ttt done MP-123 # Specific issue
|
|
78
|
+
ttt done -m "Fixed the bug" # With completion message
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
### Directory Structure
|
|
84
|
+
|
|
85
|
+
After `ttt init`, your project will have:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
your-project/
|
|
89
|
+
├── config.toon # Team configuration (gitignore recommended)
|
|
90
|
+
├── local.toon # Personal settings (gitignore)
|
|
91
|
+
└── cycle.toon # Current cycle data (gitignore, auto-generated)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Custom Config Directory
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Use -d flag
|
|
98
|
+
ttt sync -d ./team
|
|
99
|
+
|
|
100
|
+
# Or set environment variable
|
|
101
|
+
export TOON_DIR=./team
|
|
102
|
+
ttt sync
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### config.toon
|
|
106
|
+
|
|
107
|
+
Team-wide configuration (fetched from Linear):
|
|
108
|
+
|
|
109
|
+
```toon
|
|
110
|
+
teams:
|
|
111
|
+
main:
|
|
112
|
+
id: TEAM_UUID
|
|
113
|
+
name: Team Name
|
|
114
|
+
|
|
115
|
+
users:
|
|
116
|
+
alice:
|
|
117
|
+
id: USER_UUID
|
|
118
|
+
email: alice@example.com
|
|
119
|
+
displayName: Alice
|
|
120
|
+
|
|
121
|
+
labels:
|
|
122
|
+
frontend:
|
|
123
|
+
id: LABEL_UUID
|
|
124
|
+
name: Frontend
|
|
125
|
+
|
|
126
|
+
current_cycle:
|
|
127
|
+
id: CYCLE_UUID
|
|
128
|
+
name: Cycle #1
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### local.toon
|
|
132
|
+
|
|
133
|
+
Personal settings:
|
|
134
|
+
|
|
135
|
+
```toon
|
|
136
|
+
current_user: alice
|
|
137
|
+
label: Frontend
|
|
138
|
+
exclude_assignees[1]: bob
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Field | Description |
|
|
142
|
+
|-------|-------------|
|
|
143
|
+
| `current_user` | Your user key from config.toon |
|
|
144
|
+
| `label` | Filter issues by label (optional) |
|
|
145
|
+
| `exclude_assignees` | Hide issues from these users (optional) |
|
|
146
|
+
|
|
147
|
+
## Environment Variables
|
|
148
|
+
|
|
149
|
+
| Variable | Description |
|
|
150
|
+
|----------|-------------|
|
|
151
|
+
| `LINEAR_API_KEY` | **Required.** Your Linear API key |
|
|
152
|
+
| `TOON_DIR` | Config directory (default: current directory) |
|
|
153
|
+
|
|
154
|
+
## Integration Examples
|
|
155
|
+
|
|
156
|
+
### With Claude Code
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
# .claude/commands/sync.md
|
|
160
|
+
---
|
|
161
|
+
description: Sync Linear issues
|
|
162
|
+
---
|
|
163
|
+
ttt sync -d team
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### As Git Submodule
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Add config directory as submodule
|
|
170
|
+
git submodule add https://github.com/your-org/team-config.git team
|
|
171
|
+
cd team && ttt sync
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### In package.json
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"scripts": {
|
|
179
|
+
"sync": "ttt sync -d team",
|
|
180
|
+
"work": "ttt work-on -d team"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
package/bin/cli.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const COMMANDS = ['init', 'sync', 'work-on', 'done', 'help', 'version'] as const;
|
|
5
|
+
type Command = typeof COMMANDS[number];
|
|
6
|
+
|
|
7
|
+
function printHelp() {
|
|
8
|
+
console.log(`
|
|
9
|
+
team-toon-tack (ttt) - Linear task sync & management CLI
|
|
10
|
+
|
|
11
|
+
USAGE:
|
|
12
|
+
ttt <command> [options]
|
|
13
|
+
|
|
14
|
+
COMMANDS:
|
|
15
|
+
init Initialize config files in current directory
|
|
16
|
+
sync Sync issues from Linear to local cycle.toon
|
|
17
|
+
work-on Start working on a task (interactive or by ID)
|
|
18
|
+
done Mark current task as completed
|
|
19
|
+
help Show this help message
|
|
20
|
+
version Show version
|
|
21
|
+
|
|
22
|
+
GLOBAL OPTIONS:
|
|
23
|
+
-d, --dir <path> Config directory (default: current directory)
|
|
24
|
+
Can also set via TOON_DIR environment variable
|
|
25
|
+
|
|
26
|
+
EXAMPLES:
|
|
27
|
+
ttt init # Initialize in current directory
|
|
28
|
+
ttt init -d ./team # Initialize in ./team directory
|
|
29
|
+
ttt sync # Sync from Linear
|
|
30
|
+
ttt work-on # Interactive task selection
|
|
31
|
+
ttt work-on MP-123 # Work on specific issue
|
|
32
|
+
ttt work-on next # Auto-select highest priority
|
|
33
|
+
ttt done # Complete current task
|
|
34
|
+
ttt done -m "Fixed the bug" # With completion message
|
|
35
|
+
|
|
36
|
+
ENVIRONMENT:
|
|
37
|
+
LINEAR_API_KEY Required. Your Linear API key
|
|
38
|
+
TOON_DIR Optional. Default config directory
|
|
39
|
+
|
|
40
|
+
More info: https://github.com/wayne930242/team-toon-tack
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printVersion() {
|
|
45
|
+
console.log('team-toon-tack v1.0.0');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseGlobalArgs(args: string[]): { dir: string; commandArgs: string[] } {
|
|
49
|
+
let dir = process.env.TOON_DIR || process.cwd();
|
|
50
|
+
const commandArgs: string[] = [];
|
|
51
|
+
|
|
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
|
+
} else {
|
|
57
|
+
commandArgs.push(arg);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { dir, commandArgs };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
const args = process.argv.slice(2);
|
|
66
|
+
|
|
67
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '-h' || args[0] === '--help') {
|
|
68
|
+
printHelp();
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
|
|
73
|
+
printVersion();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const command = args[0] as Command;
|
|
78
|
+
const restArgs = args.slice(1);
|
|
79
|
+
const { dir, commandArgs } = parseGlobalArgs(restArgs);
|
|
80
|
+
|
|
81
|
+
// Set TOON_DIR for scripts to use
|
|
82
|
+
process.env.TOON_DIR = dir;
|
|
83
|
+
|
|
84
|
+
if (!COMMANDS.includes(command)) {
|
|
85
|
+
console.error(`Unknown command: ${command}`);
|
|
86
|
+
console.error(`Run 'ttt help' for usage.`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Import and run the appropriate script
|
|
91
|
+
const scriptDir = new URL('../scripts/', import.meta.url).pathname;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
switch (command) {
|
|
95
|
+
case 'init':
|
|
96
|
+
process.argv = ['bun', 'init.ts', ...commandArgs];
|
|
97
|
+
await import(`${scriptDir}init.ts`);
|
|
98
|
+
break;
|
|
99
|
+
case 'sync':
|
|
100
|
+
await import(`${scriptDir}sync.ts`);
|
|
101
|
+
break;
|
|
102
|
+
case 'work-on':
|
|
103
|
+
process.argv = ['bun', 'work-on.ts', ...commandArgs];
|
|
104
|
+
await import(`${scriptDir}work-on.ts`);
|
|
105
|
+
break;
|
|
106
|
+
case 'done':
|
|
107
|
+
process.argv = ['bun', 'done-job.ts', ...commandArgs];
|
|
108
|
+
await import(`${scriptDir}done-job.ts`);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof Error) {
|
|
113
|
+
console.error(`Error: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "team-toon-tack",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Linear task sync & management CLI with TOON format",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ttt": "./bin/cli.ts",
|
|
8
|
+
"team-toon-tack": "./bin/cli.ts"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./scripts/utils.ts",
|
|
12
|
+
"./utils": "./scripts/utils.ts"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"scripts",
|
|
17
|
+
"templates"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "bun bin/cli.ts",
|
|
21
|
+
"build": "bun build bin/cli.ts --outdir dist --target node",
|
|
22
|
+
"prepublishOnly": "bun run build"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@linear/sdk": "^29.0.0",
|
|
26
|
+
"@toon-format/toon": "^2.0.0",
|
|
27
|
+
"prompts": "^2.4.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "^1.2.0",
|
|
31
|
+
"@types/prompts": "^2.4.9"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"linear",
|
|
35
|
+
"task-management",
|
|
36
|
+
"cli",
|
|
37
|
+
"toon",
|
|
38
|
+
"sync",
|
|
39
|
+
"project-management"
|
|
40
|
+
],
|
|
41
|
+
"author": "wayne930242",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/wayne930242/team-toon-tack.git"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18.0.0",
|
|
49
|
+
"bun": ">=1.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { getLinearClient, loadConfig, loadCycleData, saveCycleData, getTeamId, getDefaultTeamKey } from './utils';
|
|
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 { issueId: argIssueId, message: argMessage } = parseArgs(process.argv.slice(2));
|
|
66
|
+
let issueId = argIssueId;
|
|
67
|
+
|
|
68
|
+
const config = await loadConfig();
|
|
69
|
+
const data = await loadCycleData();
|
|
70
|
+
|
|
71
|
+
if (!data) {
|
|
72
|
+
console.error('No cycle data found. Run /sync-linear first.');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find in-progress tasks
|
|
77
|
+
const inProgressTasks = data.tasks.filter(t => t.localStatus === 'in-progress');
|
|
78
|
+
|
|
79
|
+
if (inProgressTasks.length === 0) {
|
|
80
|
+
console.log('沒有進行中的任務');
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Phase 0: Issue Resolution
|
|
85
|
+
if (!issueId) {
|
|
86
|
+
if (inProgressTasks.length === 1) {
|
|
87
|
+
issueId = inProgressTasks[0].id;
|
|
88
|
+
console.log(`Auto-selected: ${issueId}`);
|
|
89
|
+
} else if (process.stdin.isTTY) {
|
|
90
|
+
const choices = inProgressTasks.map(task => ({
|
|
91
|
+
title: `${task.id}: ${task.title}`,
|
|
92
|
+
value: task.id,
|
|
93
|
+
description: task.labels.join(', ')
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
const response = await prompts({
|
|
97
|
+
type: 'select',
|
|
98
|
+
name: 'issueId',
|
|
99
|
+
message: '選擇要完成的任務:',
|
|
100
|
+
choices: choices
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!response.issueId) {
|
|
104
|
+
console.log('已取消');
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
issueId = response.issueId;
|
|
108
|
+
} else {
|
|
109
|
+
console.error('多個進行中任務,請指定 issue ID:');
|
|
110
|
+
inProgressTasks.forEach(t => console.log(` - ${t.id}: ${t.title}`));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Phase 1: Find task
|
|
116
|
+
const task = data.tasks.find(t => t.id === issueId || t.id === `MP-${issueId}`);
|
|
117
|
+
if (!task) {
|
|
118
|
+
console.error(`Issue ${issueId} not found in current cycle.`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (task.localStatus !== 'in-progress') {
|
|
123
|
+
console.log(`⚠️ 任務 ${task.id} 不在進行中狀態 (目前: ${task.localStatus})`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Get latest commit for comment
|
|
128
|
+
const commit = await getLatestCommit();
|
|
129
|
+
|
|
130
|
+
// Phase 2: Get AI summary message
|
|
131
|
+
let aiMessage = argMessage || '';
|
|
132
|
+
if (!aiMessage && process.stdin.isTTY) {
|
|
133
|
+
const aiMsgResponse = await prompts({
|
|
134
|
+
type: 'text',
|
|
135
|
+
name: 'aiMessage',
|
|
136
|
+
message: 'AI 修復說明 (如何解決此問題):',
|
|
137
|
+
});
|
|
138
|
+
aiMessage = aiMsgResponse.aiMessage || '';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Phase 3: Update Linear
|
|
142
|
+
if (task.linearId && process.env.LINEAR_API_KEY) {
|
|
143
|
+
try {
|
|
144
|
+
const client = getLinearClient();
|
|
145
|
+
const workflowStates = await client.workflowStates({
|
|
146
|
+
filter: { team: { id: { eq: getTeamId(config) } } }
|
|
147
|
+
});
|
|
148
|
+
const doneState = workflowStates.nodes.find(s => s.name === 'Done');
|
|
149
|
+
|
|
150
|
+
// Update issue to Done
|
|
151
|
+
if (doneState) {
|
|
152
|
+
await client.updateIssue(task.linearId, { stateId: doneState.id });
|
|
153
|
+
console.log(`Linear: ${task.id} → Done`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add comment with commit info and AI summary
|
|
157
|
+
if (commit) {
|
|
158
|
+
const commitLink = commit.commitUrl
|
|
159
|
+
? `[${commit.shortHash}](${commit.commitUrl})`
|
|
160
|
+
: `\`${commit.shortHash}\``;
|
|
161
|
+
|
|
162
|
+
const commentParts = [
|
|
163
|
+
'## ✅ 開發完成',
|
|
164
|
+
'',
|
|
165
|
+
'### 🤖 AI 修復說明',
|
|
166
|
+
aiMessage || '_No description provided_',
|
|
167
|
+
'',
|
|
168
|
+
'### 📝 Commit Info',
|
|
169
|
+
`**Commit:** ${commitLink}`,
|
|
170
|
+
`**Message:** ${commit.message}`,
|
|
171
|
+
'',
|
|
172
|
+
'### 📊 Changes',
|
|
173
|
+
'```',
|
|
174
|
+
commit.diffStat,
|
|
175
|
+
'```',
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
await client.createComment({
|
|
179
|
+
issueId: task.linearId,
|
|
180
|
+
body: commentParts.join('\n')
|
|
181
|
+
});
|
|
182
|
+
console.log(`Linear: 已新增 commit 留言`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Update parent to Testing if exists
|
|
186
|
+
if (task.parentIssueId) {
|
|
187
|
+
try {
|
|
188
|
+
// Find parent issue by identifier
|
|
189
|
+
const searchResult = await client.searchIssues(task.parentIssueId);
|
|
190
|
+
const parentIssue = searchResult.nodes.find(
|
|
191
|
+
issue => issue.identifier === task.parentIssueId
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (parentIssue) {
|
|
195
|
+
// Get parent's team workflow states
|
|
196
|
+
const parentTeam = await parentIssue.team;
|
|
197
|
+
if (parentTeam) {
|
|
198
|
+
const parentWorkflowStates = await client.workflowStates({
|
|
199
|
+
filter: { team: { id: { eq: parentTeam.id } } }
|
|
200
|
+
});
|
|
201
|
+
const testingState = parentWorkflowStates.nodes.find(s => s.name === 'Testing');
|
|
202
|
+
|
|
203
|
+
if (testingState) {
|
|
204
|
+
await client.updateIssue(parentIssue.id, { stateId: testingState.id });
|
|
205
|
+
console.log(`Linear: Parent ${task.parentIssueId} → Testing`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch (parentError) {
|
|
210
|
+
console.error('Failed to update parent issue:', parentError);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.error('Failed to update Linear:', e);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Phase 4: Update local status
|
|
219
|
+
task.localStatus = 'completed';
|
|
220
|
+
await saveCycleData(data);
|
|
221
|
+
console.log(`Local: ${task.id} → completed`);
|
|
222
|
+
|
|
223
|
+
// Phase 5: Summary
|
|
224
|
+
console.log(`\n${'═'.repeat(50)}`);
|
|
225
|
+
console.log(`✅ ${task.id}: ${task.title}`);
|
|
226
|
+
console.log(`${'═'.repeat(50)}`);
|
|
227
|
+
if (commit) {
|
|
228
|
+
console.log(`Commit: ${commit.shortHash} - ${commit.message}`);
|
|
229
|
+
if (commit.commitUrl) {
|
|
230
|
+
console.log(`URL: ${commit.commitUrl}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (aiMessage) {
|
|
234
|
+
console.log(`AI: ${aiMessage}`);
|
|
235
|
+
}
|
|
236
|
+
if (task.parentIssueId) {
|
|
237
|
+
console.log(`Parent: ${task.parentIssueId} → Testing`);
|
|
238
|
+
}
|
|
239
|
+
console.log(`\n🎉 任務完成!`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
doneJob().catch(console.error);
|