linear-github-cli 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/.env.example +3 -0
- package/LICENSE +15 -0
- package/README.md +336 -0
- package/dist/branch-utils.js +270 -0
- package/dist/cli.js +43 -0
- package/dist/commands/commit-first.js +88 -0
- package/dist/commands/create-parent.js +160 -0
- package/dist/commands/create-sub.js +174 -0
- package/dist/github-client.js +123 -0
- package/dist/input-handler.js +190 -0
- package/dist/lgcmf.js +13 -0
- package/dist/linear-client.js +433 -0
- package/dist/validate.js +34 -0
- package/package.json +56 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commitFirst = commitFirst;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const dotenv_1 = require("dotenv");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const branch_utils_1 = require("../branch-utils");
|
|
8
|
+
const linear_client_1 = require("../linear-client");
|
|
9
|
+
// Load .env file from the project root
|
|
10
|
+
// __dirname points to dist/commands/ in compiled output, so we go up two levels
|
|
11
|
+
(0, dotenv_1.config)({ path: (0, path_1.resolve)(__dirname, '../..', '.env') });
|
|
12
|
+
/**
|
|
13
|
+
* Creates the first commit with proper message format
|
|
14
|
+
* - Extracts branch prefix (commit type) and Linear issue ID from current branch name
|
|
15
|
+
* - Fetches Linear issue title and GitHub issue number
|
|
16
|
+
* - Generates commit message: "{prefix}: {linearId} {title}" with body "solve: #{githubIssueNumber}"
|
|
17
|
+
*/
|
|
18
|
+
async function commitFirst() {
|
|
19
|
+
const linearApiKey = process.env.LINEAR_API_KEY;
|
|
20
|
+
if (!linearApiKey) {
|
|
21
|
+
console.error('ā LINEAR_API_KEY environment variable is required');
|
|
22
|
+
console.error('');
|
|
23
|
+
console.error(' Option 1: Create a .env file in the project root:');
|
|
24
|
+
console.error(' echo "LINEAR_API_KEY=lin_api_..." > .env');
|
|
25
|
+
console.error('');
|
|
26
|
+
console.error(' Option 2: Export in your shell:');
|
|
27
|
+
console.error(' export LINEAR_API_KEY="lin_api_..."');
|
|
28
|
+
console.error('');
|
|
29
|
+
console.error(' Get your API key from: https://linear.app/settings/api');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
// Step 1: Get current branch name
|
|
34
|
+
let branchName;
|
|
35
|
+
try {
|
|
36
|
+
branchName = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error('ā Error: Not in a git repository or unable to get branch name');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// Step 2: Extract branch prefix and Linear issue ID from branch name
|
|
43
|
+
const prefix = (0, branch_utils_1.extractBranchPrefix)(branchName);
|
|
44
|
+
const linearId = (0, branch_utils_1.extractLinearIssueId)(branchName);
|
|
45
|
+
if (!linearId) {
|
|
46
|
+
console.error(`ā Error: Could not extract Linear issue ID from branch name: ${branchName}`);
|
|
47
|
+
console.error(' Branch name should follow pattern: prefix/LEA-123-title');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
console.log(`š Found Linear issue ID: ${linearId}`);
|
|
51
|
+
console.log(`š Using commit type: ${prefix}`);
|
|
52
|
+
// Step 3: Initialize Linear client and fetch issue data
|
|
53
|
+
const linearClient = new linear_client_1.LinearClientWrapper(linearApiKey);
|
|
54
|
+
console.log('š Fetching Linear issue title...');
|
|
55
|
+
const title = await linearClient.getIssueTitle(linearId);
|
|
56
|
+
if (!title) {
|
|
57
|
+
console.error(`ā Error: Linear issue ${linearId} not found`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
console.log('š Fetching GitHub issue number...');
|
|
61
|
+
const githubIssueNumber = await linearClient.getGitHubIssueNumber(linearId);
|
|
62
|
+
if (!githubIssueNumber) {
|
|
63
|
+
console.error(`ā Error: GitHub issue number not found for Linear issue ${linearId}`);
|
|
64
|
+
console.error(' Make sure the Linear issue is linked to a GitHub issue');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
// Step 4: Generate commit message
|
|
68
|
+
const commitTitle = `${prefix}: ${linearId} ${title}`;
|
|
69
|
+
const commitBody = `solve: #${githubIssueNumber}`;
|
|
70
|
+
console.log('\nš Commit message:');
|
|
71
|
+
console.log(` ${commitTitle}`);
|
|
72
|
+
console.log(` ${commitBody}\n`);
|
|
73
|
+
// Step 5: Execute commit
|
|
74
|
+
try {
|
|
75
|
+
(0, child_process_1.execSync)(`git commit --allow-empty -m "${commitTitle.replace(/"/g, '\\"')}" -m "${commitBody.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
|
|
76
|
+
console.log('ā
Commit created successfully!');
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error('ā Error: Failed to create commit');
|
|
80
|
+
console.error(error instanceof Error ? error.message : error);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error('ā Error:', error instanceof Error ? error.message : error);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createParentIssue = createParentIssue;
|
|
7
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
|
+
const branch_utils_1 = require("../branch-utils");
|
|
9
|
+
const github_client_1 = require("../github-client");
|
|
10
|
+
const input_handler_1 = require("../input-handler");
|
|
11
|
+
const linear_client_1 = require("../linear-client");
|
|
12
|
+
async function createParentIssue() {
|
|
13
|
+
const linearApiKey = process.env.LINEAR_API_KEY;
|
|
14
|
+
if (!linearApiKey) {
|
|
15
|
+
console.error('ā LINEAR_API_KEY environment variable is required');
|
|
16
|
+
console.error('');
|
|
17
|
+
console.error(' Option 1: Create a .env file in the project root:');
|
|
18
|
+
console.error(' echo "LINEAR_API_KEY=lin_api_..." > .env');
|
|
19
|
+
console.error('');
|
|
20
|
+
console.error(' Option 2: Export in your shell:');
|
|
21
|
+
console.error(' export LINEAR_API_KEY="lin_api_..."');
|
|
22
|
+
console.error('');
|
|
23
|
+
console.error(' Get your API key from: https://linear.app/settings/api');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const linearClient = new linear_client_1.LinearClientWrapper(linearApiKey);
|
|
27
|
+
// Step 1: Select repository
|
|
28
|
+
console.log('š¦ Fetching repositories...');
|
|
29
|
+
const githubClient = new github_client_1.GitHubClientWrapper('');
|
|
30
|
+
const inputHandler = new input_handler_1.InputHandler(linearClient, githubClient);
|
|
31
|
+
const repo = await inputHandler.selectRepository();
|
|
32
|
+
githubClient.repo = repo;
|
|
33
|
+
// Check for unpushed commits on current branch
|
|
34
|
+
const unpushedCheck = (0, branch_utils_1.checkUnpushedCommitsOnCurrentBranch)();
|
|
35
|
+
if (unpushedCheck.hasUnpushed) {
|
|
36
|
+
console.log('\nā ļø Warning: There are unpushed commits on the current branch.');
|
|
37
|
+
console.log(` Found ${unpushedCheck.count} unpushed commit(s):`);
|
|
38
|
+
unpushedCheck.commits.forEach(commit => {
|
|
39
|
+
console.log(` - ${commit}`);
|
|
40
|
+
});
|
|
41
|
+
console.log('\n If you create a branch from this state, these commits will be included in PR body.');
|
|
42
|
+
console.log(' Please push commits first:');
|
|
43
|
+
console.log(' git push');
|
|
44
|
+
console.log('\n Then re-run this command.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log('ā No unpushed commits on current branch');
|
|
49
|
+
}
|
|
50
|
+
// Step 2: Get issue details
|
|
51
|
+
const details = await inputHandler.promptIssueDetails(repo);
|
|
52
|
+
// Step 3: Select GitHub project (optional)
|
|
53
|
+
const githubProject = await inputHandler.selectProject(repo);
|
|
54
|
+
// Step 4: Create GitHub issue
|
|
55
|
+
console.log('\nš Creating GitHub issue...');
|
|
56
|
+
const body = details.dueDate
|
|
57
|
+
? `Due Date: ${details.dueDate}\n\n${details.description}`
|
|
58
|
+
: details.description;
|
|
59
|
+
const issue = await githubClient.createIssue({
|
|
60
|
+
repo,
|
|
61
|
+
title: details.title,
|
|
62
|
+
body,
|
|
63
|
+
labels: details.labels,
|
|
64
|
+
assignees: ['@me'],
|
|
65
|
+
project: githubProject || undefined,
|
|
66
|
+
});
|
|
67
|
+
console.log(`ā
GitHub Issue #${issue.number} created: ${issue.url}`);
|
|
68
|
+
// Step 5: Wait for Linear sync, then update metadata
|
|
69
|
+
console.log('\nā³ Waiting for Linear sync (5 seconds)...');
|
|
70
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
71
|
+
const linearIssueId = await linearClient.findIssueByGitHubUrl(issue.url);
|
|
72
|
+
if (linearIssueId) {
|
|
73
|
+
console.log('ā
Found Linear issue, updating metadata...');
|
|
74
|
+
// Auto-find Linear project if GitHub project was selected
|
|
75
|
+
let linearProjectId = null;
|
|
76
|
+
if (githubProject) {
|
|
77
|
+
console.log(` Looking for Linear project matching "${githubProject}"...`);
|
|
78
|
+
linearProjectId = await linearClient.findProjectByName(githubProject);
|
|
79
|
+
if (linearProjectId) {
|
|
80
|
+
console.log(` ā
Found matching Linear project: ${githubProject}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(` ā ļø No matching Linear project found. You can set it manually.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// If no auto-match, ask user (only if GitHub project wasn't selected)
|
|
87
|
+
if (!linearProjectId && !githubProject) {
|
|
88
|
+
linearProjectId = await inputHandler.selectLinearProject();
|
|
89
|
+
}
|
|
90
|
+
// Set labels on Linear issue
|
|
91
|
+
if (details.labels && details.labels.length > 0) {
|
|
92
|
+
console.log(` Setting labels: ${details.labels.join(', ')}`);
|
|
93
|
+
const labelIds = await linearClient.setIssueLabels(linearIssueId, details.labels);
|
|
94
|
+
if (labelIds.length > 0) {
|
|
95
|
+
console.log(` ā
${labelIds.length} label(s) set on Linear issue`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log(' ā ļø Failed to set labels. You can set them manually in Linear.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Update issue metadata (due date and project, but not status)
|
|
102
|
+
const success = await linearClient.updateIssueMetadata(linearIssueId, details.dueDate || undefined, linearProjectId || undefined);
|
|
103
|
+
if (success) {
|
|
104
|
+
console.log('ā
Linear issue metadata updated!');
|
|
105
|
+
if (linearProjectId) {
|
|
106
|
+
console.log(` Project: ${githubProject || 'selected project'}`);
|
|
107
|
+
}
|
|
108
|
+
if (details.dueDate) {
|
|
109
|
+
console.log(` Due date: ${details.dueDate}`);
|
|
110
|
+
}
|
|
111
|
+
console.log(' Status: Will be updated automatically via PR integration');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log('ā ļø Failed to update Linear issue metadata. You can update it manually in Linear.');
|
|
115
|
+
}
|
|
116
|
+
// Step 6: Create branch
|
|
117
|
+
if (linearIssueId) {
|
|
118
|
+
const { createBranch } = await inquirer_1.default.prompt([
|
|
119
|
+
{
|
|
120
|
+
type: 'confirm',
|
|
121
|
+
name: 'createBranch',
|
|
122
|
+
message: 'Create git branch for this issue?',
|
|
123
|
+
default: true,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
if (createBranch) {
|
|
127
|
+
// Get Linear issue identifier (e.g., LEA-123) instead of UUID
|
|
128
|
+
const linearIssueIdentifier = await linearClient.getIssueIdentifier(linearIssueId);
|
|
129
|
+
if (!linearIssueIdentifier) {
|
|
130
|
+
console.log('ā ļø Could not get Linear issue identifier. Branch creation skipped.');
|
|
131
|
+
console.log(` Linear issue ID: ${linearIssueId}`);
|
|
132
|
+
console.log(` GitHub issue #${issue.number}`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const branchPrefix = await (0, branch_utils_1.selectBranchPrefix)(details.labels);
|
|
136
|
+
if (branchPrefix) {
|
|
137
|
+
const branchName = (0, branch_utils_1.generateBranchName)(branchPrefix, linearIssueIdentifier, details.title);
|
|
138
|
+
const success = await (0, branch_utils_1.createGitBranch)(branchName);
|
|
139
|
+
if (success) {
|
|
140
|
+
console.log(`ā
Branch created: ${branchName}`);
|
|
141
|
+
console.log(` Linear issue ID: ${linearIssueIdentifier}`);
|
|
142
|
+
console.log(` GitHub issue #${issue.number}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log('ā ļø No suitable label found for branch prefix. Branch creation skipped.');
|
|
147
|
+
console.log(` Linear issue ID: ${linearIssueIdentifier}`);
|
|
148
|
+
console.log(` GitHub issue #${issue.number}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
console.log('ā ļø Linear issue not found yet. Metadata will be set by GitHub Actions.');
|
|
156
|
+
}
|
|
157
|
+
console.log('\nš” Next steps:');
|
|
158
|
+
console.log(` Create sub-issues: lg create-sub`);
|
|
159
|
+
console.log(` Then select issue #${issue.number}`);
|
|
160
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createSubIssue = createSubIssue;
|
|
7
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
|
+
const branch_utils_1 = require("../branch-utils");
|
|
9
|
+
const github_client_1 = require("../github-client");
|
|
10
|
+
const input_handler_1 = require("../input-handler");
|
|
11
|
+
const linear_client_1 = require("../linear-client");
|
|
12
|
+
async function createSubIssue() {
|
|
13
|
+
const linearApiKey = process.env.LINEAR_API_KEY;
|
|
14
|
+
if (!linearApiKey) {
|
|
15
|
+
console.error('ā LINEAR_API_KEY environment variable is required');
|
|
16
|
+
console.error('');
|
|
17
|
+
console.error(' Option 1: Create a .env file in the project root:');
|
|
18
|
+
console.error(' echo "LINEAR_API_KEY=lin_api_..." > .env');
|
|
19
|
+
console.error('');
|
|
20
|
+
console.error(' Option 2: Export in your shell:');
|
|
21
|
+
console.error(' export LINEAR_API_KEY="lin_api_..."');
|
|
22
|
+
console.error('');
|
|
23
|
+
console.error(' Get your API key from: https://linear.app/settings/api');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const linearClient = new linear_client_1.LinearClientWrapper(linearApiKey);
|
|
27
|
+
const githubClient = new github_client_1.GitHubClientWrapper('');
|
|
28
|
+
const inputHandler = new input_handler_1.InputHandler(linearClient, githubClient);
|
|
29
|
+
// Step 1: Select repository
|
|
30
|
+
console.log('š¦ Fetching repositories...');
|
|
31
|
+
const repo = await inputHandler.selectRepository();
|
|
32
|
+
githubClient.repo = repo;
|
|
33
|
+
// Check for unpushed commits on current branch
|
|
34
|
+
const unpushedCheck = (0, branch_utils_1.checkUnpushedCommitsOnCurrentBranch)();
|
|
35
|
+
if (unpushedCheck.hasUnpushed) {
|
|
36
|
+
console.log('\nā ļø Warning: There are unpushed commits on the current branch.');
|
|
37
|
+
console.log(` Found ${unpushedCheck.count} unpushed commit(s):`);
|
|
38
|
+
unpushedCheck.commits.forEach(commit => {
|
|
39
|
+
console.log(` - ${commit}`);
|
|
40
|
+
});
|
|
41
|
+
console.log('\n If you create a branch from this state, these commits will be included in PR body.');
|
|
42
|
+
console.log(' Please push commits first:');
|
|
43
|
+
console.log(' git push');
|
|
44
|
+
console.log('\n Then re-run this command.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log('ā No unpushed commits on current branch');
|
|
49
|
+
}
|
|
50
|
+
// Step 2: Select parent issue
|
|
51
|
+
console.log('\nš Fetching issues...');
|
|
52
|
+
const parentIssueNumber = await inputHandler.selectParentIssue(repo);
|
|
53
|
+
// Step 3: Get sub-issue details
|
|
54
|
+
const details = await inputHandler.promptIssueDetails(repo);
|
|
55
|
+
// Step 4: Create sub-issue
|
|
56
|
+
console.log('\nš Creating sub-issue...');
|
|
57
|
+
const body = details.dueDate
|
|
58
|
+
? `Due Date: ${details.dueDate}\n\n${details.description}`
|
|
59
|
+
: details.description;
|
|
60
|
+
const subIssue = await githubClient.createSubIssue({
|
|
61
|
+
repo,
|
|
62
|
+
parentIssueNumber,
|
|
63
|
+
title: details.title,
|
|
64
|
+
body,
|
|
65
|
+
labels: details.labels,
|
|
66
|
+
assignees: ['@me'],
|
|
67
|
+
});
|
|
68
|
+
console.log(`ā
Sub-Issue #${subIssue.number} created: ${subIssue.url}`);
|
|
69
|
+
console.log(` Parent: #${parentIssueNumber}`);
|
|
70
|
+
// Step 5: Wait for Linear sync, then update metadata
|
|
71
|
+
console.log('\nā³ Waiting for Linear sync (5 seconds)...');
|
|
72
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
73
|
+
const linearIssueId = await linearClient.findIssueByGitHubUrl(subIssue.url);
|
|
74
|
+
if (linearIssueId) {
|
|
75
|
+
console.log('ā
Found Linear issue, updating metadata...');
|
|
76
|
+
// Get parent issue to check if it has a project
|
|
77
|
+
const parentIssueUrl = `https://github.com/${repo}/issues/${parentIssueNumber}`;
|
|
78
|
+
console.log(` Looking for parent issue's Linear project...`);
|
|
79
|
+
const parentLinearIssueId = await linearClient.findIssueByGitHubUrl(parentIssueUrl);
|
|
80
|
+
let linearProjectId = null;
|
|
81
|
+
let parentProjectName = null;
|
|
82
|
+
if (parentLinearIssueId) {
|
|
83
|
+
console.log(` ā
Found parent Linear issue: ${parentLinearIssueId}`);
|
|
84
|
+
// Try to get parent issue's project
|
|
85
|
+
const parentProject = await linearClient.getIssueProject(parentLinearIssueId);
|
|
86
|
+
if (parentProject) {
|
|
87
|
+
linearProjectId = parentProject.id;
|
|
88
|
+
parentProjectName = parentProject.name;
|
|
89
|
+
console.log(` ā
Found parent issue's project: ${parentProjectName}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(` ā ļø Parent issue has no project set`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(` ā ļø Parent Linear issue not found yet (may need more time to sync)`);
|
|
97
|
+
}
|
|
98
|
+
// If no parent project, ask user (only if no parent project found)
|
|
99
|
+
if (!linearProjectId) {
|
|
100
|
+
linearProjectId = await inputHandler.selectLinearProject();
|
|
101
|
+
}
|
|
102
|
+
// Set labels on Linear issue
|
|
103
|
+
if (details.labels && details.labels.length > 0) {
|
|
104
|
+
console.log(` Setting labels: ${details.labels.join(', ')}`);
|
|
105
|
+
const labelIds = await linearClient.setIssueLabels(linearIssueId, details.labels);
|
|
106
|
+
if (labelIds.length > 0) {
|
|
107
|
+
console.log(` ā
${labelIds.length} label(s) set on Linear issue`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.log(' ā ļø Failed to set labels. You can set them manually in Linear.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Update issue metadata (due date and project, but not status)
|
|
114
|
+
const success = await linearClient.updateIssueMetadata(linearIssueId, details.dueDate || undefined, linearProjectId || undefined);
|
|
115
|
+
if (success) {
|
|
116
|
+
console.log('ā
Linear issue metadata updated!');
|
|
117
|
+
if (linearProjectId) {
|
|
118
|
+
if (parentProjectName) {
|
|
119
|
+
console.log(` Project: ${parentProjectName} (inherited from parent)`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(` Project: linked`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (details.dueDate) {
|
|
126
|
+
console.log(` Due date: ${details.dueDate}`);
|
|
127
|
+
}
|
|
128
|
+
console.log(' Status: Will be updated automatically via PR integration');
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log('ā ļø Failed to update Linear issue metadata. You can update it manually in Linear.');
|
|
132
|
+
}
|
|
133
|
+
// Step 6: Create branch
|
|
134
|
+
if (linearIssueId) {
|
|
135
|
+
const { createBranch } = await inquirer_1.default.prompt([
|
|
136
|
+
{
|
|
137
|
+
type: 'confirm',
|
|
138
|
+
name: 'createBranch',
|
|
139
|
+
message: 'Create git branch for this issue?',
|
|
140
|
+
default: true,
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
if (createBranch) {
|
|
144
|
+
// Get Linear issue identifier (e.g., LEA-123) instead of UUID
|
|
145
|
+
const linearIssueIdentifier = await linearClient.getIssueIdentifier(linearIssueId);
|
|
146
|
+
if (!linearIssueIdentifier) {
|
|
147
|
+
console.log('ā ļø Could not get Linear issue identifier. Branch creation skipped.');
|
|
148
|
+
console.log(` Linear issue ID: ${linearIssueId}`);
|
|
149
|
+
console.log(` GitHub issue #${subIssue.number}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const branchPrefix = await (0, branch_utils_1.selectBranchPrefix)(details.labels);
|
|
153
|
+
if (branchPrefix) {
|
|
154
|
+
const branchName = (0, branch_utils_1.generateBranchName)(branchPrefix, linearIssueIdentifier, details.title);
|
|
155
|
+
const success = await (0, branch_utils_1.createGitBranch)(branchName);
|
|
156
|
+
if (success) {
|
|
157
|
+
console.log(`ā
Branch created: ${branchName}`);
|
|
158
|
+
console.log(` Linear issue ID: ${linearIssueIdentifier}`);
|
|
159
|
+
console.log(` GitHub issue #${subIssue.number}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log('ā ļø No suitable label found for branch prefix. Branch creation skipped.');
|
|
164
|
+
console.log(` Linear issue ID: ${linearIssueIdentifier}`);
|
|
165
|
+
console.log(` GitHub issue #${subIssue.number}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log('ā ļø Linear issue not found yet. Metadata will be set by GitHub Actions.');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GitHubClientWrapper = void 0;
|
|
4
|
+
const rest_1 = require("@octokit/rest");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
class GitHubClientWrapper {
|
|
7
|
+
octokit;
|
|
8
|
+
repo;
|
|
9
|
+
constructor(repo, token) {
|
|
10
|
+
this.repo = repo;
|
|
11
|
+
if (token) {
|
|
12
|
+
this.octokit = new rest_1.Octokit({ auth: token });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async getRepositories() {
|
|
16
|
+
// Use gh CLI to get accessible repos
|
|
17
|
+
const output = (0, child_process_1.execSync)('gh repo list --limit 100 --json nameWithOwner', {
|
|
18
|
+
encoding: 'utf-8',
|
|
19
|
+
});
|
|
20
|
+
const repos = JSON.parse(output);
|
|
21
|
+
return repos.map((r) => {
|
|
22
|
+
const [owner, name] = r.nameWithOwner.split('/');
|
|
23
|
+
return { owner, name, fullName: r.nameWithOwner };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async getProjects(repo) {
|
|
27
|
+
// Extract owner from repo (format: owner/repo)
|
|
28
|
+
const [owner] = repo.split('/');
|
|
29
|
+
// Use gh CLI to get projects - note: gh project list uses --owner, not --repo
|
|
30
|
+
// Output format: {"projects": [...], "totalCount": N}
|
|
31
|
+
const output = (0, child_process_1.execSync)(`gh project list --owner ${owner} --limit 50 --format json`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
32
|
+
const result = JSON.parse(output);
|
|
33
|
+
// Handle both array format and object format
|
|
34
|
+
const projects = Array.isArray(result) ? result : (result.projects || []);
|
|
35
|
+
return projects.map((p) => ({
|
|
36
|
+
id: p.number.toString(),
|
|
37
|
+
name: p.title,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
async createIssue(params) {
|
|
41
|
+
// gh issue create doesn't support --json flag, so we parse the URL from output
|
|
42
|
+
// Output format: "https://github.com/owner/repo/issues/123"
|
|
43
|
+
const command = `gh issue create --repo ${params.repo} ` +
|
|
44
|
+
`--title "${params.title.replace(/"/g, '\\"')}" ` +
|
|
45
|
+
`--body "${params.body.replace(/"/g, '\\"')}" ` +
|
|
46
|
+
(params.labels && params.labels.length > 0 ? `--label "${params.labels.join(',')}" ` : '') +
|
|
47
|
+
(params.assignees && params.assignees.length > 0 ? `--assignee "${params.assignees.join(',')}" ` : '') +
|
|
48
|
+
(params.project ? `--project "${params.project}" ` : '');
|
|
49
|
+
const output = (0, child_process_1.execSync)(command, { encoding: 'utf-8' });
|
|
50
|
+
// Parse URL from output: "https://github.com/owner/repo/issues/123"
|
|
51
|
+
const urlMatch = output.match(/https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/);
|
|
52
|
+
if (!urlMatch) {
|
|
53
|
+
throw new Error(`Failed to parse issue URL from output: ${output}`);
|
|
54
|
+
}
|
|
55
|
+
const issueNumber = parseInt(urlMatch[1], 10);
|
|
56
|
+
const issueUrl = urlMatch[0];
|
|
57
|
+
// Get issue ID using GraphQL API
|
|
58
|
+
const issueId = await this.getIssueIdByNumber(params.repo, issueNumber);
|
|
59
|
+
return {
|
|
60
|
+
number: issueNumber,
|
|
61
|
+
url: issueUrl,
|
|
62
|
+
id: issueId,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async createSubIssue(params) {
|
|
66
|
+
// First create the issue
|
|
67
|
+
const issue = await this.createIssue({
|
|
68
|
+
repo: params.repo,
|
|
69
|
+
title: params.title,
|
|
70
|
+
body: params.body,
|
|
71
|
+
labels: params.labels,
|
|
72
|
+
assignees: params.assignees,
|
|
73
|
+
});
|
|
74
|
+
// Then set parent-child relationship via GraphQL
|
|
75
|
+
const parentIssue = await this.getIssueById(params.repo, params.parentIssueNumber);
|
|
76
|
+
(0, child_process_1.execSync)(`gh api graphql -H "GraphQL-Features: sub_issues" -f query="
|
|
77
|
+
mutation {
|
|
78
|
+
addSubIssue(input: {
|
|
79
|
+
issueId: \\\"${parentIssue.id}\\\",
|
|
80
|
+
subIssueId: \\\"${issue.id}\\\"
|
|
81
|
+
}) {
|
|
82
|
+
issue { title }
|
|
83
|
+
subIssue { title }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
"`, { encoding: 'utf-8' });
|
|
87
|
+
return issue;
|
|
88
|
+
}
|
|
89
|
+
async getIssueIdByNumber(repo, issueNumber) {
|
|
90
|
+
// Use GraphQL API to get issue ID
|
|
91
|
+
const [owner, name] = repo.split('/');
|
|
92
|
+
const query = `query { repository(owner: "${owner}", name: "${name}") { issue(number: ${issueNumber}) { id } } }`;
|
|
93
|
+
const output = (0, child_process_1.execSync)(`gh api graphql -f query="${query.replace(/"/g, '\\"')}"`, { encoding: 'utf-8' });
|
|
94
|
+
const result = JSON.parse(output);
|
|
95
|
+
return result.data.repository.issue.id;
|
|
96
|
+
}
|
|
97
|
+
async getIssueById(repo, issueNumber) {
|
|
98
|
+
const id = await this.getIssueIdByNumber(repo, issueNumber);
|
|
99
|
+
return { id };
|
|
100
|
+
}
|
|
101
|
+
async getLabels(repo) {
|
|
102
|
+
try {
|
|
103
|
+
// Use gh CLI to get labels from repository
|
|
104
|
+
const output = (0, child_process_1.execSync)(`gh label list --repo ${repo} --limit 100 --json name`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
105
|
+
const labels = JSON.parse(output);
|
|
106
|
+
return labels.map((l) => l.name).sort();
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
// Fallback to standard labels if API call fails
|
|
110
|
+
console.error('ā ļø Failed to fetch labels from GitHub, using default labels');
|
|
111
|
+
return [
|
|
112
|
+
'feat',
|
|
113
|
+
'fix',
|
|
114
|
+
'chore',
|
|
115
|
+
'docs',
|
|
116
|
+
'refactor',
|
|
117
|
+
'test',
|
|
118
|
+
'research',
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.GitHubClientWrapper = GitHubClientWrapper;
|