@fink-andreas/pi-linear-tools 0.1.0 → 0.2.1
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/CHANGELOG.md +20 -1
- package/README.md +18 -2
- package/extensions/pi-linear-tools.js +449 -113
- package/index.js +916 -6
- package/package.json +6 -4
- package/src/auth/callback-server.js +337 -0
- package/src/auth/index.js +246 -0
- package/src/auth/oauth.js +281 -0
- package/src/auth/pkce.js +111 -0
- package/src/auth/token-refresh.js +210 -0
- package/src/auth/token-store.js +415 -0
- package/src/cli.js +238 -65
- package/src/handlers.js +18 -10
- package/src/linear-client.js +36 -6
- package/src/linear.js +16 -9
- package/src/settings.js +107 -6
package/src/cli.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { loadSettings, saveSettings } from './settings.js';
|
|
2
2
|
import { createLinearClient } from './linear-client.js';
|
|
3
3
|
import { resolveProjectRef } from './linear.js';
|
|
4
|
+
import {
|
|
5
|
+
authenticate,
|
|
6
|
+
logout,
|
|
7
|
+
getAuthStatus,
|
|
8
|
+
isAuthenticated,
|
|
9
|
+
getAccessToken,
|
|
10
|
+
} from './auth/index.js';
|
|
4
11
|
import {
|
|
5
12
|
executeIssueList,
|
|
6
13
|
executeIssueView,
|
|
@@ -60,31 +67,61 @@ function parseBoolean(value) {
|
|
|
60
67
|
return undefined;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
function withMilestoneScopeHint(error) {
|
|
71
|
+
const message = String(error?.message || error || 'Unknown error');
|
|
72
|
+
|
|
73
|
+
if (/invalid scope/i.test(message) && /write/i.test(message)) {
|
|
74
|
+
return new Error(
|
|
75
|
+
`${message}\nHint: Milestone create/update/delete require Linear write scope. ` +
|
|
76
|
+
`Use API key auth: pi-linear-tools config --api-key <key>`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ===== AUTH RESOLUTION =====
|
|
64
84
|
|
|
65
85
|
let cachedApiKey = null;
|
|
66
86
|
|
|
67
|
-
async function
|
|
87
|
+
async function getLinearAuth() {
|
|
68
88
|
const envKey = process.env.LINEAR_API_KEY;
|
|
69
89
|
if (envKey && envKey.trim()) {
|
|
70
|
-
return envKey.trim();
|
|
90
|
+
return { apiKey: envKey.trim() };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const settings = await loadSettings();
|
|
94
|
+
const authMethod = settings.authMethod || 'api-key';
|
|
95
|
+
|
|
96
|
+
if (authMethod === 'oauth') {
|
|
97
|
+
const accessToken = await getAccessToken();
|
|
98
|
+
if (accessToken) {
|
|
99
|
+
return { accessToken };
|
|
100
|
+
}
|
|
71
101
|
}
|
|
72
102
|
|
|
73
103
|
if (cachedApiKey) {
|
|
74
|
-
return cachedApiKey;
|
|
104
|
+
return { apiKey: cachedApiKey };
|
|
75
105
|
}
|
|
76
106
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
const apiKey = settings.apiKey || settings.linearApiKey;
|
|
108
|
+
if (apiKey && apiKey.trim()) {
|
|
109
|
+
cachedApiKey = apiKey.trim();
|
|
110
|
+
return { apiKey: cachedApiKey };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const fallbackAccessToken = await getAccessToken();
|
|
114
|
+
if (fallbackAccessToken) {
|
|
115
|
+
return { accessToken: fallbackAccessToken };
|
|
85
116
|
}
|
|
86
117
|
|
|
87
|
-
throw new Error(
|
|
118
|
+
throw new Error(
|
|
119
|
+
'No Linear authentication configured. Run: pi-linear-tools auth login or pi-linear-tools config --api-key <key>'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function createAuthenticatedClient() {
|
|
124
|
+
return createLinearClient(await getLinearAuth());
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
async function resolveDefaultTeam(projectId) {
|
|
@@ -106,15 +143,23 @@ Usage:
|
|
|
106
143
|
pi-linear-tools <command> [options]
|
|
107
144
|
|
|
108
145
|
Commands:
|
|
109
|
-
help Show this help message
|
|
110
|
-
config Show current configuration
|
|
111
|
-
config --api-key <key> Set Linear API key
|
|
112
|
-
config --default-team <key> Set default team
|
|
113
146
|
issue <action> [options] Manage issues
|
|
114
147
|
project <action> [options] Manage projects
|
|
115
148
|
team <action> [options] Manage teams
|
|
116
149
|
milestone <action> [options] Manage milestones
|
|
117
150
|
|
|
151
|
+
Other commands:
|
|
152
|
+
help Show this help message
|
|
153
|
+
auth <action> Manage authentication (OAuth 2.0)
|
|
154
|
+
config Show current configuration
|
|
155
|
+
config --api-key <key> Set Linear API key (legacy)
|
|
156
|
+
config --default-team <key> Set default team
|
|
157
|
+
|
|
158
|
+
Auth Actions:
|
|
159
|
+
login Authenticate with Linear via OAuth 2.0
|
|
160
|
+
logout Clear stored authentication tokens
|
|
161
|
+
status Show current authentication status
|
|
162
|
+
|
|
118
163
|
Issue Actions:
|
|
119
164
|
list [--project X] [--states X,Y] [--assignee me|all] [--limit N]
|
|
120
165
|
view <issue> [--no-comments]
|
|
@@ -122,7 +167,7 @@ Issue Actions:
|
|
|
122
167
|
update <issue> [--title X] [--description X] [--state X] [--priority 0-4]
|
|
123
168
|
[--assignee me|ID] [--milestone X] [--sub-issue-of X]
|
|
124
169
|
comment <issue> --body X
|
|
125
|
-
start <issue> [--
|
|
170
|
+
start <issue> [--from-ref X] [--on-branch-exists switch|suffix]
|
|
126
171
|
delete <issue>
|
|
127
172
|
|
|
128
173
|
Project Actions:
|
|
@@ -147,6 +192,8 @@ Common Flags:
|
|
|
147
192
|
--limit Max results (default: 50)
|
|
148
193
|
|
|
149
194
|
Examples:
|
|
195
|
+
pi-linear-tools auth login
|
|
196
|
+
pi-linear-tools auth status
|
|
150
197
|
pi-linear-tools issue list --project MyProject --states "In Progress,Backlog"
|
|
151
198
|
pi-linear-tools issue view ENG-123
|
|
152
199
|
pi-linear-tools issue create --title "Fix bug" --team ENG --priority 2
|
|
@@ -154,6 +201,12 @@ Examples:
|
|
|
154
201
|
pi-linear-tools issue start ENG-123
|
|
155
202
|
pi-linear-tools milestone list --project MyProject
|
|
156
203
|
pi-linear-tools config --api-key lin_xxx
|
|
204
|
+
|
|
205
|
+
Authentication:
|
|
206
|
+
API key is the recommended authentication method (supports milestones).
|
|
207
|
+
Run 'pi-linear-tools config --api-key <key>' to authenticate.
|
|
208
|
+
For CI/headless environments, set the LINEAR_API_KEY environment variable.
|
|
209
|
+
OAuth 2.0 is also available via 'pi-linear-tools auth login'.
|
|
157
210
|
`);
|
|
158
211
|
}
|
|
159
212
|
|
|
@@ -207,7 +260,6 @@ Comment Options:
|
|
|
207
260
|
|
|
208
261
|
Start Options:
|
|
209
262
|
<issue> Issue key or ID
|
|
210
|
-
--branch X Custom branch name (default: issue's branch name)
|
|
211
263
|
--from-ref X Git ref to branch from (default: HEAD)
|
|
212
264
|
--on-branch-exists X "switch" or "suffix" (default: switch)
|
|
213
265
|
|
|
@@ -276,6 +328,132 @@ Delete Options:
|
|
|
276
328
|
`);
|
|
277
329
|
}
|
|
278
330
|
|
|
331
|
+
function printAuthHelp() {
|
|
332
|
+
console.log(`pi-linear-tools auth - Manage Linear authentication
|
|
333
|
+
|
|
334
|
+
Usage:
|
|
335
|
+
pi-linear-tools auth <action>
|
|
336
|
+
|
|
337
|
+
Actions:
|
|
338
|
+
login Authenticate with Linear via OAuth 2.0
|
|
339
|
+
logout Clear stored authentication tokens
|
|
340
|
+
status Show current authentication status
|
|
341
|
+
|
|
342
|
+
Login:
|
|
343
|
+
Starts the OAuth 2.0 authentication flow:
|
|
344
|
+
1. Opens your browser to Linear's authorization page
|
|
345
|
+
2. You authorize the application
|
|
346
|
+
3. Tokens are stored securely in your OS keychain
|
|
347
|
+
4. Automatic token refresh keeps you authenticated
|
|
348
|
+
|
|
349
|
+
Logout:
|
|
350
|
+
Clears stored OAuth tokens from your keychain.
|
|
351
|
+
You'll need to authenticate again to access Linear.
|
|
352
|
+
|
|
353
|
+
Status:
|
|
354
|
+
Shows your current authentication status:
|
|
355
|
+
- Whether you're authenticated
|
|
356
|
+
- Token expiry time
|
|
357
|
+
- Granted OAuth scopes
|
|
358
|
+
|
|
359
|
+
Examples:
|
|
360
|
+
pi-linear-tools auth login
|
|
361
|
+
pi-linear-tools auth status
|
|
362
|
+
pi-linear-tools auth logout
|
|
363
|
+
|
|
364
|
+
Environment Variables (for CI/headless environments):
|
|
365
|
+
LINEAR_ACCESS_TOKEN OAuth access token
|
|
366
|
+
LINEAR_REFRESH_TOKEN OAuth refresh token
|
|
367
|
+
LINEAR_EXPIRES_AT Token expiry timestamp (milliseconds)
|
|
368
|
+
`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ===== AUTH HANDLERS =====
|
|
372
|
+
|
|
373
|
+
async function handleAuthLogin(args) {
|
|
374
|
+
const port = readFlag(args, '--port');
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const tokens = await authenticate({
|
|
378
|
+
port: port ? parseInt(port, 10) : undefined,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const settings = await loadSettings();
|
|
382
|
+
settings.authMethod = 'oauth';
|
|
383
|
+
await saveSettings(settings);
|
|
384
|
+
|
|
385
|
+
console.log('\n✓ Authentication successful!');
|
|
386
|
+
console.log(`✓ Token expires at: ${new Date(tokens.expiresAt).toLocaleString()}`);
|
|
387
|
+
console.log(`✓ Granted scopes: ${tokens.scope.join(', ')}`);
|
|
388
|
+
console.log('\nYou can now use pi-linear-tools commands.');
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error('\n✗ Authentication failed:', error.message);
|
|
391
|
+
process.exitCode = 1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function handleAuthLogout() {
|
|
396
|
+
try {
|
|
397
|
+
await logout();
|
|
398
|
+
console.log('\n✓ Logged out successfully');
|
|
399
|
+
console.log('\nYou will need to authenticate again to access Linear.');
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.error('\n✗ Logout failed:', error.message);
|
|
402
|
+
process.exitCode = 1;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function handleAuthStatus() {
|
|
407
|
+
try {
|
|
408
|
+
const status = await getAuthStatus();
|
|
409
|
+
|
|
410
|
+
if (!status) {
|
|
411
|
+
console.log('\nAuthentication status: Not authenticated');
|
|
412
|
+
console.log('\nTo authenticate, run: pi-linear-tools auth login');
|
|
413
|
+
console.log('\nFor CI/headless environments, set these environment variables:');
|
|
414
|
+
console.log(' LINEAR_ACCESS_TOKEN');
|
|
415
|
+
console.log(' LINEAR_REFRESH_TOKEN');
|
|
416
|
+
console.log(' LINEAR_EXPIRES_AT');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const isAuth = await isAuthenticated();
|
|
421
|
+
|
|
422
|
+
console.log('\nAuthentication status:', isAuth ? 'Authenticated' : 'Token expired');
|
|
423
|
+
console.log(`Token expires at: ${new Date(status.expiresAt).toLocaleString()}`);
|
|
424
|
+
|
|
425
|
+
if (status.expiresIn > 0) {
|
|
426
|
+
const minutes = Math.floor(status.expiresIn / 60000);
|
|
427
|
+
console.log(`Time until expiry: ${minutes} minute${minutes !== 1 ? 's' : ''}`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
console.log(`Granted scopes: ${status.scopes.join(', ')}`);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.error('\n✗ Failed to get authentication status:', error.message);
|
|
433
|
+
process.exitCode = 1;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function handleAuth(args) {
|
|
438
|
+
const [action] = args;
|
|
439
|
+
|
|
440
|
+
if (!action || action === '--help' || action === '-h') {
|
|
441
|
+
printAuthHelp();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
switch (action) {
|
|
446
|
+
case 'login':
|
|
447
|
+
return handleAuthLogin(args);
|
|
448
|
+
case 'logout':
|
|
449
|
+
return handleAuthLogout();
|
|
450
|
+
case 'status':
|
|
451
|
+
return handleAuthStatus();
|
|
452
|
+
default:
|
|
453
|
+
throw new Error(`Unknown auth action: ${action}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
279
457
|
// ===== CONFIG HANDLER =====
|
|
280
458
|
|
|
281
459
|
async function tryResolveProjectId(projectRef, explicitApiKey = null) {
|
|
@@ -303,7 +481,8 @@ async function handleConfig(args) {
|
|
|
303
481
|
|
|
304
482
|
if (apiKey) {
|
|
305
483
|
const settings = await loadSettings();
|
|
306
|
-
settings.
|
|
484
|
+
settings.apiKey = apiKey;
|
|
485
|
+
settings.authMethod = 'api-key';
|
|
307
486
|
await saveSettings(settings);
|
|
308
487
|
cachedApiKey = null;
|
|
309
488
|
console.log('LINEAR_API_KEY saved to settings');
|
|
@@ -324,7 +503,7 @@ async function handleConfig(args) {
|
|
|
324
503
|
}
|
|
325
504
|
|
|
326
505
|
const settings = await loadSettings();
|
|
327
|
-
const projectId = await tryResolveProjectId(projectName, settings.linearApiKey);
|
|
506
|
+
const projectId = await tryResolveProjectId(projectName, settings.apiKey || settings.linearApiKey);
|
|
328
507
|
|
|
329
508
|
if (!settings.projects[projectId]) {
|
|
330
509
|
settings.projects[projectId] = { scope: { team: null } };
|
|
@@ -341,8 +520,8 @@ async function handleConfig(args) {
|
|
|
341
520
|
}
|
|
342
521
|
|
|
343
522
|
const settings = await loadSettings();
|
|
344
|
-
const hasKey = !!(settings.linearApiKey || process.env.LINEAR_API_KEY);
|
|
345
|
-
const keySource = process.env.LINEAR_API_KEY ? 'environment' : (settings.linearApiKey ? 'settings' : 'not set');
|
|
523
|
+
const hasKey = !!(settings.apiKey || settings.linearApiKey || process.env.LINEAR_API_KEY);
|
|
524
|
+
const keySource = process.env.LINEAR_API_KEY ? 'environment' : (settings.apiKey || settings.linearApiKey ? 'settings' : 'not set');
|
|
346
525
|
|
|
347
526
|
console.log(`Configuration:
|
|
348
527
|
LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})
|
|
@@ -358,8 +537,7 @@ Commands:
|
|
|
358
537
|
// ===== ISSUE HANDLERS =====
|
|
359
538
|
|
|
360
539
|
async function handleIssueList(args) {
|
|
361
|
-
const
|
|
362
|
-
const client = createLinearClient(apiKey);
|
|
540
|
+
const client = await createAuthenticatedClient();
|
|
363
541
|
|
|
364
542
|
const params = {
|
|
365
543
|
project: readFlag(args, '--project'),
|
|
@@ -373,8 +551,7 @@ async function handleIssueList(args) {
|
|
|
373
551
|
}
|
|
374
552
|
|
|
375
553
|
async function handleIssueView(args) {
|
|
376
|
-
const
|
|
377
|
-
const client = createLinearClient(apiKey);
|
|
554
|
+
const client = await createAuthenticatedClient();
|
|
378
555
|
|
|
379
556
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
380
557
|
if (positional.length === 0) {
|
|
@@ -391,8 +568,7 @@ async function handleIssueView(args) {
|
|
|
391
568
|
}
|
|
392
569
|
|
|
393
570
|
async function handleIssueCreate(args) {
|
|
394
|
-
const
|
|
395
|
-
const client = createLinearClient(apiKey);
|
|
571
|
+
const client = await createAuthenticatedClient();
|
|
396
572
|
|
|
397
573
|
const params = {
|
|
398
574
|
title: readFlag(args, '--title'),
|
|
@@ -414,8 +590,7 @@ async function handleIssueCreate(args) {
|
|
|
414
590
|
}
|
|
415
591
|
|
|
416
592
|
async function handleIssueUpdate(args) {
|
|
417
|
-
const
|
|
418
|
-
const client = createLinearClient(apiKey);
|
|
593
|
+
const client = await createAuthenticatedClient();
|
|
419
594
|
|
|
420
595
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
421
596
|
if (positional.length === 0) {
|
|
@@ -438,8 +613,7 @@ async function handleIssueUpdate(args) {
|
|
|
438
613
|
}
|
|
439
614
|
|
|
440
615
|
async function handleIssueComment(args) {
|
|
441
|
-
const
|
|
442
|
-
const client = createLinearClient(apiKey);
|
|
616
|
+
const client = await createAuthenticatedClient();
|
|
443
617
|
|
|
444
618
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
445
619
|
if (positional.length === 0) {
|
|
@@ -460,8 +634,7 @@ async function handleIssueComment(args) {
|
|
|
460
634
|
}
|
|
461
635
|
|
|
462
636
|
async function handleIssueStart(args) {
|
|
463
|
-
const
|
|
464
|
-
const client = createLinearClient(apiKey);
|
|
637
|
+
const client = await createAuthenticatedClient();
|
|
465
638
|
|
|
466
639
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
467
640
|
if (positional.length === 0) {
|
|
@@ -470,7 +643,6 @@ async function handleIssueStart(args) {
|
|
|
470
643
|
|
|
471
644
|
const params = {
|
|
472
645
|
issue: positional[0],
|
|
473
|
-
branch: readFlag(args, '--branch'),
|
|
474
646
|
fromRef: readFlag(args, '--from-ref'),
|
|
475
647
|
onBranchExists: readFlag(args, '--on-branch-exists'),
|
|
476
648
|
};
|
|
@@ -480,8 +652,7 @@ async function handleIssueStart(args) {
|
|
|
480
652
|
}
|
|
481
653
|
|
|
482
654
|
async function handleIssueDelete(args) {
|
|
483
|
-
const
|
|
484
|
-
const client = createLinearClient(apiKey);
|
|
655
|
+
const client = await createAuthenticatedClient();
|
|
485
656
|
|
|
486
657
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
487
658
|
if (positional.length === 0) {
|
|
@@ -527,8 +698,7 @@ async function handleIssue(args) {
|
|
|
527
698
|
// ===== PROJECT HANDLERS =====
|
|
528
699
|
|
|
529
700
|
async function handleProjectList() {
|
|
530
|
-
const
|
|
531
|
-
const client = createLinearClient(apiKey);
|
|
701
|
+
const client = await createAuthenticatedClient();
|
|
532
702
|
|
|
533
703
|
const result = await executeProjectList(client);
|
|
534
704
|
console.log(result.content[0].text);
|
|
@@ -553,8 +723,7 @@ async function handleProject(args) {
|
|
|
553
723
|
// ===== TEAM HANDLERS =====
|
|
554
724
|
|
|
555
725
|
async function handleTeamList() {
|
|
556
|
-
const
|
|
557
|
-
const client = createLinearClient(apiKey);
|
|
726
|
+
const client = await createAuthenticatedClient();
|
|
558
727
|
|
|
559
728
|
const result = await executeTeamList(client);
|
|
560
729
|
console.log(result.content[0].text);
|
|
@@ -579,8 +748,7 @@ async function handleTeam(args) {
|
|
|
579
748
|
// ===== MILESTONE HANDLERS =====
|
|
580
749
|
|
|
581
750
|
async function handleMilestoneList(args) {
|
|
582
|
-
const
|
|
583
|
-
const client = createLinearClient(apiKey);
|
|
751
|
+
const client = await createAuthenticatedClient();
|
|
584
752
|
|
|
585
753
|
const params = {
|
|
586
754
|
project: readFlag(args, '--project'),
|
|
@@ -591,8 +759,7 @@ async function handleMilestoneList(args) {
|
|
|
591
759
|
}
|
|
592
760
|
|
|
593
761
|
async function handleMilestoneView(args) {
|
|
594
|
-
const
|
|
595
|
-
const client = createLinearClient(apiKey);
|
|
762
|
+
const client = await createAuthenticatedClient();
|
|
596
763
|
|
|
597
764
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
598
765
|
if (positional.length === 0) {
|
|
@@ -608,8 +775,7 @@ async function handleMilestoneView(args) {
|
|
|
608
775
|
}
|
|
609
776
|
|
|
610
777
|
async function handleMilestoneCreate(args) {
|
|
611
|
-
const
|
|
612
|
-
const client = createLinearClient(apiKey);
|
|
778
|
+
const client = await createAuthenticatedClient();
|
|
613
779
|
|
|
614
780
|
const params = {
|
|
615
781
|
project: readFlag(args, '--project'),
|
|
@@ -628,8 +794,7 @@ async function handleMilestoneCreate(args) {
|
|
|
628
794
|
}
|
|
629
795
|
|
|
630
796
|
async function handleMilestoneUpdate(args) {
|
|
631
|
-
const
|
|
632
|
-
const client = createLinearClient(apiKey);
|
|
797
|
+
const client = await createAuthenticatedClient();
|
|
633
798
|
|
|
634
799
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
635
800
|
if (positional.length === 0) {
|
|
@@ -649,8 +814,7 @@ async function handleMilestoneUpdate(args) {
|
|
|
649
814
|
}
|
|
650
815
|
|
|
651
816
|
async function handleMilestoneDelete(args) {
|
|
652
|
-
const
|
|
653
|
-
const client = createLinearClient(apiKey);
|
|
817
|
+
const client = await createAuthenticatedClient();
|
|
654
818
|
|
|
655
819
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
656
820
|
if (positional.length === 0) {
|
|
@@ -673,19 +837,23 @@ async function handleMilestone(args) {
|
|
|
673
837
|
return;
|
|
674
838
|
}
|
|
675
839
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
840
|
+
try {
|
|
841
|
+
switch (action) {
|
|
842
|
+
case 'list':
|
|
843
|
+
return await handleMilestoneList(rest);
|
|
844
|
+
case 'view':
|
|
845
|
+
return await handleMilestoneView(rest);
|
|
846
|
+
case 'create':
|
|
847
|
+
return await handleMilestoneCreate(rest);
|
|
848
|
+
case 'update':
|
|
849
|
+
return await handleMilestoneUpdate(rest);
|
|
850
|
+
case 'delete':
|
|
851
|
+
return await handleMilestoneDelete(rest);
|
|
852
|
+
default:
|
|
853
|
+
throw new Error(`Unknown milestone action: ${action}`);
|
|
854
|
+
}
|
|
855
|
+
} catch (error) {
|
|
856
|
+
throw withMilestoneScopeHint(error);
|
|
689
857
|
}
|
|
690
858
|
}
|
|
691
859
|
|
|
@@ -699,6 +867,11 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
699
867
|
return;
|
|
700
868
|
}
|
|
701
869
|
|
|
870
|
+
if (command === 'auth') {
|
|
871
|
+
await handleAuth(rest);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
702
875
|
if (command === 'config') {
|
|
703
876
|
await handleConfig(rest);
|
|
704
877
|
return;
|
package/src/handlers.js
CHANGED
|
@@ -447,28 +447,31 @@ export async function executeIssueStart(client, params, options = {}) {
|
|
|
447
447
|
const issue = ensureNonEmpty(params.issue, 'issue');
|
|
448
448
|
const prepared = await prepareIssueStart(client, issue);
|
|
449
449
|
|
|
450
|
-
|
|
451
|
-
|
|
450
|
+
// Always use Linear's suggested branchName - it cannot be changed via API
|
|
451
|
+
// and using a custom branch would break Linear's branch-to-issue linking
|
|
452
|
+
const branchName = prepared.branchName;
|
|
453
|
+
if (!branchName) {
|
|
452
454
|
throw new Error(
|
|
453
|
-
`No branch name
|
|
455
|
+
`No branch name available for issue ${prepared.issue.identifier}. The issue may not have a team assigned.`
|
|
454
456
|
);
|
|
455
457
|
}
|
|
456
458
|
|
|
457
459
|
let gitResult;
|
|
458
460
|
if (gitExecutor) {
|
|
459
461
|
// Use provided git executor (e.g., pi.exec)
|
|
460
|
-
gitResult = await gitExecutor(
|
|
462
|
+
gitResult = await gitExecutor(branchName, params.fromRef || 'HEAD', params.onBranchExists || 'switch');
|
|
461
463
|
} else {
|
|
462
464
|
// Use built-in child_process git operations
|
|
463
|
-
gitResult = await startGitBranch(
|
|
465
|
+
gitResult = await startGitBranch(branchName, params.fromRef || 'HEAD', params.onBranchExists || 'switch');
|
|
464
466
|
}
|
|
465
467
|
|
|
466
468
|
const updatedIssue = await setIssueState(client, prepared.issue.id, prepared.startedState.id);
|
|
467
469
|
|
|
470
|
+
const identifier = updatedIssue.identifier || prepared.issue.identifier;
|
|
468
471
|
const compactTitle = String(updatedIssue.title || prepared.issue?.title || '').trim().toLowerCase();
|
|
469
472
|
const summary = compactTitle
|
|
470
|
-
? `Started issue ${
|
|
471
|
-
: `Started issue ${
|
|
473
|
+
? `Started issue ${identifier} (${compactTitle})`
|
|
474
|
+
: `Started issue ${identifier}`;
|
|
472
475
|
|
|
473
476
|
return toTextResult(summary, {
|
|
474
477
|
issueId: updatedIssue.id,
|
|
@@ -589,7 +592,7 @@ export async function executeMilestoneList(client, params) {
|
|
|
589
592
|
? ` → ${milestone.targetDate.split('T')[0]}`
|
|
590
593
|
: '';
|
|
591
594
|
|
|
592
|
-
lines.push(`- ${statusEmoji} **${milestone.name}** _[${milestone.status}]_ (${progressLabel})${dateLabel}
|
|
595
|
+
lines.push(`- ${statusEmoji} **${milestone.name}** _[${milestone.status}]_ (${progressLabel})${dateLabel} \`${milestone.id}\``);
|
|
593
596
|
if (milestone.description) {
|
|
594
597
|
lines.push(` ${milestone.description.split('\n')[0].slice(0, 100)}${milestone.description.length > 100 ? '...' : ''}`);
|
|
595
598
|
}
|
|
@@ -731,11 +734,11 @@ export async function executeMilestoneCreate(client, params) {
|
|
|
731
734
|
export async function executeMilestoneUpdate(client, params) {
|
|
732
735
|
const milestoneId = ensureNonEmpty(params.milestone, 'milestone');
|
|
733
736
|
|
|
737
|
+
// Note: status is not included as it's a computed/read-only field in Linear's API
|
|
734
738
|
const result = await updateProjectMilestone(client, milestoneId, {
|
|
735
739
|
name: params.name,
|
|
736
740
|
description: params.description,
|
|
737
741
|
targetDate: params.targetDate,
|
|
738
|
-
status: params.status,
|
|
739
742
|
});
|
|
740
743
|
|
|
741
744
|
const friendlyChanges = result.changed;
|
|
@@ -771,10 +774,15 @@ export async function executeMilestoneDelete(client, params) {
|
|
|
771
774
|
const milestoneId = ensureNonEmpty(params.milestone, 'milestone');
|
|
772
775
|
const result = await deleteProjectMilestone(client, milestoneId);
|
|
773
776
|
|
|
777
|
+
const label = result.name
|
|
778
|
+
? `**${result.name}** (\`${milestoneId}\`)`
|
|
779
|
+
: `\`${milestoneId}\``;
|
|
780
|
+
|
|
774
781
|
return toTextResult(
|
|
775
|
-
`Deleted milestone
|
|
782
|
+
`Deleted milestone ${label}`,
|
|
776
783
|
{
|
|
777
784
|
milestoneId: result.milestoneId,
|
|
785
|
+
name: result.name,
|
|
778
786
|
success: result.success,
|
|
779
787
|
}
|
|
780
788
|
);
|
package/src/linear-client.js
CHANGED
|
@@ -2,29 +2,59 @@
|
|
|
2
2
|
* Linear SDK client factory
|
|
3
3
|
*
|
|
4
4
|
* Creates a configured LinearClient instance for interacting with Linear API.
|
|
5
|
+
* Supports both API key and OAuth token authentication.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { LinearClient } from '@linear/sdk';
|
|
9
|
+
import { debug, warn, error as logError } from './logger.js';
|
|
8
10
|
|
|
9
11
|
/** @type {Function|null} Test-only client factory override */
|
|
10
12
|
let _testClientFactory = null;
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Create a Linear SDK client
|
|
14
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* Supports two authentication methods:
|
|
18
|
+
* 1. API Key: Pass as string or { apiKey: '...' }
|
|
19
|
+
* 2. OAuth Token: Pass as { accessToken: '...' }
|
|
20
|
+
*
|
|
21
|
+
* @param {string|object} auth - Authentication credential
|
|
22
|
+
* @param {string} [auth.apiKey] - Linear API key (for API key auth)
|
|
23
|
+
* @param {string} [auth.accessToken] - OAuth access token (for OAuth auth)
|
|
15
24
|
* @returns {LinearClient} Configured Linear client
|
|
16
25
|
*/
|
|
17
|
-
export function createLinearClient(
|
|
26
|
+
export function createLinearClient(auth) {
|
|
18
27
|
// Allow test override
|
|
19
28
|
if (_testClientFactory) {
|
|
20
|
-
return _testClientFactory(
|
|
29
|
+
return _testClientFactory(auth);
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
let clientConfig;
|
|
33
|
+
|
|
34
|
+
// Handle different input formats
|
|
35
|
+
if (typeof auth === 'string') {
|
|
36
|
+
// Legacy: API key passed as string
|
|
37
|
+
clientConfig = { apiKey: auth };
|
|
38
|
+
} else if (typeof auth === 'object' && auth !== null) {
|
|
39
|
+
// Object format: { apiKey: '...' } or { accessToken: '...' }
|
|
40
|
+
if (auth.accessToken) {
|
|
41
|
+
clientConfig = { apiKey: auth.accessToken };
|
|
42
|
+
debug('Creating Linear client with OAuth access token');
|
|
43
|
+
} else if (auth.apiKey) {
|
|
44
|
+
clientConfig = { apiKey: auth.apiKey };
|
|
45
|
+
debug('Creating Linear client with API key');
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Auth object must contain either apiKey or accessToken'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'Invalid auth parameter: must be a string (API key) or an object with apiKey or accessToken'
|
|
54
|
+
);
|
|
25
55
|
}
|
|
26
56
|
|
|
27
|
-
return new LinearClient(
|
|
57
|
+
return new LinearClient(clientConfig);
|
|
28
58
|
}
|
|
29
59
|
|
|
30
60
|
/**
|