linear-github-cli 1.1.5 → 1.3.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.
@@ -68,9 +68,19 @@ async function createParentIssue() {
68
68
  await githubClient.setProjectDateFields(repo, githubProject, issue.id, details.dueDate || undefined, details.startDate || undefined);
69
69
  }
70
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(issue.url);
71
+ const linearSyncDelayMs = 500;
72
+ const linearSyncMaxWaitMs = 10000;
73
+ const linearSyncMaxAttempts = Math.floor(linearSyncMaxWaitMs / linearSyncDelayMs) + 1;
74
+ console.log('\n⏳ Waiting for Linear sync (polling for up to 10s)...');
75
+ const linearIssueId = await linearClient.waitForIssueByGitHubUrl(issue.url, {
76
+ maxAttempts: linearSyncMaxAttempts,
77
+ delayMs: linearSyncDelayMs,
78
+ onRetry: (attempt, maxAttempts, delayMs) => {
79
+ if (attempt % 5 === 0) {
80
+ console.log(` ⏳ Linear issue not found yet, retrying in ${delayMs}ms... (${attempt}/${maxAttempts - 1})`);
81
+ }
82
+ },
83
+ });
74
84
  if (linearIssueId) {
75
85
  console.log('✅ Found Linear issue, updating metadata...');
76
86
  // Auto-find Linear project if GitHub project was selected
@@ -65,15 +65,33 @@ async function createSubIssue() {
65
65
  console.log(`✅ Sub-Issue #${subIssue.number} created: ${subIssue.url}`);
66
66
  console.log(` Parent: #${parentIssueNumber}`);
67
67
  // Step 5: Wait for Linear sync, then update metadata
68
- console.log('\n⏳ Waiting for Linear sync (5 seconds)...');
69
- await new Promise(resolve => setTimeout(resolve, 5000));
70
- const linearIssueId = await linearClient.findIssueByGitHubUrl(subIssue.url);
68
+ const linearSyncDelayMs = 500;
69
+ const linearSyncMaxWaitMs = 10000;
70
+ const linearSyncMaxAttempts = Math.floor(linearSyncMaxWaitMs / linearSyncDelayMs) + 1;
71
+ console.log('\n⏳ Waiting for Linear sync (polling for up to 10s)...');
72
+ const linearIssueId = await linearClient.waitForIssueByGitHubUrl(subIssue.url, {
73
+ maxAttempts: linearSyncMaxAttempts,
74
+ delayMs: linearSyncDelayMs,
75
+ onRetry: (attempt, maxAttempts, delayMs) => {
76
+ if (attempt % 5 === 0) {
77
+ console.log(` ⏳ Linear issue not found yet, retrying in ${delayMs}ms... (${attempt}/${maxAttempts - 1})`);
78
+ }
79
+ },
80
+ });
71
81
  if (linearIssueId) {
72
82
  console.log('✅ Found Linear issue, updating metadata...');
73
83
  // Get parent issue to check if it has a project
74
84
  const parentIssueUrl = `https://github.com/${repo}/issues/${parentIssueNumber}`;
75
85
  console.log(` Looking for parent issue's Linear project...`);
76
- const parentLinearIssueId = await linearClient.findIssueByGitHubUrl(parentIssueUrl);
86
+ const parentLinearIssueId = await linearClient.waitForIssueByGitHubUrl(parentIssueUrl, {
87
+ maxAttempts: 6,
88
+ delayMs: 500,
89
+ onRetry: (attempt, maxAttempts, delayMs) => {
90
+ if (attempt % 3 === 0) {
91
+ console.log(` ⏳ Parent Linear issue not found yet, retrying in ${delayMs}ms... (${attempt}/${maxAttempts - 1})`);
92
+ }
93
+ },
94
+ });
77
95
  let linearProjectId = null;
78
96
  let parentProjectName = null;
79
97
  if (parentLinearIssueId) {
package/dist/env-utils.js CHANGED
@@ -14,6 +14,22 @@ const os_1 = require("os");
14
14
  * - Installed globally (user can create .env in their project or home directory)
15
15
  * - Installed locally (works with project's .env file)
16
16
  */
17
+ function hasLinearApiKey(envPath) {
18
+ const contents = (0, fs_1.readFileSync)(envPath, 'utf-8');
19
+ const match = contents.match(/^\s*LINEAR_API_KEY\s*=\s*(.+)\s*$/m);
20
+ if (!match) {
21
+ return false;
22
+ }
23
+ const rawValue = match[1].trim();
24
+ const unquoted = rawValue.replace(/^['"]|['"]$/g, '').trim();
25
+ return unquoted.length > 0;
26
+ }
27
+ function exitMissingLinearApiKey() {
28
+ console.error('❌ LINEAR_API_KEY not found in .env');
29
+ console.error(' Add it to your .env file:');
30
+ console.error(' echo "LINEAR_API_KEY=lin_api_..." > .env');
31
+ process.exit(1);
32
+ }
17
33
  function loadEnvFile() {
18
34
  // First, try to find .env in current working directory and parent directories
19
35
  let currentDir = process.cwd();
@@ -21,6 +37,10 @@ function loadEnvFile() {
21
37
  while (currentDir !== root) {
22
38
  const envPath = (0, path_1.resolve)(currentDir, '.env');
23
39
  if ((0, fs_1.existsSync)(envPath)) {
40
+ if (!hasLinearApiKey(envPath)) {
41
+ exitMissingLinearApiKey();
42
+ }
43
+ console.log(`✅ LINEAR_API_KEY found in ${envPath}`);
24
44
  (0, dotenv_1.config)({ path: envPath });
25
45
  return;
26
46
  }
@@ -34,10 +54,12 @@ function loadEnvFile() {
34
54
  // Fallback: try home directory
35
55
  const homeEnvPath = (0, path_1.resolve)((0, os_1.homedir)(), '.env');
36
56
  if ((0, fs_1.existsSync)(homeEnvPath)) {
57
+ if (!hasLinearApiKey(homeEnvPath)) {
58
+ exitMissingLinearApiKey();
59
+ }
60
+ console.log(`✅ LINEAR_API_KEY found in ${homeEnvPath}`);
37
61
  (0, dotenv_1.config)({ path: homeEnvPath });
38
62
  return;
39
63
  }
40
- // If no .env file found, dotenv will use environment variables
41
- // This is fine - we don't need to throw an error here
42
- (0, dotenv_1.config)();
64
+ exitMissingLinearApiKey();
43
65
  }
@@ -154,9 +154,13 @@ class InputHandler {
154
154
  validate: (input) => input.length > 0 || 'Title is required',
155
155
  },
156
156
  {
157
- type: 'editor',
158
- name: 'description',
159
- message: 'Issue description (opens editor):',
157
+ type: 'input',
158
+ name: 'descriptionAction',
159
+ message: 'Body [(e) to launch vim, enter to skip]:',
160
+ validate: (input) => {
161
+ const value = input.trim().toLowerCase();
162
+ return value === '' || value === 'e' || 'Enter "e" to edit or press enter to skip';
163
+ },
160
164
  },
161
165
  {
162
166
  type: 'input',
@@ -190,9 +194,20 @@ class InputHandler {
190
194
  },
191
195
  },
192
196
  ]);
197
+ let description = '';
198
+ if (answers.descriptionAction.trim().toLowerCase() === 'e') {
199
+ const { description: editedDescription } = await inquirer_1.default.prompt([
200
+ {
201
+ type: 'editor',
202
+ name: 'description',
203
+ message: 'Issue description:',
204
+ },
205
+ ]);
206
+ description = editedDescription || '';
207
+ }
193
208
  return {
194
209
  title: answers.title,
195
- description: answers.description || '',
210
+ description,
196
211
  dueDate: answers.dueDate || '',
197
212
  startDate: answers.startDate || '',
198
213
  labels: answers.labels || [],
@@ -184,6 +184,26 @@ class LinearClientWrapper {
184
184
  });
185
185
  return issues.nodes[0]?.id || null;
186
186
  }
187
+ async waitForIssueByGitHubUrl(githubUrl, options = {}) {
188
+ const delayMs = options.delayMs ?? 5000;
189
+ const maxAttempts = options.maxAttempts;
190
+ for (let attempt = 1;; attempt++) {
191
+ const issueId = await this.findIssueByGitHubUrl(githubUrl);
192
+ if (issueId) {
193
+ return issueId;
194
+ }
195
+ if (maxAttempts !== undefined && attempt >= maxAttempts) {
196
+ return null;
197
+ }
198
+ options.onRetry?.(attempt, maxAttempts ?? Number.POSITIVE_INFINITY, delayMs);
199
+ if (delayMs > 0) {
200
+ await new Promise(resolve => setTimeout(resolve, delayMs));
201
+ }
202
+ else {
203
+ await new Promise(resolve => setTimeout(resolve, 0));
204
+ }
205
+ }
206
+ }
187
207
  async getIssueIdentifier(issueId) {
188
208
  try {
189
209
  const issue = await this.client.issue(issueId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-github-cli",
3
- "version": "1.1.5",
3
+ "version": "1.3.0",
4
4
  "description": "CLI tool for creating GitHub issues with Linear integration",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {