team-toon-tack 1.0.11 → 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 +2 -0
- package/bin/cli.js +111 -0
- package/{dist/bin/cli.js → bin/cli.ts} +47 -29
- package/package.json +12 -10
- package/scripts/done-job.d.ts +1 -0
- package/scripts/done-job.js +230 -0
- package/{dist/scripts/done-job.js → scripts/done-job.ts} +127 -82
- package/scripts/init.d.ts +2 -0
- package/scripts/init.js +331 -0
- package/scripts/init.ts +375 -0
- package/scripts/sync.d.ts +1 -0
- package/scripts/sync.js +178 -0
- package/{dist/scripts/sync.js → scripts/sync.ts} +92 -52
- package/scripts/utils.d.ts +101 -0
- package/scripts/utils.js +137 -0
- package/scripts/utils.ts +236 -0
- package/scripts/work-on.d.ts +1 -0
- package/scripts/work-on.js +138 -0
- package/scripts/work-on.ts +161 -0
- package/dist/cli-6rkvcjaj.js +0 -4923
- package/dist/cli-pyanjjwn.js +0 -21
- package/dist/scripts/init.js +0 -313
- package/dist/scripts/utils.js +0 -14793
- package/dist/scripts/work-on.js +0 -142
package/bin/cli.d.ts
ADDED
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();
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
9
|
+
const VERSION = pkg.version;
|
|
10
|
+
|
|
11
|
+
const COMMANDS = ['init', 'sync', 'work-on', 'done', 'help', 'version'] as const;
|
|
12
|
+
type Command = typeof COMMANDS[number];
|
|
13
|
+
|
|
11
14
|
function printHelp() {
|
|
12
15
|
console.log(`
|
|
13
16
|
team-toon-tack (ttt) - Linear task sync & management CLI
|
|
@@ -44,57 +47,71 @@ ENVIRONMENT:
|
|
|
44
47
|
More info: https://github.com/wayne930242/team-toon-tack
|
|
45
48
|
`);
|
|
46
49
|
}
|
|
50
|
+
|
|
47
51
|
function printVersion() {
|
|
48
52
|
console.log(`team-toon-tack v${VERSION}`);
|
|
49
53
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
|
|
55
|
+
function parseGlobalArgs(args: string[]): { dir: string; commandArgs: string[] } {
|
|
56
|
+
let dir = process.env.TOON_DIR || resolve(process.cwd(), '.ttt');
|
|
57
|
+
const commandArgs: string[] = [];
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < args.length; i++) {
|
|
54
60
|
const arg = args[i];
|
|
55
|
-
if (arg ===
|
|
56
|
-
dir = resolve(args[++i] ||
|
|
61
|
+
if (arg === '-d' || arg === '--dir') {
|
|
62
|
+
dir = resolve(args[++i] || '.');
|
|
57
63
|
} else {
|
|
58
64
|
commandArgs.push(arg);
|
|
59
65
|
}
|
|
60
66
|
}
|
|
67
|
+
|
|
61
68
|
return { dir, commandArgs };
|
|
62
69
|
}
|
|
70
|
+
|
|
63
71
|
async function main() {
|
|
64
72
|
const args = process.argv.slice(2);
|
|
65
|
-
|
|
73
|
+
|
|
74
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '-h' || args[0] === '--help') {
|
|
66
75
|
printHelp();
|
|
67
76
|
process.exit(0);
|
|
68
77
|
}
|
|
69
|
-
|
|
78
|
+
|
|
79
|
+
if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
|
|
70
80
|
printVersion();
|
|
71
81
|
process.exit(0);
|
|
72
82
|
}
|
|
73
|
-
|
|
83
|
+
|
|
84
|
+
const command = args[0] as Command;
|
|
74
85
|
const restArgs = args.slice(1);
|
|
75
86
|
const { dir, commandArgs } = parseGlobalArgs(restArgs);
|
|
87
|
+
|
|
88
|
+
// Set TOON_DIR for scripts to use
|
|
76
89
|
process.env.TOON_DIR = dir;
|
|
90
|
+
|
|
77
91
|
if (!COMMANDS.includes(command)) {
|
|
78
92
|
console.error(`Unknown command: ${command}`);
|
|
79
93
|
console.error(`Run 'ttt help' for usage.`);
|
|
80
94
|
process.exit(1);
|
|
81
95
|
}
|
|
96
|
+
|
|
97
|
+
// Import and run the appropriate script
|
|
98
|
+
const scriptDir = new URL('../scripts/', import.meta.url).pathname;
|
|
82
99
|
try {
|
|
83
100
|
switch (command) {
|
|
84
|
-
case
|
|
85
|
-
process.argv = [
|
|
86
|
-
await import(
|
|
101
|
+
case 'init':
|
|
102
|
+
process.argv = ['node', 'init.js', ...commandArgs];
|
|
103
|
+
await import(`${scriptDir}init.js`);
|
|
87
104
|
break;
|
|
88
|
-
case
|
|
89
|
-
await import(
|
|
105
|
+
case 'sync':
|
|
106
|
+
await import(`${scriptDir}sync.js`);
|
|
90
107
|
break;
|
|
91
|
-
case
|
|
92
|
-
process.argv = [
|
|
93
|
-
await import(
|
|
108
|
+
case 'work-on':
|
|
109
|
+
process.argv = ['node', 'work-on.js', ...commandArgs];
|
|
110
|
+
await import(`${scriptDir}work-on.js`);
|
|
94
111
|
break;
|
|
95
|
-
case
|
|
96
|
-
process.argv = [
|
|
97
|
-
await import(
|
|
112
|
+
case 'done':
|
|
113
|
+
process.argv = ['node', 'done-job.js', ...commandArgs];
|
|
114
|
+
await import(`${scriptDir}done-job.js`);
|
|
98
115
|
break;
|
|
99
116
|
}
|
|
100
117
|
} catch (error) {
|
|
@@ -104,4 +121,5 @@ async function main() {
|
|
|
104
121
|
process.exit(1);
|
|
105
122
|
}
|
|
106
123
|
}
|
|
124
|
+
|
|
107
125
|
main();
|
package/package.json
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-toon-tack",
|
|
3
|
-
"version": "1.0.
|
|
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": "./
|
|
8
|
-
"team-toon-tack": "./
|
|
7
|
+
"ttt": "./bin/cli.js",
|
|
8
|
+
"team-toon-tack": "./bin/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
|
-
".": "./
|
|
12
|
-
"./utils": "./
|
|
11
|
+
".": "./scripts/utils.js",
|
|
12
|
+
"./utils": "./scripts/utils.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"
|
|
15
|
+
"bin",
|
|
16
|
+
"scripts",
|
|
16
17
|
"templates",
|
|
17
18
|
"package.json"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
|
-
"build": "
|
|
21
|
-
"prepublishOnly": "
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
22
23
|
"release": "npm config set //registry.npmjs.org/:_authToken=$NPM_PUBLISH_KEY && npm publish"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
@@ -27,8 +28,9 @@
|
|
|
27
28
|
"prompts": "^2.4.2"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@types/
|
|
31
|
-
"@types/prompts": "^2.4.9"
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"@types/prompts": "^2.4.9",
|
|
33
|
+
"typescript": "^5.0.0"
|
|
32
34
|
},
|
|
33
35
|
"keywords": [
|
|
34
36
|
"linear",
|
|
@@ -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);
|