@notis_ai/cli 0.2.8 → 0.2.10

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.
@@ -1,19 +1,52 @@
1
1
  import { COMPOSIO_SEARCH_TOOLS, healthCheck, probeAuth } from './helpers.js';
2
2
  import { findCommandSpec, formatDescribe } from '../runtime/help.js';
3
- import { createExpiredAuthError } from '../runtime/desktop-auth.js';
4
- import { isJwtExpired } from '../runtime/profiles.js';
3
+ import { createExpiredAuthError, getDesktopAuthRecovery } from '../runtime/desktop-auth.js';
4
+ import {
5
+ credentialIsExpired,
6
+ getJwtCanonicalUserId,
7
+ getProfile,
8
+ loadConfig,
9
+ } from '../runtime/profiles.js';
10
+ import { ensureFreshOAuthCredential } from '../runtime/oauth.js';
11
+
12
+ export const DOCTOR_TOOL_ROUNDTRIP_TIMEOUT_MS = 90_000;
13
+
14
+ export function doctorToolRoundtripRuntime(runtime) {
15
+ return {
16
+ ...runtime,
17
+ timeoutMs: Math.max(runtime.timeoutMs || 0, DOCTOR_TOOL_ROUNDTRIP_TIMEOUT_MS),
18
+ };
19
+ }
5
20
 
