linear-sdk-cli 0.2.4 → 0.3.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 +36 -16
- package/MIGRATING.md +7 -6
- package/README.md +120 -31
- package/package.json +5 -3
- package/skills/linear-sdk-cli/SKILL.md +43 -8
- package/skills/linear-sdk-cli/references/api.md +1 -1
- package/skills/linear-sdk-cli/references/attachment.md +1 -1
- package/skills/linear-sdk-cli/references/auth.md +26 -7
- package/skills/linear-sdk-cli/references/commands.md +1 -1
- package/skills/linear-sdk-cli/references/comment.md +1 -1
- package/skills/linear-sdk-cli/references/completion.md +1 -1
- package/skills/linear-sdk-cli/references/config.md +5 -1
- package/skills/linear-sdk-cli/references/cycle.md +1 -1
- package/skills/linear-sdk-cli/references/document.md +1 -1
- package/skills/linear-sdk-cli/references/favorite.md +1 -1
- package/skills/linear-sdk-cli/references/initiative-update.md +1 -1
- package/skills/linear-sdk-cli/references/initiative.md +1 -1
- package/skills/linear-sdk-cli/references/issue.md +1 -1
- package/skills/linear-sdk-cli/references/label.md +1 -1
- package/skills/linear-sdk-cli/references/milestone.md +1 -1
- package/skills/linear-sdk-cli/references/notification.md +1 -1
- package/skills/linear-sdk-cli/references/open.md +1 -1
- package/skills/linear-sdk-cli/references/organization.md +1 -1
- package/skills/linear-sdk-cli/references/project-update.md +1 -1
- package/skills/linear-sdk-cli/references/project.md +1 -1
- package/skills/linear-sdk-cli/references/roadmap.md +1 -1
- package/skills/linear-sdk-cli/references/schema.md +1 -1
- package/skills/linear-sdk-cli/references/state.md +1 -1
- package/skills/linear-sdk-cli/references/team.md +1 -1
- package/skills/linear-sdk-cli/references/user.md +1 -1
- package/skills/linear-sdk-cli/references/webhook.md +1 -1
- package/skills/linear-sdk-cli/references/whoami.md +1 -1
- package/skills/linear-sdk-cli/scripts/generate-docs.ts +3 -1
- package/src/client.ts +57 -2
- package/src/commands/meta.ts +240 -65
- package/src/config.ts +319 -16
- package/src/context.ts +2 -0
- package/src/index.ts +19 -0
- package/src/lib/introspect.ts +1 -0
- package/src/lib/options.ts +1 -0
- package/src/lib/output-shapes.ts +21 -2
- package/src/oauth.ts +582 -0
package/src/commands/meta.ts
CHANGED
|
@@ -13,8 +13,11 @@ import {
|
|
|
13
13
|
probeKeyringCredential,
|
|
14
14
|
adoptKeyringCredential,
|
|
15
15
|
removeCredential,
|
|
16
|
+
removeOAuthCredential,
|
|
16
17
|
setDefaultWorkspace,
|
|
17
18
|
listCredentials,
|
|
19
|
+
readOAuthCredential,
|
|
20
|
+
writeOAuthCredential,
|
|
18
21
|
migrateCredentials,
|
|
19
22
|
redactKey,
|
|
20
23
|
resolveConfig,
|
|
@@ -28,10 +31,24 @@ import {
|
|
|
28
31
|
import { keyring } from "../lib/keyring.js";
|
|
29
32
|
import { readStdinSync } from "../lib/body.js";
|
|
30
33
|
import { authError, normalizeError, usageError } from "../lib/errors.js";
|
|
31
|
-
import { confirmDestructive,
|
|
34
|
+
import { confirmDestructive, promptSelect } from "../lib/prompt.js";
|
|
32
35
|
import { listTeams } from "../services/team.js";
|
|
33
36
|
import { ISSUE_SORTS } from "../services/issue.js";
|
|
34
37
|
import { firstTeam, type Context } from "../context.js";
|
|
38
|
+
import { openUrl } from "../lib/open.js";
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_OAUTH_SCOPES,
|
|
41
|
+
buildAuthorizationUrl,
|
|
42
|
+
createPkceRequest,
|
|
43
|
+
exchangeAuthorizationCode,
|
|
44
|
+
revokeOAuthToken,
|
|
45
|
+
startLoopbackCallback,
|
|
46
|
+
type OAuthUserCredential,
|
|
47
|
+
} from "../oauth.js";
|
|
48
|
+
|
|
49
|
+
export const DEFAULT_OAUTH_REDIRECT_URI = "http://127.0.0.1:43821/oauth/callback";
|
|
50
|
+
/** Public PKCE client identity. This is not a credential; no client secret is shipped or used. */
|
|
51
|
+
export const DEFAULT_OAUTH_CLIENT_ID = "eda60862e4bb8b8af82cfb3193b65c2f";
|
|
35
52
|
|
|
36
53
|
/**
|
|
37
54
|
* The `whoami` handler, shared by the top-level `linear whoami` and the
|
|
@@ -70,10 +87,15 @@ interface AuthValidationClient {
|
|
|
70
87
|
organization: Promise<{ id: string; name: string; urlKey: string }>;
|
|
71
88
|
}
|
|
72
89
|
|
|
73
|
-
export type AuthValidationClientFactory = (
|
|
90
|
+
export type AuthValidationClientFactory = (
|
|
91
|
+
credential: string,
|
|
92
|
+
kind?: "api-key" | "oauth-access-token",
|
|
93
|
+
) => AuthValidationClient;
|
|
74
94
|
|
|
75
|
-
const defaultAuthValidationClientFactory: AuthValidationClientFactory = (
|
|
76
|
-
new LinearClient(
|
|
95
|
+
const defaultAuthValidationClientFactory: AuthValidationClientFactory = (credential, kind) =>
|
|
96
|
+
new LinearClient(
|
|
97
|
+
kind === "oauth-access-token" ? { accessToken: credential } : { apiKey: credential },
|
|
98
|
+
) as unknown as AuthValidationClient;
|
|
77
99
|
let authValidationClientFactory = defaultAuthValidationClientFactory;
|
|
78
100
|
|
|
79
101
|
/** Test seam for command-level auth tests; production always uses LinearClient. */
|
|
@@ -96,10 +118,11 @@ export function selectLoginKey(
|
|
|
96
118
|
|
|
97
119
|
/** Validate a credential and prove that an explicitly requested slug matches it. */
|
|
98
120
|
export async function validateAuthCredential(
|
|
99
|
-
|
|
121
|
+
credential: string,
|
|
100
122
|
requestedWorkspace?: string,
|
|
123
|
+
kind: "api-key" | "oauth-access-token" = "api-key",
|
|
101
124
|
): Promise<AuthIdentity> {
|
|
102
|
-
const client = authValidationClientFactory(
|
|
125
|
+
const client = authValidationClientFactory(credential, kind);
|
|
103
126
|
let me;
|
|
104
127
|
let org;
|
|
105
128
|
try {
|
|
@@ -108,13 +131,17 @@ export async function validateAuthCredential(
|
|
|
108
131
|
} catch (err) {
|
|
109
132
|
const normalized = normalizeError(err);
|
|
110
133
|
if (normalized.code === "auth" || normalized.code === "forbidden") {
|
|
111
|
-
throw authError(
|
|
134
|
+
throw authError(
|
|
135
|
+
kind === "api-key"
|
|
136
|
+
? "That API key was rejected by Linear."
|
|
137
|
+
: "Linear rejected the OAuth access token.",
|
|
138
|
+
);
|
|
112
139
|
}
|
|
113
140
|
throw normalized;
|
|
114
141
|
}
|
|
115
142
|
if (requestedWorkspace && requestedWorkspace !== org.urlKey) {
|
|
116
143
|
throw usageError(
|
|
117
|
-
`--workspace '${requestedWorkspace}' does not match the
|
|
144
|
+
`--workspace '${requestedWorkspace}' does not match the credential's workspace '${org.urlKey}'. Store credentials under their Linear workspace slug.`,
|
|
118
145
|
);
|
|
119
146
|
}
|
|
120
147
|
return {
|
|
@@ -148,57 +175,150 @@ export function registerMeta(program: Command): void {
|
|
|
148
175
|
// login -------------------------------------------------------------------
|
|
149
176
|
auth
|
|
150
177
|
.command("login")
|
|
151
|
-
.description("
|
|
152
|
-
.option("--key <key>", "API key (
|
|
178
|
+
.description("Authenticate in the browser with OAuth, or store a personal API key")
|
|
179
|
+
.option("--key <key>", "use a personal API key ('-' reads it from stdin)")
|
|
153
180
|
.option("--plaintext", "Store the key in the config file (0600) instead of the system keyring")
|
|
181
|
+
.option("--no-browser", "print the authorization URL instead of opening it")
|
|
182
|
+
.option("--read-only", "request read-only OAuth access")
|
|
183
|
+
.option("--admin", "explicitly add the OAuth admin scope")
|
|
184
|
+
.option("--timeout <seconds>", "seconds to wait for the loopback callback", "120")
|
|
185
|
+
.option("--client-id <id>", "OAuth client ID (defaults to the packaged CLI app)")
|
|
186
|
+
.option("--redirect-uri <uri>", "registered HTTP loopback callback URI")
|
|
154
187
|
.action(
|
|
155
|
-
// The global `--workspace <slug>` selects the slug to store under
|
|
156
|
-
// (default: derived from the key's organization urlKey).
|
|
157
188
|
action(async (ctx: Context, opts) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
if (ctx.options.accessToken !== undefined) {
|
|
190
|
+
throw usageError(
|
|
191
|
+
"`auth login` cannot persist an injected access token. Remove --access-token and use browser OAuth, or use --key - for a personal API key.",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
162
194
|
let key: string | undefined = selectLoginKey(opts.key, ctx.options.apiKey);
|
|
163
|
-
if (key
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
195
|
+
if (key !== undefined) {
|
|
196
|
+
if (
|
|
197
|
+
opts.readOnly ||
|
|
198
|
+
opts.admin ||
|
|
199
|
+
opts.browser === false ||
|
|
200
|
+
opts.redirectUri ||
|
|
201
|
+
opts.clientId
|
|
202
|
+
) {
|
|
203
|
+
throw usageError("OAuth-only options cannot be combined with --key or --api-key.");
|
|
204
|
+
}
|
|
205
|
+
if (key === "-") key = readStdinSync();
|
|
206
|
+
else
|
|
207
|
+
ctx.output.warn(
|
|
208
|
+
"An API key on the command line is visible to other processes and shell history; prefer --key - (stdin).",
|
|
209
|
+
);
|
|
210
|
+
key = key.trim();
|
|
211
|
+
if (!key) throw usageError("No API key provided.");
|
|
212
|
+
const identity = await validateAuthCredential(key, ctx.options.workspace);
|
|
213
|
+
const { user: me, organization: org } = identity;
|
|
214
|
+
const { path, storage, keyringLabel } = writeCredential(org.urlKey, key, {
|
|
215
|
+
plaintext: opts.plaintext === true,
|
|
216
|
+
});
|
|
217
|
+
const where =
|
|
218
|
+
storage === "keychain"
|
|
219
|
+
? `Credential saved to the ${keyringLabel}; ${path} records the workspace.`
|
|
220
|
+
: `Credential saved to ${path} (plaintext, 0600).`;
|
|
221
|
+
ctx.output.emit(
|
|
222
|
+
{
|
|
223
|
+
success: true,
|
|
224
|
+
credentialType: "api-key",
|
|
225
|
+
workspace: org.urlKey,
|
|
226
|
+
user: me,
|
|
227
|
+
storage,
|
|
228
|
+
path,
|
|
229
|
+
},
|
|
230
|
+
() =>
|
|
231
|
+
ctx.output.success(
|
|
232
|
+
`Authenticated as ${me.name} <${me.email}> for workspace '${org.urlKey}'. ${where}`,
|
|
233
|
+
),
|
|
170
234
|
);
|
|
171
|
-
|
|
172
|
-
key = await promptSecret(ctx, "Linear API key:", { required: true });
|
|
235
|
+
return;
|
|
173
236
|
}
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
workspace: slug,
|
|
193
|
-
user: { id: me.id, name: me.name, email: me.email },
|
|
194
|
-
storage,
|
|
195
|
-
path,
|
|
196
|
-
},
|
|
197
|
-
() =>
|
|
198
|
-
ctx.output.success(
|
|
199
|
-
`Authenticated as ${me.name} <${me.email}> for workspace '${slug}'. ${where}`,
|
|
200
|
-
),
|
|
237
|
+
|
|
238
|
+
if (opts.plaintext) {
|
|
239
|
+
throw usageError(
|
|
240
|
+
"OAuth credentials are keyring-only; --plaintext requires --key or --api-key.",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (opts.readOnly && opts.admin) {
|
|
244
|
+
throw usageError("--read-only cannot be combined with --admin.");
|
|
245
|
+
}
|
|
246
|
+
const timeoutSeconds = Number(opts.timeout);
|
|
247
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
|
|
248
|
+
throw usageError("--timeout must be a positive number of seconds.");
|
|
249
|
+
}
|
|
250
|
+
const clientId = String(
|
|
251
|
+
opts.clientId ?? process.env.LINEAR_OAUTH_CLIENT_ID ?? DEFAULT_OAUTH_CLIENT_ID,
|
|
252
|
+
).trim();
|
|
253
|
+
const redirectUri = String(
|
|
254
|
+
opts.redirectUri ?? process.env.LINEAR_OAUTH_REDIRECT_URI ?? DEFAULT_OAUTH_REDIRECT_URI,
|
|
201
255
|
);
|
|
256
|
+
const scopes = opts.readOnly
|
|
257
|
+
? ["read"]
|
|
258
|
+
: opts.admin
|
|
259
|
+
? [...DEFAULT_OAUTH_SCOPES, "admin"]
|
|
260
|
+
: [...DEFAULT_OAUTH_SCOPES];
|
|
261
|
+
const pkce = createPkceRequest();
|
|
262
|
+
const callback = await startLoopbackCallback({
|
|
263
|
+
redirectUri,
|
|
264
|
+
state: pkce.state,
|
|
265
|
+
timeoutMs: timeoutSeconds * 1000,
|
|
266
|
+
});
|
|
267
|
+
try {
|
|
268
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
269
|
+
clientId,
|
|
270
|
+
redirectUri: callback.redirectUri,
|
|
271
|
+
scopes,
|
|
272
|
+
challenge: pkce.challenge,
|
|
273
|
+
state: pkce.state,
|
|
274
|
+
});
|
|
275
|
+
if (opts.browser === false) {
|
|
276
|
+
ctx.output.info(`Open this URL to authenticate with Linear:\n${authorizationUrl}`);
|
|
277
|
+
} else {
|
|
278
|
+
ctx.output.info("Opening Linear in your browser…");
|
|
279
|
+
await openUrl(authorizationUrl);
|
|
280
|
+
}
|
|
281
|
+
const code = await callback.wait;
|
|
282
|
+
const token = await exchangeAuthorizationCode({
|
|
283
|
+
code,
|
|
284
|
+
verifier: pkce.verifier,
|
|
285
|
+
clientId,
|
|
286
|
+
redirectUri: callback.redirectUri,
|
|
287
|
+
});
|
|
288
|
+
const identity = await validateAuthCredential(
|
|
289
|
+
token.accessToken,
|
|
290
|
+
ctx.options.workspace,
|
|
291
|
+
"oauth-access-token",
|
|
292
|
+
);
|
|
293
|
+
const credential: OAuthUserCredential = {
|
|
294
|
+
version: 1,
|
|
295
|
+
kind: "oauth-user",
|
|
296
|
+
actor: "user",
|
|
297
|
+
...token,
|
|
298
|
+
clientId,
|
|
299
|
+
workspace: identity.organization,
|
|
300
|
+
user: identity.user,
|
|
301
|
+
};
|
|
302
|
+
const saved = writeOAuthCredential(credential);
|
|
303
|
+
ctx.output.emit(
|
|
304
|
+
{
|
|
305
|
+
success: true,
|
|
306
|
+
credentialType: "oauth-user",
|
|
307
|
+
workspace: credential.workspace.urlKey,
|
|
308
|
+
user: credential.user,
|
|
309
|
+
storage: saved.storage,
|
|
310
|
+
scopes: credential.scopes,
|
|
311
|
+
expiresAt: new Date(credential.expiresAt).toISOString(),
|
|
312
|
+
path: saved.path,
|
|
313
|
+
},
|
|
314
|
+
() =>
|
|
315
|
+
ctx.output.success(
|
|
316
|
+
`Authenticated as ${credential.user.name} <${credential.user.email}> for workspace '${credential.workspace.urlKey}'. OAuth credentials saved to the ${saved.keyringLabel}.`,
|
|
317
|
+
),
|
|
318
|
+
);
|
|
319
|
+
} finally {
|
|
320
|
+
await callback.close();
|
|
321
|
+
}
|
|
202
322
|
}),
|
|
203
323
|
);
|
|
204
324
|
|
|
@@ -254,6 +374,7 @@ export function registerMeta(program: Command): void {
|
|
|
254
374
|
[
|
|
255
375
|
{ key: "slug", header: "Workspace", value: (e) => e.slug },
|
|
256
376
|
{ key: "isDefault", header: "Default", value: (e) => (e.isDefault ? "yes" : "") },
|
|
377
|
+
{ key: "credentialType", header: "Type", value: (e) => e.credentialType },
|
|
257
378
|
{ key: "storage", header: "Storage", value: (e) => e.storage },
|
|
258
379
|
],
|
|
259
380
|
entries,
|
|
@@ -281,6 +402,11 @@ export function registerMeta(program: Command): void {
|
|
|
281
402
|
.action(
|
|
282
403
|
action(async (ctx: Context) => {
|
|
283
404
|
const c = ctx.config;
|
|
405
|
+
if (c.accessToken) {
|
|
406
|
+
throw usageError(
|
|
407
|
+
"`auth token` exports stored API keys only. OAuth access tokens are never exported; use the authenticated command directly.",
|
|
408
|
+
);
|
|
409
|
+
}
|
|
284
410
|
if (!c.apiKey) {
|
|
285
411
|
// Surface the precise selection error (ambiguous / unstored slug) if any.
|
|
286
412
|
throw (
|
|
@@ -298,25 +424,45 @@ export function registerMeta(program: Command): void {
|
|
|
298
424
|
// status ------------------------------------------------------------------
|
|
299
425
|
auth
|
|
300
426
|
.command("status")
|
|
301
|
-
.description("Show where the
|
|
427
|
+
.description("Show where the active credential is resolved from (value redacted)")
|
|
302
428
|
.action(
|
|
303
429
|
action(async (ctx) => {
|
|
304
430
|
const c = ctx.config;
|
|
431
|
+
const credential = c.accessToken ?? c.apiKey;
|
|
432
|
+
const credentialType = c.oauthCredential
|
|
433
|
+
? "oauth-user"
|
|
434
|
+
: c.accessToken
|
|
435
|
+
? "oauth-access-token"
|
|
436
|
+
: c.apiKey
|
|
437
|
+
? "api-key"
|
|
438
|
+
: null;
|
|
439
|
+
const source = c.accessToken ? c.accessTokenSource : c.apiKeySource;
|
|
305
440
|
const backend = keyring();
|
|
306
441
|
ctx.output.detail(
|
|
307
442
|
{
|
|
308
|
-
authenticated: !!
|
|
309
|
-
|
|
443
|
+
authenticated: !!credential,
|
|
444
|
+
credentialType,
|
|
445
|
+
source,
|
|
310
446
|
workspace: c.credentialWorkspace ?? null,
|
|
311
|
-
key: redactKey(
|
|
447
|
+
key: redactKey(credential),
|
|
312
448
|
keyring: backend?.name ?? null,
|
|
449
|
+
scopes: c.oauthCredential?.scopes ?? null,
|
|
450
|
+
expiresAt: c.oauthCredential
|
|
451
|
+
? new Date(c.oauthCredential.expiresAt).toISOString()
|
|
452
|
+
: null,
|
|
313
453
|
},
|
|
314
454
|
[
|
|
315
|
-
["Authenticated", !!
|
|
316
|
-
["
|
|
455
|
+
["Authenticated", !!credential],
|
|
456
|
+
["Credential type", credentialType ?? "(none)"],
|
|
457
|
+
["Source", source],
|
|
317
458
|
["Workspace", c.credentialWorkspace ?? "(none)"],
|
|
318
|
-
["
|
|
459
|
+
["Credential", redactKey(credential)],
|
|
319
460
|
["Keyring", backend ? backend.label : "(none on this platform)"],
|
|
461
|
+
["Scopes", c.oauthCredential?.scopes.join(", ")],
|
|
462
|
+
[
|
|
463
|
+
"Expires",
|
|
464
|
+
c.oauthCredential ? new Date(c.oauthCredential.expiresAt).toISOString() : undefined,
|
|
465
|
+
],
|
|
320
466
|
],
|
|
321
467
|
);
|
|
322
468
|
// A plaintext key on a machine that has a keyring is worth one nudge
|
|
@@ -350,10 +496,11 @@ export function registerMeta(program: Command): void {
|
|
|
350
496
|
auth
|
|
351
497
|
.command("logout")
|
|
352
498
|
.description("Remove a stored workspace credential (select with --workspace <slug>)")
|
|
499
|
+
.option("--local-only", "remove local credentials without OAuth revocation")
|
|
353
500
|
.action(
|
|
354
501
|
// Targets the global `--workspace <slug>`. When omitted, the sole
|
|
355
502
|
// configured workspace is used; if several exist, this errors.
|
|
356
|
-
action(async (ctx: Context) => {
|
|
503
|
+
action(async (ctx: Context, opts) => {
|
|
357
504
|
let slug: string | undefined = ctx.options.workspace;
|
|
358
505
|
if (!slug) {
|
|
359
506
|
const configured = listCredentials();
|
|
@@ -368,11 +515,35 @@ export function registerMeta(program: Command): void {
|
|
|
368
515
|
);
|
|
369
516
|
}
|
|
370
517
|
if (!(await confirmDestructive(ctx, `Remove credential for workspace '${slug}'?`))) return;
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
518
|
+
const entry = listCredentials().find((candidate) => candidate.slug === slug);
|
|
519
|
+
let revocation: "revoked" | "already-revoked" | "skipped" = "skipped";
|
|
520
|
+
if (entry?.credentialType === "oauth-user" && opts.localOnly !== true) {
|
|
521
|
+
const oauth = readOAuthCredential(slug);
|
|
522
|
+
if (oauth) {
|
|
523
|
+
revocation = await revokeOAuthToken({
|
|
524
|
+
token: oauth.refreshToken,
|
|
525
|
+
tokenTypeHint: "refresh_token",
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const local =
|
|
530
|
+
entry?.credentialType === "oauth-user"
|
|
531
|
+
? removeOAuthCredential(slug)
|
|
532
|
+
: { removed: removeCredential(slug), fallbackCredentialType: null };
|
|
533
|
+
ctx.output.emit(
|
|
534
|
+
{
|
|
535
|
+
success: true,
|
|
536
|
+
workspace: slug,
|
|
537
|
+
removed: local.removed,
|
|
538
|
+
revocation,
|
|
539
|
+
fallbackCredentialType: local.fallbackCredentialType,
|
|
540
|
+
},
|
|
541
|
+
() =>
|
|
542
|
+
local.removed
|
|
543
|
+
? ctx.output.success(
|
|
544
|
+
`Removed workspace '${slug}'.${revocation === "revoked" ? " OAuth access was revoked." : revocation === "already-revoked" ? " OAuth access was already revoked." : ""}${local.fallbackCredentialType ? " The existing personal API-key profile is active again." : ""}`,
|
|
545
|
+
)
|
|
546
|
+
: ctx.output.info(`No credential for workspace '${slug}' to remove.`),
|
|
376
547
|
);
|
|
377
548
|
}),
|
|
378
549
|
);
|
|
@@ -393,6 +564,7 @@ export function registerMeta(program: Command): void {
|
|
|
393
564
|
const c = resolveConfig({
|
|
394
565
|
flags: {
|
|
395
566
|
apiKey: ctx.options.apiKey,
|
|
567
|
+
accessToken: ctx.options.accessToken,
|
|
396
568
|
// Still the *flag* value (so `teamSource` stays honest), just
|
|
397
569
|
// narrowed: `--team` is repeatable on the issue queries.
|
|
398
570
|
team: firstTeam(ctx.options.team),
|
|
@@ -414,6 +586,8 @@ export function registerMeta(program: Command): void {
|
|
|
414
586
|
{
|
|
415
587
|
apiKey: redactKey(c.apiKey),
|
|
416
588
|
apiKeySource: c.apiKeySource,
|
|
589
|
+
accessToken: redactKey(c.accessToken),
|
|
590
|
+
accessTokenSource: c.accessTokenSource,
|
|
417
591
|
credentialWorkspace: c.credentialWorkspace ?? null,
|
|
418
592
|
team: c.team ?? null,
|
|
419
593
|
workspace: c.workspace ?? null,
|
|
@@ -425,6 +599,7 @@ export function registerMeta(program: Command): void {
|
|
|
425
599
|
},
|
|
426
600
|
[
|
|
427
601
|
["API key", `${redactKey(c.apiKey)} (${c.apiKeySource})`],
|
|
602
|
+
["OAuth access token", `${redactKey(c.accessToken)} (${c.accessTokenSource})`],
|
|
428
603
|
["Credential workspace", c.credentialWorkspace ?? "(none)"],
|
|
429
604
|
["Team", show("team", c.team)],
|
|
430
605
|
["Workspace", show("workspace", c.workspace)],
|