@fink-andreas/pi-linear-tools 0.1.0 → 0.2.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/CHANGELOG.md +20 -1
- package/README.md +18 -2
- package/extensions/pi-linear-tools.js +454 -109
- package/package.json +4 -2
- 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 +232 -59
- package/src/handlers.js +7 -2
- package/src/linear-client.js +36 -6
- package/src/linear.js +13 -1
- 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
|
-
return cachedApiKey;
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
// ignore, error below
|
|
107
|
+
const apiKey = settings.apiKey || settings.linearApiKey;
|
|
108
|
+
if (apiKey && apiKey.trim()) {
|
|
109
|
+
cachedApiKey = apiKey.trim();
|
|
110
|
+
return { apiKey: cachedApiKey };
|
|
85
111
|
}
|
|
86
112
|
|
|
87
|
-
|
|
113
|
+
const fallbackAccessToken = await getAccessToken();
|
|
114
|
+
if (fallbackAccessToken) {
|
|
115
|
+
return { accessToken: fallbackAccessToken };
|
|
116
|
+
}
|
|
117
|
+
|
|
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) {
|
|
@@ -107,14 +144,20 @@ Usage:
|
|
|
107
144
|
|
|
108
145
|
Commands:
|
|
109
146
|
help Show this help message
|
|
147
|
+
auth <action> Manage authentication (OAuth 2.0)
|
|
110
148
|
config Show current configuration
|
|
111
|
-
config --api-key <key> Set Linear API key
|
|
149
|
+
config --api-key <key> Set Linear API key (legacy)
|
|
112
150
|
config --default-team <key> Set default team
|
|
113
151
|
issue <action> [options] Manage issues
|
|
114
152
|
project <action> [options] Manage projects
|
|
115
153
|
team <action> [options] Manage teams
|
|
116
154
|
milestone <action> [options] Manage milestones
|
|
117
155
|
|
|
156
|
+
Auth Actions:
|
|
157
|
+
login Authenticate with Linear via OAuth 2.0
|
|
158
|
+
logout Clear stored authentication tokens
|
|
159
|
+
status Show current authentication status
|
|
160
|
+
|
|
118
161
|
Issue Actions:
|
|
119
162
|
list [--project X] [--states X,Y] [--assignee me|all] [--limit N]
|
|
120
163
|
view <issue> [--no-comments]
|
|
@@ -147,6 +190,8 @@ Common Flags:
|
|
|
147
190
|
--limit Max results (default: 50)
|
|
148
191
|
|
|
149
192
|
Examples:
|
|
193
|
+
pi-linear-tools auth login
|
|
194
|
+
pi-linear-tools auth status
|
|
150
195
|
pi-linear-tools issue list --project MyProject --states "In Progress,Backlog"
|
|
151
196
|
pi-linear-tools issue view ENG-123
|
|
152
197
|
pi-linear-tools issue create --title "Fix bug" --team ENG --priority 2
|
|
@@ -154,6 +199,12 @@ Examples:
|
|
|
154
199
|
pi-linear-tools issue start ENG-123
|
|
155
200
|
pi-linear-tools milestone list --project MyProject
|
|
156
201
|
pi-linear-tools config --api-key lin_xxx
|
|
202
|
+
|
|
203
|
+
Authentication:
|
|
204
|
+
OAuth 2.0 is the recommended authentication method.
|
|
205
|
+
Run 'pi-linear-tools auth login' to authenticate.
|
|
206
|
+
For CI/headless environments, set environment variables:
|
|
207
|
+
LINEAR_ACCESS_TOKEN, LINEAR_REFRESH_TOKEN, LINEAR_EXPIRES_AT
|
|
157
208
|
`);
|
|
158
209
|
}
|
|
159
210
|
|
|
@@ -276,6 +327,132 @@ Delete Options:
|
|
|
276
327
|
`);
|
|
277
328
|
}
|
|
278
329
|
|
|
330
|
+
function printAuthHelp() {
|
|
331
|
+
console.log(`pi-linear-tools auth - Manage Linear authentication
|
|
332
|
+
|
|
333
|
+
Usage:
|
|
334
|
+
pi-linear-tools auth <action>
|
|
335
|
+
|
|
336
|
+
Actions:
|
|
337
|
+
login Authenticate with Linear via OAuth 2.0
|
|
338
|
+
logout Clear stored authentication tokens
|
|
339
|
+
status Show current authentication status
|
|
340
|
+
|
|
341
|
+
Login:
|
|
342
|
+
Starts the OAuth 2.0 authentication flow:
|
|
343
|
+
1. Opens your browser to Linear's authorization page
|
|
344
|
+
2. You authorize the application
|
|
345
|
+
3. Tokens are stored securely in your OS keychain
|
|
346
|
+
4. Automatic token refresh keeps you authenticated
|
|
347
|
+
|
|
348
|
+
Logout:
|
|
349
|
+
Clears stored OAuth tokens from your keychain.
|
|
350
|
+
You'll need to authenticate again to access Linear.
|
|
351
|
+
|
|
352
|
+
Status:
|
|
353
|
+
Shows your current authentication status:
|
|
354
|
+
- Whether you're authenticated
|
|
355
|
+
- Token expiry time
|
|
356
|
+
- Granted OAuth scopes
|
|
357
|
+
|
|
358
|
+
Examples:
|
|
359
|
+
pi-linear-tools auth login
|
|
360
|
+
pi-linear-tools auth status
|
|
361
|
+
pi-linear-tools auth logout
|
|
362
|
+
|
|
363
|
+
Environment Variables (for CI/headless environments):
|
|
364
|
+
LINEAR_ACCESS_TOKEN OAuth access token
|
|
365
|
+
LINEAR_REFRESH_TOKEN OAuth refresh token
|
|
366
|
+
LINEAR_EXPIRES_AT Token expiry timestamp (milliseconds)
|
|
367
|
+
`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ===== AUTH HANDLERS =====
|
|
371
|
+
|
|
372
|
+
async function handleAuthLogin(args) {
|
|
373
|
+
const port = readFlag(args, '--port');
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const tokens = await authenticate({
|
|
377
|
+
port: port ? parseInt(port, 10) : undefined,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const settings = await loadSettings();
|
|
381
|
+
settings.authMethod = 'oauth';
|
|
382
|
+
await saveSettings(settings);
|
|
383
|
+
|
|
384
|
+
console.log('\n✓ Authentication successful!');
|
|
385
|
+
console.log(`✓ Token expires at: ${new Date(tokens.expiresAt).toLocaleString()}`);
|
|
386
|
+
console.log(`✓ Granted scopes: ${tokens.scope.join(', ')}`);
|
|
387
|
+
console.log('\nYou can now use pi-linear-tools commands.');
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error('\n✗ Authentication failed:', error.message);
|
|
390
|
+
process.exitCode = 1;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function handleAuthLogout() {
|
|
395
|
+
try {
|
|
396
|
+
await logout();
|
|
397
|
+
console.log('\n✓ Logged out successfully');
|
|
398
|
+
console.log('\nYou will need to authenticate again to access Linear.');
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error('\n✗ Logout failed:', error.message);
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function handleAuthStatus() {
|
|
406
|
+
try {
|
|
407
|
+
const status = await getAuthStatus();
|
|
408
|
+
|
|
409
|
+
if (!status) {
|
|
410
|
+
console.log('\nAuthentication status: Not authenticated');
|
|
411
|
+
console.log('\nTo authenticate, run: pi-linear-tools auth login');
|
|
412
|
+
console.log('\nFor CI/headless environments, set these environment variables:');
|
|
413
|
+
console.log(' LINEAR_ACCESS_TOKEN');
|
|
414
|
+
console.log(' LINEAR_REFRESH_TOKEN');
|
|
415
|
+
console.log(' LINEAR_EXPIRES_AT');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const isAuth = await isAuthenticated();
|
|
420
|
+
|
|
421
|
+
console.log('\nAuthentication status:', isAuth ? 'Authenticated' : 'Token expired');
|
|
422
|
+
console.log(`Token expires at: ${new Date(status.expiresAt).toLocaleString()}`);
|
|
423
|
+
|
|
424
|
+
if (status.expiresIn > 0) {
|
|
425
|
+
const minutes = Math.floor(status.expiresIn / 60000);
|
|
426
|
+
console.log(`Time until expiry: ${minutes} minute${minutes !== 1 ? 's' : ''}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
console.log(`Granted scopes: ${status.scopes.join(', ')}`);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
console.error('\n✗ Failed to get authentication status:', error.message);
|
|
432
|
+
process.exitCode = 1;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function handleAuth(args) {
|
|
437
|
+
const [action] = args;
|
|
438
|
+
|
|
439
|
+
if (!action || action === '--help' || action === '-h') {
|
|
440
|
+
printAuthHelp();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
switch (action) {
|
|
445
|
+
case 'login':
|
|
446
|
+
return handleAuthLogin(args);
|
|
447
|
+
case 'logout':
|
|
448
|
+
return handleAuthLogout();
|
|
449
|
+
case 'status':
|
|
450
|
+
return handleAuthStatus();
|
|
451
|
+
default:
|
|
452
|
+
throw new Error(`Unknown auth action: ${action}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
279
456
|
// ===== CONFIG HANDLER =====
|
|
280
457
|
|
|
281
458
|
async function tryResolveProjectId(projectRef, explicitApiKey = null) {
|
|
@@ -303,7 +480,8 @@ async function handleConfig(args) {
|
|
|
303
480
|
|
|
304
481
|
if (apiKey) {
|
|
305
482
|
const settings = await loadSettings();
|
|
306
|
-
settings.
|
|
483
|
+
settings.apiKey = apiKey;
|
|
484
|
+
settings.authMethod = 'api-key';
|
|
307
485
|
await saveSettings(settings);
|
|
308
486
|
cachedApiKey = null;
|
|
309
487
|
console.log('LINEAR_API_KEY saved to settings');
|
|
@@ -324,7 +502,7 @@ async function handleConfig(args) {
|
|
|
324
502
|
}
|
|
325
503
|
|
|
326
504
|
const settings = await loadSettings();
|
|
327
|
-
const projectId = await tryResolveProjectId(projectName, settings.linearApiKey);
|
|
505
|
+
const projectId = await tryResolveProjectId(projectName, settings.apiKey || settings.linearApiKey);
|
|
328
506
|
|
|
329
507
|
if (!settings.projects[projectId]) {
|
|
330
508
|
settings.projects[projectId] = { scope: { team: null } };
|
|
@@ -341,8 +519,8 @@ async function handleConfig(args) {
|
|
|
341
519
|
}
|
|
342
520
|
|
|
343
521
|
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');
|
|
522
|
+
const hasKey = !!(settings.apiKey || settings.linearApiKey || process.env.LINEAR_API_KEY);
|
|
523
|
+
const keySource = process.env.LINEAR_API_KEY ? 'environment' : (settings.apiKey || settings.linearApiKey ? 'settings' : 'not set');
|
|
346
524
|
|
|
347
525
|
console.log(`Configuration:
|
|
348
526
|
LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})
|
|
@@ -358,8 +536,7 @@ Commands:
|
|
|
358
536
|
// ===== ISSUE HANDLERS =====
|
|
359
537
|
|
|
360
538
|
async function handleIssueList(args) {
|
|
361
|
-
const
|
|
362
|
-
const client = createLinearClient(apiKey);
|
|
539
|
+
const client = await createAuthenticatedClient();
|
|
363
540
|
|
|
364
541
|
const params = {
|
|
365
542
|
project: readFlag(args, '--project'),
|
|
@@ -373,8 +550,7 @@ async function handleIssueList(args) {
|
|
|
373
550
|
}
|
|
374
551
|
|
|
375
552
|
async function handleIssueView(args) {
|
|
376
|
-
const
|
|
377
|
-
const client = createLinearClient(apiKey);
|
|
553
|
+
const client = await createAuthenticatedClient();
|
|
378
554
|
|
|
379
555
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
380
556
|
if (positional.length === 0) {
|
|
@@ -391,8 +567,7 @@ async function handleIssueView(args) {
|
|
|
391
567
|
}
|
|
392
568
|
|
|
393
569
|
async function handleIssueCreate(args) {
|
|
394
|
-
const
|
|
395
|
-
const client = createLinearClient(apiKey);
|
|
570
|
+
const client = await createAuthenticatedClient();
|
|
396
571
|
|
|
397
572
|
const params = {
|
|
398
573
|
title: readFlag(args, '--title'),
|
|
@@ -414,8 +589,7 @@ async function handleIssueCreate(args) {
|
|
|
414
589
|
}
|
|
415
590
|
|
|
416
591
|
async function handleIssueUpdate(args) {
|
|
417
|
-
const
|
|
418
|
-
const client = createLinearClient(apiKey);
|
|
592
|
+
const client = await createAuthenticatedClient();
|
|
419
593
|
|
|
420
594
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
421
595
|
if (positional.length === 0) {
|
|
@@ -438,8 +612,7 @@ async function handleIssueUpdate(args) {
|
|
|
438
612
|
}
|
|
439
613
|
|
|
440
614
|
async function handleIssueComment(args) {
|
|
441
|
-
const
|
|
442
|
-
const client = createLinearClient(apiKey);
|
|
615
|
+
const client = await createAuthenticatedClient();
|
|
443
616
|
|
|
444
617
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
445
618
|
if (positional.length === 0) {
|
|
@@ -460,8 +633,7 @@ async function handleIssueComment(args) {
|
|
|
460
633
|
}
|
|
461
634
|
|
|
462
635
|
async function handleIssueStart(args) {
|
|
463
|
-
const
|
|
464
|
-
const client = createLinearClient(apiKey);
|
|
636
|
+
const client = await createAuthenticatedClient();
|
|
465
637
|
|
|
466
638
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
467
639
|
if (positional.length === 0) {
|
|
@@ -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
|
@@ -589,7 +589,7 @@ export async function executeMilestoneList(client, params) {
|
|
|
589
589
|
? ` → ${milestone.targetDate.split('T')[0]}`
|
|
590
590
|
: '';
|
|
591
591
|
|
|
592
|
-
lines.push(`- ${statusEmoji} **${milestone.name}** _[${milestone.status}]_ (${progressLabel})${dateLabel}
|
|
592
|
+
lines.push(`- ${statusEmoji} **${milestone.name}** _[${milestone.status}]_ (${progressLabel})${dateLabel} \`${milestone.id}\``);
|
|
593
593
|
if (milestone.description) {
|
|
594
594
|
lines.push(` ${milestone.description.split('\n')[0].slice(0, 100)}${milestone.description.length > 100 ? '...' : ''}`);
|
|
595
595
|
}
|
|
@@ -771,10 +771,15 @@ export async function executeMilestoneDelete(client, params) {
|
|
|
771
771
|
const milestoneId = ensureNonEmpty(params.milestone, 'milestone');
|
|
772
772
|
const result = await deleteProjectMilestone(client, milestoneId);
|
|
773
773
|
|
|
774
|
+
const label = result.name
|
|
775
|
+
? `**${result.name}** (\`${milestoneId}\`)`
|
|
776
|
+
: `\`${milestoneId}\``;
|
|
777
|
+
|
|
774
778
|
return toTextResult(
|
|
775
|
-
`Deleted milestone
|
|
779
|
+
`Deleted milestone ${label}`,
|
|
776
780
|
{
|
|
777
781
|
milestoneId: result.milestoneId,
|
|
782
|
+
name: result.name,
|
|
778
783
|
success: result.success,
|
|
779
784
|
}
|
|
780
785
|
);
|
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
|
/**
|
package/src/linear.js
CHANGED
|
@@ -1166,14 +1166,26 @@ export async function updateProjectMilestone(client, milestoneId, patch = {}) {
|
|
|
1166
1166
|
* Delete a project milestone
|
|
1167
1167
|
* @param {LinearClient} client - Linear SDK client
|
|
1168
1168
|
* @param {string} milestoneId - Milestone ID
|
|
1169
|
-
* @returns {Promise<{success: boolean, milestoneId: string}>}
|
|
1169
|
+
* @returns {Promise<{success: boolean, milestoneId: string, name: string|null}>}
|
|
1170
1170
|
*/
|
|
1171
1171
|
export async function deleteProjectMilestone(client, milestoneId) {
|
|
1172
|
+
let milestoneName = null;
|
|
1173
|
+
|
|
1174
|
+
try {
|
|
1175
|
+
const existing = await client.projectMilestone(milestoneId);
|
|
1176
|
+
if (existing?.name) {
|
|
1177
|
+
milestoneName = existing.name;
|
|
1178
|
+
}
|
|
1179
|
+
} catch {
|
|
1180
|
+
milestoneName = null;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1172
1183
|
const result = await client.deleteProjectMilestone(milestoneId);
|
|
1173
1184
|
|
|
1174
1185
|
return {
|
|
1175
1186
|
success: result.success,
|
|
1176
1187
|
milestoneId,
|
|
1188
|
+
name: milestoneName,
|
|
1177
1189
|
};
|
|
1178
1190
|
}
|
|
1179
1191
|
|