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,190 @@
|
|
|
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.InputHandler = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
9
|
+
class InputHandler {
|
|
10
|
+
linearClient;
|
|
11
|
+
githubClient;
|
|
12
|
+
constructor(linearClient, githubClient) {
|
|
13
|
+
this.linearClient = linearClient;
|
|
14
|
+
this.githubClient = githubClient;
|
|
15
|
+
}
|
|
16
|
+
async selectRepository() {
|
|
17
|
+
const repos = await this.githubClient.getRepositories();
|
|
18
|
+
const { repo } = await inquirer_1.default.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: 'list',
|
|
21
|
+
name: 'repo',
|
|
22
|
+
message: 'Select repository:',
|
|
23
|
+
choices: repos.map(r => ({
|
|
24
|
+
name: `${r.fullName}`,
|
|
25
|
+
value: r.fullName,
|
|
26
|
+
})),
|
|
27
|
+
pageSize: 20,
|
|
28
|
+
},
|
|
29
|
+
]);
|
|
30
|
+
return repo;
|
|
31
|
+
}
|
|
32
|
+
async selectProject(repo) {
|
|
33
|
+
try {
|
|
34
|
+
const projects = await this.githubClient.getProjects(repo);
|
|
35
|
+
if (projects.length === 0) {
|
|
36
|
+
// No projects available
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const { project } = await inquirer_1.default.prompt([
|
|
40
|
+
{
|
|
41
|
+
type: 'list',
|
|
42
|
+
name: 'project',
|
|
43
|
+
message: 'Select GitHub Project (or skip):',
|
|
44
|
+
choices: [
|
|
45
|
+
{ name: 'Skip', value: null },
|
|
46
|
+
...projects.map(p => ({ name: p.name, value: p.name })),
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
return project;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const errorMessage = error.message || error.stderr || String(error);
|
|
54
|
+
// Check if it's an authentication error
|
|
55
|
+
if (errorMessage.includes('authentication token') || errorMessage.includes('required scopes')) {
|
|
56
|
+
console.error('\n❌ GitHub authentication token missing required scopes for projects.');
|
|
57
|
+
console.error(' Required scope: read:project');
|
|
58
|
+
console.error('\n To fix:');
|
|
59
|
+
console.error(' Run: gh auth refresh -s read:project\n');
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.error('\n❌ Failed to fetch GitHub projects.');
|
|
63
|
+
console.error(` Error: ${errorMessage}\n`);
|
|
64
|
+
}
|
|
65
|
+
// Ask user if they want to proceed without selecting a project
|
|
66
|
+
const { proceed } = await inquirer_1.default.prompt([
|
|
67
|
+
{
|
|
68
|
+
type: 'confirm',
|
|
69
|
+
name: 'proceed',
|
|
70
|
+
message: 'Do you want to continue without selecting a project?',
|
|
71
|
+
default: true,
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
if (!proceed) {
|
|
75
|
+
console.log('Cancelled. Please fix the issue and try again.');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async selectLinearProject() {
|
|
82
|
+
const projects = await this.linearClient.getProjects();
|
|
83
|
+
if (projects.length === 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const { project } = await inquirer_1.default.prompt([
|
|
87
|
+
{
|
|
88
|
+
type: 'list',
|
|
89
|
+
name: 'project',
|
|
90
|
+
message: 'Select Linear Project (or skip):',
|
|
91
|
+
choices: [
|
|
92
|
+
{ name: 'Skip', value: null },
|
|
93
|
+
...projects.map(p => ({ name: p.name, value: p.id })),
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
return project;
|
|
98
|
+
}
|
|
99
|
+
async selectParentIssue(repo) {
|
|
100
|
+
// Fetch recent issues
|
|
101
|
+
const output = (0, child_process_1.execSync)(`gh issue list --repo ${repo} --limit 50 --json number,title,state`, { encoding: 'utf-8' });
|
|
102
|
+
const issues = JSON.parse(output);
|
|
103
|
+
const { issueNumber } = await inquirer_1.default.prompt([
|
|
104
|
+
{
|
|
105
|
+
type: 'list',
|
|
106
|
+
name: 'issueNumber',
|
|
107
|
+
message: 'Select parent issue:',
|
|
108
|
+
choices: issues.map((i) => ({
|
|
109
|
+
name: `#${i.number}: ${i.title} [${i.state}]`,
|
|
110
|
+
value: i.number,
|
|
111
|
+
})),
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
return issueNumber;
|
|
115
|
+
}
|
|
116
|
+
async promptIssueDetails(repo) {
|
|
117
|
+
// Fetch labels from GitHub repository if repo is provided
|
|
118
|
+
let labelChoices = [];
|
|
119
|
+
if (repo) {
|
|
120
|
+
try {
|
|
121
|
+
console.log('📋 Fetching labels from GitHub...');
|
|
122
|
+
labelChoices = await this.githubClient.getLabels(repo);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Fallback to default labels if fetch fails
|
|
126
|
+
labelChoices = [
|
|
127
|
+
'feat',
|
|
128
|
+
'fix',
|
|
129
|
+
'chore',
|
|
130
|
+
'docs',
|
|
131
|
+
'refactor',
|
|
132
|
+
'test',
|
|
133
|
+
'research',
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Fallback to default labels if no repo provided
|
|
139
|
+
labelChoices = [
|
|
140
|
+
'feat',
|
|
141
|
+
'fix',
|
|
142
|
+
'chore',
|
|
143
|
+
'docs',
|
|
144
|
+
'refactor',
|
|
145
|
+
'test',
|
|
146
|
+
'research',
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
const answers = await inquirer_1.default.prompt([
|
|
150
|
+
{
|
|
151
|
+
type: 'input',
|
|
152
|
+
name: 'title',
|
|
153
|
+
message: 'Issue title:',
|
|
154
|
+
validate: (input) => input.length > 0 || 'Title is required',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: 'editor',
|
|
158
|
+
name: 'description',
|
|
159
|
+
message: 'Issue description (opens editor):',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'input',
|
|
163
|
+
name: 'dueDate',
|
|
164
|
+
message: 'Due date (YYYY-MM-DD):',
|
|
165
|
+
validate: (input) => {
|
|
166
|
+
if (!input)
|
|
167
|
+
return true; // Optional
|
|
168
|
+
const date = new Date(input);
|
|
169
|
+
return !isNaN(date.getTime()) || 'Invalid date format';
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'checkbox',
|
|
174
|
+
name: 'labels',
|
|
175
|
+
message: 'Select GitHub labels (at least one required):',
|
|
176
|
+
choices: labelChoices,
|
|
177
|
+
validate: (input) => {
|
|
178
|
+
return input.length > 0 || 'At least one label is required';
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
return {
|
|
183
|
+
title: answers.title,
|
|
184
|
+
description: answers.description || '',
|
|
185
|
+
dueDate: answers.dueDate || '',
|
|
186
|
+
labels: answers.labels || [],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.InputHandler = InputHandler;
|
package/dist/lgcmf.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const dotenv_1 = require("dotenv");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const commit_first_1 = require("./commands/commit-first");
|
|
7
|
+
// Load .env file from the project root
|
|
8
|
+
// __dirname points to dist/ in compiled output, so we go up one level
|
|
9
|
+
(0, dotenv_1.config)({ path: (0, path_1.resolve)(__dirname, '..', '.env') });
|
|
10
|
+
(0, commit_first_1.commitFirst)().catch((error) => {
|
|
11
|
+
console.error('❌ Error:', error instanceof Error ? error.message : error);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LinearClientWrapper = void 0;
|
|
4
|
+
const sdk_1 = require("@linear/sdk");
|
|
5
|
+
// Store API key for direct GraphQL calls
|
|
6
|
+
let linearApiKey = '';
|
|
7
|
+
class LinearClientWrapper {
|
|
8
|
+
client;
|
|
9
|
+
constructor(apiKey) {
|
|
10
|
+
linearApiKey = apiKey;
|
|
11
|
+
this.client = new sdk_1.LinearClient({ apiKey });
|
|
12
|
+
}
|
|
13
|
+
async getProjects() {
|
|
14
|
+
const projects = await this.client.projects();
|
|
15
|
+
return projects.nodes.map(p => ({ id: p.id, name: p.name }));
|
|
16
|
+
}
|
|
17
|
+
async findProjectByName(projectName) {
|
|
18
|
+
try {
|
|
19
|
+
const projects = await this.client.projects({
|
|
20
|
+
filter: { name: { eq: projectName } }
|
|
21
|
+
});
|
|
22
|
+
return projects.nodes[0]?.id || null;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// Fallback: get all projects and search manually
|
|
26
|
+
const allProjects = await this.getProjects();
|
|
27
|
+
const project = allProjects.find(p => p.name === projectName);
|
|
28
|
+
return project?.id || null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async getWorkflowStates(teamId) {
|
|
32
|
+
try {
|
|
33
|
+
// Get workflow states from teams
|
|
34
|
+
let teamList;
|
|
35
|
+
if (teamId) {
|
|
36
|
+
// If a teamId is provided, fetch the single team and wrap in an array
|
|
37
|
+
const singleTeam = await this.client.team(teamId);
|
|
38
|
+
teamList = [singleTeam];
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Otherwise, fetch all teams and use the nodes property from TeamConnection
|
|
42
|
+
const teamsConnection = await this.client.teams();
|
|
43
|
+
teamList = teamsConnection.nodes;
|
|
44
|
+
}
|
|
45
|
+
if (!teamList || teamList.length === 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
// Get workflow states from the first team
|
|
49
|
+
const team = Array.isArray(teamList) ? teamList[0] : teamList;
|
|
50
|
+
if (!team) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
// Use GraphQL to get workflow states
|
|
54
|
+
const query = `query GetWorkflowStates($teamId: String!) {
|
|
55
|
+
team(id: $teamId) {
|
|
56
|
+
states {
|
|
57
|
+
nodes {
|
|
58
|
+
id
|
|
59
|
+
name
|
|
60
|
+
type
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}`;
|
|
65
|
+
const teamIdValue = typeof team === 'object' && 'id' in team ? team.id : teamId || '';
|
|
66
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'Authorization': linearApiKey,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
query,
|
|
74
|
+
variables: { teamId: teamIdValue },
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const result = await response.json();
|
|
78
|
+
if (result.errors || !result.data?.team?.states) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return result.data.team.states.nodes.map((s) => ({
|
|
82
|
+
id: s.id,
|
|
83
|
+
name: s.name,
|
|
84
|
+
type: s.type,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error('Error fetching workflow states:', error);
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async findOrCreateLabel(teamId, labelName) {
|
|
93
|
+
try {
|
|
94
|
+
// First, try to find existing label
|
|
95
|
+
const labels = await this.getLabels(teamId);
|
|
96
|
+
const existingLabel = labels.find(l => l.name.toLowerCase() === labelName.toLowerCase());
|
|
97
|
+
if (existingLabel) {
|
|
98
|
+
return existingLabel.id;
|
|
99
|
+
}
|
|
100
|
+
// If not found, create a new label
|
|
101
|
+
const mutation = `mutation CreateLabel($teamId: String!, $name: String!) {
|
|
102
|
+
issueLabelCreate(input: {
|
|
103
|
+
teamId: $teamId
|
|
104
|
+
name: $name
|
|
105
|
+
}) {
|
|
106
|
+
success
|
|
107
|
+
issueLabel { id }
|
|
108
|
+
}
|
|
109
|
+
}`;
|
|
110
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
'Authorization': linearApiKey,
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
query: mutation,
|
|
118
|
+
variables: { teamId, name: labelName },
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
const result = await response.json();
|
|
122
|
+
if (result.errors || !result.data?.issueLabelCreate?.success) {
|
|
123
|
+
console.error(`❌ Failed to create label "${labelName}":`, result.errors);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return result.data.issueLabelCreate.issueLabel.id;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error(`❌ Error finding/creating label "${labelName}":`, error);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async getIssueTeamId(issueId) {
|
|
134
|
+
try {
|
|
135
|
+
const issue = await this.client.issue(issueId);
|
|
136
|
+
const team = await issue.team;
|
|
137
|
+
return team?.id || null;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error('❌ Error getting issue team:', error);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async getIssueProjectId(issueId) {
|
|
145
|
+
try {
|
|
146
|
+
const issue = await this.client.issue(issueId);
|
|
147
|
+
const project = await issue.project;
|
|
148
|
+
return project?.id || null;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// Silently fail - project might not be set
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async getIssueProject(issueId) {
|
|
156
|
+
try {
|
|
157
|
+
const issue = await this.client.issue(issueId);
|
|
158
|
+
const project = await issue.project;
|
|
159
|
+
if (project) {
|
|
160
|
+
const name = await project.name;
|
|
161
|
+
return { id: project.id, name };
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
// Silently fail - project might not be set
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async getTeams() {
|
|
171
|
+
const teams = await this.client.teams();
|
|
172
|
+
return teams.nodes.map(t => ({ id: t.id, name: t.name, key: t.key }));
|
|
173
|
+
}
|
|
174
|
+
async getLabels(teamId) {
|
|
175
|
+
const team = await this.client.team(teamId);
|
|
176
|
+
const labels = await team?.labels();
|
|
177
|
+
return labels?.nodes.map(l => ({ id: l.id, name: l.name })) || [];
|
|
178
|
+
}
|
|
179
|
+
async findIssueByGitHubUrl(githubUrl) {
|
|
180
|
+
const issues = await this.client.issues({
|
|
181
|
+
filter: {
|
|
182
|
+
attachments: { url: { contains: githubUrl } }
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return issues.nodes[0]?.id || null;
|
|
186
|
+
}
|
|
187
|
+
async getIssueIdentifier(issueId) {
|
|
188
|
+
try {
|
|
189
|
+
const issue = await this.client.issue(issueId);
|
|
190
|
+
if (!issue) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const identifier = await issue.identifier;
|
|
194
|
+
return identifier || null;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error('❌ Error getting Linear issue identifier:', error instanceof Error ? error.message : error);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Finds Linear issue by identifier (e.g., 'LEA-123') and returns its ID
|
|
203
|
+
* Uses GraphQL query to find issue by identifier
|
|
204
|
+
* @param identifier - Linear issue identifier (e.g., 'LEA-123')
|
|
205
|
+
* @returns Linear issue ID or null if not found
|
|
206
|
+
*/
|
|
207
|
+
async findIssueByIdentifier(identifier) {
|
|
208
|
+
try {
|
|
209
|
+
// Extract team key and number from identifier (e.g., 'LEA-123' -> team: 'LEA', number: 123)
|
|
210
|
+
const match = identifier.match(/([A-Z]+)-(\d+)/);
|
|
211
|
+
if (!match) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const teamKey = match[1];
|
|
215
|
+
const issueNumber = parseInt(match[2], 10);
|
|
216
|
+
// Get team ID first
|
|
217
|
+
const teamQuery = `query GetTeam($key: String!) {
|
|
218
|
+
teams(filter: { key: { eq: $key } }) {
|
|
219
|
+
nodes {
|
|
220
|
+
id
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}`;
|
|
224
|
+
const teamResponse = await fetch('https://api.linear.app/graphql', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: {
|
|
227
|
+
'Content-Type': 'application/json',
|
|
228
|
+
'Authorization': linearApiKey,
|
|
229
|
+
},
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
query: teamQuery,
|
|
232
|
+
variables: { key: teamKey },
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
const teamResult = await teamResponse.json();
|
|
236
|
+
if (teamResult.errors || !teamResult.data?.teams?.nodes?.length) {
|
|
237
|
+
console.error(`❌ Team '${teamKey}' not found`);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const teamId = teamResult.data.teams.nodes[0].id;
|
|
241
|
+
// Now query issue by team ID and number
|
|
242
|
+
const issueQuery = `query GetIssue($teamId: ID!, $number: Float!) {
|
|
243
|
+
issues(
|
|
244
|
+
filter: {
|
|
245
|
+
team: { id: { eq: $teamId } }
|
|
246
|
+
number: { eq: $number }
|
|
247
|
+
}
|
|
248
|
+
) {
|
|
249
|
+
nodes {
|
|
250
|
+
id
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}`;
|
|
254
|
+
const issueResponse = await fetch('https://api.linear.app/graphql', {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
'Content-Type': 'application/json',
|
|
258
|
+
'Authorization': linearApiKey,
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
query: issueQuery,
|
|
262
|
+
variables: { teamId, number: issueNumber },
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
const issueResult = await issueResponse.json();
|
|
266
|
+
if (issueResult.errors) {
|
|
267
|
+
console.error('❌ Linear GraphQL errors:', JSON.stringify(issueResult.errors, null, 2));
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
if (!issueResult.data?.issues?.nodes?.length) {
|
|
271
|
+
console.error(`❌ Issue ${identifier} not found`);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return issueResult.data.issues.nodes[0].id || null;
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
console.error('❌ Error finding Linear issue by identifier:', error instanceof Error ? error.message : error);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Gets Linear issue title by identifier (e.g., 'LEA-123')
|
|
283
|
+
* @param identifier - Linear issue identifier (e.g., 'LEA-123')
|
|
284
|
+
* @returns Issue title or null if not found
|
|
285
|
+
*/
|
|
286
|
+
async getIssueTitle(identifier) {
|
|
287
|
+
try {
|
|
288
|
+
const issueId = await this.findIssueByIdentifier(identifier);
|
|
289
|
+
if (!issueId) {
|
|
290
|
+
// Error message already logged in findIssueByIdentifier
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const issue = await this.client.issue(issueId);
|
|
294
|
+
if (!issue) {
|
|
295
|
+
console.error(`❌ Linear issue ${identifier} (ID: ${issueId}) not found after lookup`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const title = await issue.title;
|
|
299
|
+
if (!title) {
|
|
300
|
+
console.error(`❌ Linear issue ${identifier} has no title`);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return title;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
console.error('❌ Error getting Linear issue title:', error instanceof Error ? error.message : error);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Gets GitHub issue number from Linear issue attachments
|
|
312
|
+
* Linear SDK/APIにはGitHub issue番号を直接取得する専用フィールドはないため、
|
|
313
|
+
* attachmentsからGitHub issue URLを取得してパースする必要がある
|
|
314
|
+
* @param identifier - Linear issue identifier (e.g., 'LEA-123')
|
|
315
|
+
* @returns GitHub issue number or null if not found
|
|
316
|
+
*/
|
|
317
|
+
async getGitHubIssueNumber(identifier) {
|
|
318
|
+
try {
|
|
319
|
+
const issueId = await this.findIssueByIdentifier(identifier);
|
|
320
|
+
if (!issueId) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const issue = await this.client.issue(issueId);
|
|
324
|
+
if (!issue) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
// Get attachments from the issue
|
|
328
|
+
const attachments = await issue.attachments();
|
|
329
|
+
// Look for GitHub issue URL in attachments
|
|
330
|
+
// Pattern: https://github.com/owner/repo/issues/123
|
|
331
|
+
const githubIssueUrlPattern = /https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/;
|
|
332
|
+
for (const attachment of attachments.nodes) {
|
|
333
|
+
const url = await attachment.url;
|
|
334
|
+
if (url) {
|
|
335
|
+
const match = url.match(githubIssueUrlPattern);
|
|
336
|
+
if (match && match[1]) {
|
|
337
|
+
return parseInt(match[1], 10);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
console.error('❌ Error getting GitHub issue number:', error instanceof Error ? error.message : error);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async updateIssueMetadata(issueId, dueDate, projectId, labelIds) {
|
|
349
|
+
try {
|
|
350
|
+
// Get the issue first to verify it exists
|
|
351
|
+
const issue = await this.client.issue(issueId);
|
|
352
|
+
if (!issue) {
|
|
353
|
+
console.error('❌ Linear issue not found');
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
// Build update input
|
|
357
|
+
const updateInput = {};
|
|
358
|
+
if (dueDate) {
|
|
359
|
+
updateInput.dueDate = new Date(dueDate);
|
|
360
|
+
}
|
|
361
|
+
if (projectId) {
|
|
362
|
+
updateInput.projectId = projectId;
|
|
363
|
+
}
|
|
364
|
+
if (labelIds && labelIds.length > 0) {
|
|
365
|
+
updateInput.labelIds = labelIds;
|
|
366
|
+
}
|
|
367
|
+
// Use GraphQL API directly via fetch
|
|
368
|
+
const mutation = `mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
369
|
+
issueUpdate(id: $id, input: $input) {
|
|
370
|
+
success
|
|
371
|
+
issue { id }
|
|
372
|
+
}
|
|
373
|
+
}`;
|
|
374
|
+
const variables = {
|
|
375
|
+
id: issueId,
|
|
376
|
+
input: updateInput,
|
|
377
|
+
};
|
|
378
|
+
// Use Linear's GraphQL endpoint directly
|
|
379
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: {
|
|
382
|
+
'Content-Type': 'application/json',
|
|
383
|
+
'Authorization': linearApiKey,
|
|
384
|
+
},
|
|
385
|
+
body: JSON.stringify({
|
|
386
|
+
query: mutation,
|
|
387
|
+
variables,
|
|
388
|
+
}),
|
|
389
|
+
});
|
|
390
|
+
const result = await response.json();
|
|
391
|
+
if (result.errors) {
|
|
392
|
+
console.error('❌ Linear API error:', result.errors);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return result.data?.issueUpdate?.success || false;
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
console.error('❌ Error updating Linear issue:', error instanceof Error ? error.message : error);
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async setIssueLabels(issueId, labelNames) {
|
|
403
|
+
try {
|
|
404
|
+
// Get team ID from issue
|
|
405
|
+
const teamId = await this.getIssueTeamId(issueId);
|
|
406
|
+
if (!teamId) {
|
|
407
|
+
console.error('❌ Could not determine team for issue');
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
// Find or create labels and collect their IDs
|
|
411
|
+
const labelIds = [];
|
|
412
|
+
for (const labelName of labelNames) {
|
|
413
|
+
const labelId = await this.findOrCreateLabel(teamId, labelName);
|
|
414
|
+
if (labelId) {
|
|
415
|
+
labelIds.push(labelId);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Update issue with labels
|
|
419
|
+
if (labelIds.length > 0) {
|
|
420
|
+
const success = await this.updateIssueMetadata(issueId, undefined, undefined, labelIds);
|
|
421
|
+
if (success) {
|
|
422
|
+
return labelIds;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
console.error('❌ Error setting issue labels:', error instanceof Error ? error.message : error);
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
exports.LinearClientWrapper = LinearClientWrapper;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Validation script to check code structure and imports
|
|
5
|
+
* Run with: tsx validate.ts
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
const linear_client_1 = require("./linear-client");
|
|
9
|
+
const github_client_1 = require("./github-client");
|
|
10
|
+
const input_handler_1 = require("./input-handler");
|
|
11
|
+
console.log('✅ All imports successful!');
|
|
12
|
+
// Test class instantiation (without API calls)
|
|
13
|
+
console.log('\n📦 Testing class structure...');
|
|
14
|
+
try {
|
|
15
|
+
// Test LinearClientWrapper structure
|
|
16
|
+
const linearClient = new linear_client_1.LinearClientWrapper('test-key');
|
|
17
|
+
console.log('✅ LinearClientWrapper can be instantiated');
|
|
18
|
+
// Test GitHubClientWrapper structure
|
|
19
|
+
const githubClient = new github_client_1.GitHubClientWrapper('test/repo');
|
|
20
|
+
console.log('✅ GitHubClientWrapper can be instantiated');
|
|
21
|
+
// Test InputHandler structure
|
|
22
|
+
const inputHandler = new input_handler_1.InputHandler(linearClient, githubClient);
|
|
23
|
+
console.log('✅ InputHandler can be instantiated');
|
|
24
|
+
console.log('\n✅ All classes are properly structured!');
|
|
25
|
+
console.log('\n📋 Next steps:');
|
|
26
|
+
console.log(' 1. Install dependencies: npm install');
|
|
27
|
+
console.log(' 2. Set LINEAR_API_KEY environment variable');
|
|
28
|
+
console.log(' 3. Authenticate GitHub CLI: gh auth login');
|
|
29
|
+
console.log(' 4. Test with: tsx create-parent-issue.ts');
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error('❌ Error:', error);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|