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.
@@ -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;
@@ -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
+ }