6
21
  async function doctorHandler(ctx) {
7
22
  const checks = {
8
23
  config: 'ok',
9
24
  auth: 'missing',
25
+ identity: 'ok',
10
26
  health: 'unknown',
11
27
  tool_roundtrip: 'unknown',
12
28
  };
13
29
 
30
+ if (ctx.runtime.credentialKind === 'oauth') {
31
+ try {
32
+ await ensureFreshOAuthCredential(ctx.runtime);
33
+ } catch {
34
+ // Doctor still reports the remaining health and recovery checks when a
35
+ // refresh endpoint is unavailable or rejects the stored credential.
36
+ }
37
+ }
38
+ let profile = getProfile(loadConfig(ctx.runtime.worktreeRuntime), ctx.runtime.profileName);
14
39
  checks.auth = ctx.runtime.jwt
15
- ? (isJwtExpired(ctx.runtime.jwt) ? 'expired' : 'configured')
40
+ ? (credentialIsExpired(ctx.runtime, profile) ? 'expired' : 'configured')
16
41
  : 'missing';
42
+ const desktopUserId = getJwtCanonicalUserId(profile.jwt);
43
+ if (
44
+ desktopUserId
45
+ && profile.oauth_user_id
46
+ && desktopUserId !== profile.oauth_user_id
47
+ ) {
48
+ checks.identity = 'error';
49
+ }
17
50
 
18
51
  try {
19
52
  await healthCheck(ctx.runtime);
@@ -24,8 +57,15 @@ async function doctorHandler(ctx) {
24
57
 
25
58
  if (ctx.runtime.jwt) {
26
59
  try {
27
- const payload = await probeAuth(ctx.runtime);
60
+ // Tool discovery may have to query several connected MCP servers on a
61
+ // cold local backend. A diagnostic must not report a false failure just
62
+ // because that legitimate roundtrip exceeds the general 30s default.
63
+ const payload = await probeAuth(doctorToolRoundtripRuntime(ctx.runtime));
28
64
  checks.tool_roundtrip = Array.isArray(payload.toolkit_connection_statuses) ? 'ok' : 'error';
65
+ profile = getProfile(loadConfig(ctx.runtime.worktreeRuntime), ctx.runtime.profileName);
66
+ checks.auth = ctx.runtime.jwt
67
+ ? (credentialIsExpired(ctx.runtime, profile) ? 'expired' : 'configured')
68
+ : 'missing';
29
69
  } catch {
30
70
  checks.tool_roundtrip = 'error';
31
71
  }
@@ -33,7 +73,7 @@ async function doctorHandler(ctx) {
33
73
 
34
74
  const hints = [];
35
75
  if (checks.auth === 'missing') {
36
- hints.push({ command: 'Open the Notis desktop app and sign in', reason: 'Configure CLI credentials' });
76
+ hints.push(...getDesktopAuthRecovery(ctx.runtime, { mode: 'missing' }).hints);
37
77
  } else if (checks.auth === 'expired') {
38
78
  hints.push(...createExpiredAuthError(ctx.runtime).hints);
39
79
  }
@@ -43,12 +83,27 @@ async function doctorHandler(ctx) {
43
83
  if (checks.tool_roundtrip === 'error') {
44
84
  hints.push({ command: 'notis whoami', reason: 'Verify your account and permissions' });
45
85
  }
86
+ if (checks.identity === 'error') {
87
+ hints.push({
88
+ command: 'notis logout',
89
+ reason: 'Desktop and OAuth credentials identify different accounts; remove the independent OAuth grant or sign Desktop back into the same account',
90
+ });
91
+ }
46
92
 
47
93
  return ctx.output.emitSuccess({
48
94
  command: ctx.spec.command_path.join(' '),
49
95
  data: {
50
96
  profile: ctx.runtime.profileName,
51
97
  api_base: ctx.runtime.apiBase,
98
+ credential_source: ctx.runtime.credentialKind || null,
99
+ ...(ctx.runtime.credentialKind === 'oauth'
100
+ ? {
101
+ oauth_client_id: profile.oauth_client_id || null,
102
+ oauth_scopes: profile.oauth_scopes || [],
103
+ oauth_access_expires_at: profile.oauth_access_expires_at || null,
104
+ oauth_refresh_expires_at: profile.oauth_refresh_expires_at || null,
105
+ }
106
+ : {}),
52
107
  checks,
53
108
  },
54
109
  humanSummary: `Doctor checks completed for profile ${ctx.runtime.profileName}`,
@@ -72,16 +127,23 @@ function decodeJwtUserId(jwt) {
72
127
  }
73
128
  }
74
129
 
130
+ export function activeRuntimeUserId(runtime) {
131
+ return runtime.credentialKind === 'oauth'
132
+ ? runtime.oauthUserId
133
+ : decodeJwtUserId(runtime.jwt);
134
+ }
135
+
75
136
  async function whoamiHandler(ctx) {
76
137
  const payload = await probeAuth(ctx.runtime);
77
138
  const toolkits = payload.toolkit_connection_statuses || [];
78
- const userId = decodeJwtUserId(ctx.runtime.jwt);
139
+ const userId = activeRuntimeUserId(ctx.runtime);
79
140
 
80
141
  return ctx.output.emitSuccess({
81
142
  command: ctx.spec.command_path.join(' '),
82
143
  data: {
83
144
  profile: ctx.runtime.profileName,
84
145
  api_base: ctx.runtime.apiBase,
146
+ credential_source: ctx.runtime.credentialKind || null,
85
147
  user_id: userId,
86
148
  toolkit_count: toolkits.length,
87
149
  toolkits: toolkits.map((t) => t.toolkit),
@@ -4,7 +4,12 @@ import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import { CliError, EXIT_CODES } from '../runtime/errors.js';
6
6
  import { getDesktopAuthRecovery, waitForDesktopAuth } from '../runtime/desktop-auth.js';
7
- import { getJwt, isJwtExpired, loadConfig } from '../runtime/profiles.js';
7
+ import {
8
+ credentialIsExpired,
9
+ getProfile,
10
+ loadConfig,
11
+ } from '../runtime/profiles.js';
12
+ import { ensureFreshOAuthCredential, loginWithOAuth } from '../runtime/oauth.js';
8
13
 
9
14
  const HERE = dirname(fileURLToPath(import.meta.url));
10
15
  const BUNDLED_BRIEF_PATH = join(HERE, '..', '..', 'skills', 'notis-onboarding', 'BRIEF.md');
@@ -85,7 +90,20 @@ async function requestSignupLink(apiBase, { email, useCases }) {
85
90
  async function startHandler(ctx) {
86
91
  const { runtime, options, output } = ctx;
87
92
  const apiBase = runtime.apiBase;
88
- const authenticated = Boolean(runtime.jwt) && !isJwtExpired(runtime.jwt);
93
+ if (runtime.credentialKind === 'oauth') {
94
+ try {
95
+ await ensureFreshOAuthCredential(runtime);
96
+ } catch {
97
+ // A failed refresh is equivalent to no usable session here. Interactive
98
+ // starts may still authorize again; brief-only and agent runs surface
99
+ // the normal authentication recovery below.
100
+ }
101
+ }
102
+ let authenticated = Boolean(runtime.jwt)
103
+ && !credentialIsExpired(
104
+ runtime,
105
+ getProfile(loadConfig(runtime.worktreeRuntime), runtime.profileName),
106
+ );
89
107
 
90
108
  // Safe to re-run: an already-authenticated machine skips straight to the brief.
91
109
  // Agents retry commands, and a second signup would invalidate the first email.
@@ -108,6 +126,54 @@ async function startHandler(ctx) {
108
126
  });
109
127
  }
110
128
 
129
+ if (
130
+ !options.email
131
+ && !runtime.agentMode
132
+ && !runtime.nonInteractive
133
+ && options.wait !== false
134
+ ) {
135
+ let authorization;
136
+ let authorizationError;
137
+ try {
138
+ authorization = await loginWithOAuth(runtime, { browser: true }, output);
139
+ } catch (error) {
140
+ // Keep the original OAuth failure while checking whether another process
141
+ // completed authentication before this attempt failed.
142
+ authorizationError = error;
143
+ authorization = null;
144
+ }
145
+ if (authorization?.agentAuthorization) {
146
+ return output.emitSuccess({
147
+ command: 'start',
148
+ data: { authenticated: false, ...authorization.agentAuthorization },
149
+ humanSummary: 'Open the authorization URL to finish signing in.',
150
+ renderHuman: () => `Authorize Notis CLI: ${authorization.agentAuthorization.authorize_url}`,
151
+ });
152
+ }
153
+ // A failed authorization leaves the stale credential that got us here in
154
+ // place, so re-apply the same expiry test used above rather than reporting
155
+ // a machine as signed in on the strength of a dead token.
156
+ authenticated = Boolean(runtime.jwt)
157
+ && !credentialIsExpired(runtime, getProfile(loadConfig(runtime.worktreeRuntime), runtime.profileName));
158
+ if (authenticated) {
159
+ const brief = await fetchBrief(apiBase, runtime.timeoutMs);
160
+ return output.emitSuccess({
161
+ command: 'start',
162
+ data: {
163
+ authenticated: true,
164
+ credential_source: runtime.credentialKind,
165
+ brief: brief.markdown,
166
+ brief_source: brief.source,
167
+ },
168
+ humanSummary: 'Notis CLI is authenticated. Follow the onboarding brief below.',
169
+ renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
170
+ });
171
+ }
172
+ if (authorizationError) {
173
+ throw authorizationError;
174
+ }
175
+ }
176
+
111
177
  if (!options.email) {
112
178
  // The one point in the flow that has to stop and talk to the human.
113
179
  throw new CliError({
@@ -143,8 +209,29 @@ async function startHandler(ctx) {
143
209
  const timeoutMs = Number(options.waitTimeoutMs) > 0 ? Number(options.waitTimeoutMs) : 300_000;
144
210
  const jwt = await waitForDesktopAuth({
145
211
  loadConfig,
146
- getJwt,
147
- isJwtExpired,
212
+ getJwt: (config, profileName) => {
213
+ const profile = getProfile(config, profileName);
214
+ if (
215
+ profile.jwt
216
+ && !credentialIsExpired(
217
+ { credentialKind: 'desktop', jwt: profile.jwt },
218
+ profile,
219
+ )
220
+ ) {
221
+ return profile.jwt;
222
+ }
223
+ if (
224
+ profile.oauth_access_token
225
+ && !credentialIsExpired(
226
+ { credentialKind: 'oauth', jwt: profile.oauth_access_token },
227
+ profile,
228
+ )
229
+ ) {
230
+ return profile.oauth_access_token;
231
+ }
232
+ return undefined;
233
+ },
234
+ isJwtExpired: () => false,
148
235
  profileName: runtime.profileName,
149
236
  timeoutMs,
150
237
  onTick: (remaining) => {
@@ -165,7 +252,8 @@ async function startHandler(ctx) {
165
252
  command: `Install Notis Desktop: ${signup.desktop_download_url || 'https://notis.ai/channels/desktop-app'}`,
166
253
  reason: 'The desktop app is what writes the CLI credential',
167
254
  },
168
- { command: 'notis start --brief-only', reason: 'Resume once the desktop app is signed in' },
255
+ { command: 'notis login', reason: 'Authorize this machine directly in a browser' },
256
+ { command: 'notis start --brief-only', reason: 'Resume once authentication is complete' },
169
257
  { command: 'notis doctor', reason: 'Re-check config, auth, and API reachability' },
170
258
  ],
171
259
  });
@@ -183,7 +271,7 @@ async function startHandler(ctx) {
183
271
  export const onboardingCommandSpecs = [
184
272
  {
185
273
  command_path: ['start'],
186
- summary: 'Create or access a Notis account and wait for the desktop app to sign the CLI in.',
274
+ summary: 'Create or access a Notis account and authorize the CLI.',
187
275
  when_to_use:
188
276
  'Run this first on a new machine, before anything that needs auth. Safe to re-run: an already-signed-in machine just reprints the onboarding brief.',
189
277
  args_schema: {
@@ -288,12 +288,13 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions,
288
288
  /**
289
289
  * Start the dev server for one or more apps.
290
290
  *
291
- * @param {{apps: Array<{slug: string, projectDir: string, appId?: string, targetAppId?: string, userId?: string}>, port: number, watch?: boolean, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
291
+ * @param {{apps: Array<{slug: string, projectDir: string, appId?: string, targetAppId?: string, userId?: string}>, port: number, watch?: boolean, sessionsFilePath?: string, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
292
292
  */
293
293
  export async function startAppDevServer({
294
294
  apps,
295
295
  port,
296
296
  watch = true,
297
+ sessionsFilePath,
297
298
  harness = {},
298
299
  log = (msg) => process.stdout.write(`${msg}\n`),
299
300
  logError = (msg) => process.stderr.write(`${msg}\n`),
@@ -466,7 +467,7 @@ export async function startAppDevServer({
466
467
  devSlug: state.slug,
467
468
  targetAppId: appId,
468
469
  lastHeartbeatAt: now,
469
- });
470
+ }, sessionsFilePath);
470
471
  const response = {
471
472
  ok: true,
472
473
  app_id: appId,
@@ -1,6 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
3
  import { dirname, join, parse as parsePath } from 'node:path';
5
4
  import { CONFIG_DIR } from './profiles.js';
6
5
 
@@ -9,37 +8,6 @@ export const APP_DEV_SESSIONS_FILE = DEFAULT_APP_DEV_SESSIONS_FILE;
9
8
  export const APP_DEV_SESSIONS_VERSION = 1;
10
9
  export const APP_DEV_SESSION_MOUNT_ACKS_VERSION = 1;
11
10
 
12
- /**
13
- * Walk up from `startDir` looking for a workspace-scoped dev-sessions registry.
14
- *
15
- * The Notis desktop (and Conductor) reads a PER-WORKSPACE registry at
16
- * `<workspace>/.context/app-dev-sessions.json` (it points its own `apps dev`
17
- * at it via `NOTIS_APP_DEV_SESSIONS_FILE`). When a coding agent runs
18
- * `apps dev` manually it does not inherit that env var, so its session would
19
- * otherwise land in the global `~/.notis` registry and never appear in the
20
- * desktop's Local development sidebar. Resolving the nearest `.context`
21
- * registry keeps agent-run and desktop-run sessions in the same file.
22
- *
23
- * Returns the registry path if a `.context/` directory is found in `startDir`
24
- * or an ancestor (stopping at the home directory / filesystem root), else null.
25
- */
26
- export function findWorkspaceAppDevSessionsFile(startDir = process.cwd()) {
27
- let dir = startDir;
28
- const home = homedir();
29
- const { root } = parsePath(dir);
30
- // Bounded walk; `.context` lives at the workspace root, never above $HOME.
31
- for (let depth = 0; depth < 64; depth += 1) {
32
- if (existsSync(join(dir, '.context'))) {
33
- return join(dir, '.context', 'app-dev-sessions.json');
34
- }
35
- if (dir === home || dir === root) break;
36
- const parent = dirname(dir);
37
- if (parent === dir) break;
38
- dir = parent;
39
- }
40
- return null;
41
- }
42
-
43
11
  export function getAppDevSessionsFile(filePath) {
44
12
  if (filePath) {
45
13
  return filePath;
@@ -48,10 +16,6 @@ export function getAppDevSessionsFile(filePath) {
48
16
  if (typeof envPath === 'string' && envPath.trim()) {
49
17
  return envPath.trim();
50
18
  }
51
- const workspaceFile = findWorkspaceAppDevSessionsFile();
52
- if (workspaceFile) {
53
- return workspaceFile;
54
- }
55
19
  return DEFAULT_APP_DEV_SESSIONS_FILE;
56
20
  }
57
21
 
@@ -173,7 +137,7 @@ function normalizeMountAcknowledgements(raw) {
173
137
  typeof acknowledgement.devSlug === 'string' &&
174
138
  typeof acknowledgement.mountNonce === 'string' &&
175
139
  typeof acknowledgement.acknowledgedAt === 'string' &&
176
- acknowledgement.stage === 'listed',
140
+ ['listed', 'rendered'].includes(acknowledgement.stage),
177
141
  );
178
142
  }
179
143
 
@@ -218,12 +182,13 @@ export function removeAppDevSessionMountAcknowledgements(sessionId, sessionsFile
218
182
  return writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath);
219
183
  }
220
184
 
221
- function mountAcknowledgementKey(value) {
222
- return `${value.sessionId}:${value.appId}:${value.devSlug}:${value.mountNonce}`;
185
+ function mountAcknowledgementKey(value, fallbackStage = 'listed') {
186
+ return `${value.sessionId}:${value.appId}:${value.devSlug}:${value.mountNonce}:${value.stage || fallbackStage}`;
223
187
  }
224
188
 
225
189
  export async function waitForAppDevSessionMountAcknowledgements(expectedSessions, options = {}) {
226
190
  const expected = Array.isArray(expectedSessions) ? expectedSessions : [expectedSessions];
191
+ const stage = options.stage === 'rendered' ? 'rendered' : 'listed';
227
192
  const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(0, options.timeoutMs) : 15_000;
228
193
  const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
229
194
  ? Math.max(1, options.pollIntervalMs)
@@ -237,7 +202,9 @@ export async function waitForAppDevSessionMountAcknowledgements(expectedSessions
237
202
  while (true) {
238
203
  const registry = readAppDevSessionMountAcknowledgements(options.sessionsFilePath);
239
204
  const acknowledgedKeys = new Set(registry.acknowledgements.map(mountAcknowledgementKey));
240
- const missing = expected.filter((session) => !acknowledgedKeys.has(mountAcknowledgementKey(session)));
205
+ const missing = expected.filter(
206
+ (session) => !acknowledgedKeys.has(mountAcknowledgementKey(session, stage)),
207
+ );
241
208
  if (missing.length === 0) {
242
209
  return { mounted: true, missing: [], acknowledgements: registry.acknowledgements };
243
210
  }
@@ -249,3 +216,10 @@ export async function waitForAppDevSessionMountAcknowledgements(expectedSessions
249
216
  await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
250
217
  }
251
218
  }
219
+
220
+ export function waitForAppDevSessionRenderAcknowledgements(expectedSessions, options = {}) {
221
+ return waitForAppDevSessionMountAcknowledgements(expectedSessions, {
222
+ ...options,
223
+ stage: 'rendered',
224
+ });
225
+ }
@@ -1,4 +1,5 @@
1
1
  // Auto-generated on npm publish.
2
- // Committed value is 'local' so in-repo runs target localhost.
3
- // scripts/set-cli-mode.js rewrites this to 'published' before the npm publish step.
4
- export const MODE = 'local';
2
+ // Committed value is 'published' so the CLI defaults to the live Notis API.
3
+ // Localhost overrides come only from the worktree test lease (`./dev.sh`).
4
+ // scripts/set-cli-mode.js can rewrite this for publish/labeling experiments.
5
+ export const MODE = 'published';
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * CLI mode detection for `notis apps dev`.
3
3
  *
4
- * The repo-local CLI package targets localhost; the published npm CLI package
5
- * targets app.notis.ai. Mode is baked into `cli-mode.generated.js` at publish
6
- * timeno runtime flag is needed (and none is exposed to end users, so a
7
- * crafted env var can't trick a published CLI into opening a localhost portal
8
- * that isn't running).
4
+ * The published npm CLI and the repo-local checkout both default to the live
5
+ * Notis API (`api.notis.ai` / `api-beta.notis.ai`). Localhost is not a CLI
6
+ * defaultthe `/notis-tests` worktree lease (`./dev.sh`) is the only
7
+ * supported loopback override for Notis developers.
9
8
  *
10
- * The `NOTIS_CLI_MODE` env var is honored for internal testing only.
9
+ * Mode is baked into `cli-mode.generated.js` at publish time. The
10
+ * `NOTIS_CLI_MODE` env var is honored for internal labeling only (e.g. the
11
+ * `apps` auto-dev loop inside `./dev.sh`).
11
12
  */
12
13
 
13
14
  import { MODE as BAKED_MODE } from './cli-mode.generated.js';
@@ -20,8 +21,12 @@ export function getCliMode() {
20
21
  return BAKED_MODE === 'published' ? 'published' : 'local';
21
22
  }
22
23
 
23
- export function getDefaultApiBase(mode = getCliMode()) {
24
- return mode === 'local' ? 'http://localhost:3001' : 'https://api.notis.ai';
24
+ export function getDefaultApiBase(mode = getCliMode(), { beta = false } = {}) {
25
+ // Mode no longer switches the default API to localhost. Local loopback is
26
+ // reserved for the worktree test lease; `mode` only affects portal origin
27
+ // labeling for the in-repo apps-dev helper.
28
+ void mode;
29
+ return beta ? 'https://api-beta.notis.ai' : 'https://api.notis.ai';
25
30
  }
26
31
 
27
32
  export function getDefaultPortalOrigin(mode = getCliMode()) {
@@ -56,8 +56,8 @@ export function getDesktopAuthRecovery(runtime, { mode = 'expired' } = {}) {
56
56
  // Leads the list: a machine that was never signed in may not even have an
57
57
  // account yet, and `notis start` covers both cases.
58
58
  hints.unshift({
59
- command: 'notis start --email <address>',
60
- reason: 'Create or access a Notis account and sign this machine in',
59
+ command: 'notis login',
60
+ reason: 'Sign in or create an account in the browser and authorize this machine',
61
61
  });
62
62
  }
63
63
  hints.push({
@@ -100,6 +100,18 @@ export async function waitForDesktopAuth({
100
100
  }
101
101
 
102
102
  export function createExpiredAuthError(runtime) {
103
+ if (runtime.credentialKind === 'oauth') {
104
+ return new CliError({
105
+ code: 'auth_expired',
106
+ message: 'Notis CLI OAuth authentication has expired',
107
+ exitCode: EXIT_CODES.auth,
108
+ details: { credential_source: 'oauth' },
109
+ hints: [
110
+ { command: 'notis login', reason: 'Authorize a new scoped CLI credential' },
111
+ { command: 'notis doctor', reason: 'Inspect the active credential state' },
112
+ ],
113
+ });
114
+ }
103
115
  if (runtime.credentialSource === 'env') {
104
116
  return new CliError({
105
117
  code: 'auth_expired',
@@ -130,6 +142,14 @@ export function createExpiredAuthError(runtime) {
130
142
  }
131
143
 
132
144
  export function createInvalidAuthHints(runtime) {
145
+ if (runtime?.credentialKind === 'oauth') {
146
+ return [
147
+ {
148
+ command: 'notis login',
149
+ reason: 'Authorize a new scoped CLI credential',
150
+ },
151
+ ];
152
+ }
133
153
  if (runtime?.credentialSource === 'env') {
134
154
  return [
135
155
  {