* CLI: explain 401s with a login hint, default login to executor.sh A 401 from a stored profile now renders as a sign-in message naming the profile and the exact login command, instead of the transport-level `Decode error (401 GET .../api/tools)`. `executor login` with no flags and no profiles now targets https://executor.sh instead of erroring, and prints the chosen server before starting the device flow. * Keep profile plumbing out of CLI auth messages The sign-in hint now mirrors how the user addressed the server: plain `executor login` when no flags were typed, echoing --server/--base-url only when they were. Login and logout output mention profiles only when the user works with named profiles. * Add changeset for CLI login hints
Showing the changes to apps/cli/src (3 of 5 files). Show all
Unified · Split
| @@ -64,7 +64,12 @@ if (sentryDsn) { · 63 unmodified lines | |||
| 64 | import { Argument as Args, Command, Flag as Options } from "effect/unstable/cli"; | 64 | import { Argument as Args, Command, Flag as Options } from "effect/unstable/cli"; |
| 65 | import { BunRuntime, BunServices } from "@effect/platform-bun"; | 65 | import { BunRuntime, BunServices } from "@effect/platform-bun"; |
| 66 | import { HttpApiClient } from "effect/unstable/httpapi"; | 66 | import { HttpApiClient } from "effect/unstable/httpapi"; |
| 67 | import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"; | 67 | import { |
| 68 | FetchHttpClient, | ||
| 69 | HttpClient, | ||
| 70 | HttpClientError, | ||
| 71 | HttpClientRequest, | ||
| 72 | } from "effect/unstable/http"; | ||
| 68 | import { FileSystem, Path as PlatformPath } from "effect"; | 73 | import { FileSystem, Path as PlatformPath } from "effect"; |
| 69 | import type { PlatformError } from "effect/PlatformError"; | 74 | import type { PlatformError } from "effect/PlatformError"; |
| 70 | import * as Effect from "effect/Effect"; | 75 | import * as Effect from "effect/Effect"; |
| @@ -131,7 +136,9 @@ import { · 60 unmodified lines | |||
| 131 | import { | 136 | import { |
| 132 | canAutoStartCliServerConnection, | 137 | canAutoStartCliServerConnection, |
| 133 | chooseCliServerConnectionWithActiveLocal, | 138 | chooseCliServerConnectionWithActiveLocal, |
| 139 | describeUnauthorizedCliServer, | ||
| 134 | parseCliExecutorServerConnection, | 140 | parseCliExecutorServerConnection, |
| 141 | profileNameFromConnectionKey, | ||
| 135 | type CliServerConnectionSource, | 142 | type CliServerConnectionSource, |
| 136 | withCliServerAuthFallback, | 143 | withCliServerAuthFallback, |
| 137 | } from "./server-connection"; | 144 | } from "./server-connection"; |
| @@ -743,9 +750,6 @@ const resolveRequestedExecutorServerConnection = ( · 605 unmodified lines | |||
| 743 | // login. The refreshed tokens are written back to the originating profile. | 750 | // login. The refreshed tokens are written back to the originating profile. |
| 744 | const OAUTH_REFRESH_SKEW_SECONDS = 60; | 751 | const OAUTH_REFRESH_SKEW_SECONDS = 60; |
| 745 | 752 | ||
| 746 | const profileNameFromKey = (key: string): string | null => | ||
| 747 | key.startsWith("profile:") ? key.slice("profile:".length) : null; | ||
| 748 | |||
| 749 | const refreshOAuthConnection = ( | 753 | const refreshOAuthConnection = ( |
| 750 | connection: ExecutorServerConnection, | 754 | connection: ExecutorServerConnection, |
| 751 | ): Effect.Effect<ExecutorServerConnection, never, FileSystem.FileSystem | PlatformPath.Path> => | 755 | ): Effect.Effect<ExecutorServerConnection, never, FileSystem.FileSystem | PlatformPath.Path> => |
| @@ -780,7 +784,7 @@ const refreshOAuthConnection = ( · 28 unmodified lines | |||
| 780 | }, | 784 | }, |
| 781 | }); | 785 | }); |
| 782 | 786 | ||
| 783 | const profileName = profileNameFromKey(connection.key); | 787 | const profileName = profileNameFromConnectionKey(connection.key); |
| 784 | if (profileName) { | 788 | if (profileName) { |
| 785 | yield* upsertCliServerConnectionProfile({ | 789 | yield* upsertCliServerConnectionProfile({ |
| 786 | name: profileName, | 790 | name: profileName, |
| @@ -961,7 +965,7 @@ const executeCode = (input: { · 174 unmodified lines | |||
| 961 | }): Effect.Effect<ExecuteCodeResult, Error, FileSystem.FileSystem | PlatformPath.Path> => | 965 | }): Effect.Effect<ExecuteCodeResult, Error, FileSystem.FileSystem | PlatformPath.Path> => |
| 962 | Effect.gen(function* () { | 966 | Effect.gen(function* () { |
| 963 | const connection = yield* resolveExecutorServerConnection(input.target); | 967 | const connection = yield* resolveExecutorServerConnection(input.target); |
| 964 | const client = yield* makeApiClient(connection); | 968 | const client = yield* makeApiClient(connection, input.target); |
| 965 | const response = yield* client.executions.execute({ | 969 | const response = yield* client.executions.execute({ |
| 966 | payload: { | 970 | payload: { |
| 967 | code: input.code, | 971 | code: input.code, |
| @@ -1050,7 +1054,7 @@ const printExecutionOutcome = (input: { · 82 unmodified lines | |||
| 1050 | // Typed API client | 1054 | // Typed API client |
| 1051 | // --------------------------------------------------------------------------- | 1055 | // --------------------------------------------------------------------------- |
| 1052 | 1056 | ||
| 1053 | const makeApiClient = (connection: ExecutorServerConnection) => { | 1057 | const makeApiClient = (connection: ExecutorServerConnection, target: ServerTarget = {}) => { |
| 1054 | const authorization = getExecutorServerAuthorizationHeader(connection); | 1058 | const authorization = getExecutorServerAuthorizationHeader(connection); |
| 1055 | return HttpApiClient.make(ExecutorApi, { | 1059 | return HttpApiClient.make(ExecutorApi, { |
| 1056 | baseUrl: connection.apiBaseUrl, | 1060 | baseUrl: connection.apiBaseUrl, |
| @@ -1061,6 +1065,18 @@ const makeApiClient = (connection: ExecutorServerConnection) => { · 4 unmodified lines | |||
| 1061 | ), | 1065 | ), |
| 1062 | } | 1066 | } |
| 1063 | : {}), | 1067 | : {}), |
| 1068 | // A 401 on an endpoint that doesn't model it is a sign-in problem: rewrite | ||
| 1069 | // the transport-level error into the login hint. Without this the client | ||
| 1070 | // fails decoding the unexpected status and prints the opaque | ||
| 1071 | // `Decode error (401 GET .../api/tools)`. Declared 401s (typed API errors) | ||
| 1072 | // decode before this catch and pass through untouched. | ||
| 1073 | transformResponse: (effect) => | ||
| 1074 | Effect.catchIf( | ||
| 1075 | effect, | ||
| 1076 | (cause) => HttpClientError.isHttpClientError(cause) && cause.response?.status === 401, | ||
| 1077 | () => | ||
| 1078 | Effect.fail(new Error(describeUnauthorizedCliServer({ connection, cliPrefix, target }))), | ||
| 1079 | ), | ||
| 1064 | }).pipe(Effect.provide(FetchHttpClient.layer)); | 1080 | }).pipe(Effect.provide(FetchHttpClient.layer)); |
| 1065 | }; | 1081 | }; |
| 1066 | 1082 | ||
| @@ -1802,11 +1818,9 @@ const runCallHelp = ( · 735 unmodified lines | |||
| 1802 | Effect.gen(function* () { | 1818 | Effect.gen(function* () { |
| 1803 | if (args.scopeDir) process.env.EXECUTOR_SCOPE_DIR = resolve(args.scopeDir); | 1819 | if (args.scopeDir) process.env.EXECUTOR_SCOPE_DIR = resolve(args.scopeDir); |
| 1804 | 1820 | ||
| 1805 | const connection = yield* resolveExecutorServerConnection({ | 1821 | const target: ServerTarget = { baseUrl: args.baseUrl, serverName: args.serverName }; |
| 1806 | baseUrl: args.baseUrl, | 1822 | const connection = yield* resolveExecutorServerConnection(target); |
| 1807 | serverName: args.serverName, | 1823 | const client = yield* makeApiClient(connection, target); |
| 1808 | }); | ||
| 1809 | const client = yield* makeApiClient(connection); | ||
| 1810 | const tools = yield* client.tools.list({ query: {} }); | 1824 | const tools = yield* client.tools.list({ query: {} }); |
| 1811 | const toolPaths = tools.map((tool) => tool.address); | 1825 | const toolPaths = tools.map((tool) => tool.address); |
| 1812 | 1826 | ||
| @@ -1987,7 +2001,7 @@ const resumeCommand = Command.make( · 174 unmodified lines | |||
| 1987 | 2001 | ||
| 1988 | const contentObj = yield* parseOptionalJsonObject(Option.getOrUndefined(content)); | 2002 | const contentObj = yield* parseOptionalJsonObject(Option.getOrUndefined(content)); |
| 1989 | 2003 | ||
| 1990 | const client = yield* makeApiClient(connection); | 2004 | const client = yield* makeApiClient(connection, target); |
| 1991 | const result = yield* client.executions.resume({ | 2005 | const result = yield* client.executions.resume({ |
| 1992 | params: { executionId }, | 2006 | params: { executionId }, |
| 1993 | payload: { action, content: contentObj }, | 2007 | payload: { action, content: contentObj }, |
| @@ -2311,9 +2325,13 @@ const chooseLoginProfileName = ( · 317 unmodified lines | |||
| 2311 | } | 2325 | } |
| 2312 | }; | 2326 | }; |
| 2313 | 2327 | ||
| 2328 | /** Where `executor login` lands with no flags and no stored profiles. */ | ||
| 2329 | const DEFAULT_LOGIN_ORIGIN = "https://executor.sh"; | ||
| 2330 | |||
| 2314 | // Resolve which server a login/logout targets: an existing profile (--server | 2331 | // Resolve which server a login/logout targets: an existing profile (--server |
| 2315 | // or the default) or a bare origin (--base-url). The profile name is decided | 2332 | // or the default), a bare origin (--base-url), or hosted Executor when |
| 2316 | // later, from the authenticated account. | 2333 | // nothing is configured yet. The profile name is decided later, from the |
| 2334 | // authenticated account. | ||
| 2317 | const resolveLoginOrigin = (input: { | 2335 | const resolveLoginOrigin = (input: { |
| 2318 | readonly baseUrl: Option.Option<string>; | 2336 | readonly baseUrl: Option.Option<string>; |
| 2319 | readonly server: Option.Option<string>; | 2337 | readonly server: Option.Option<string>; |
| @@ -2341,11 +2359,7 @@ const resolveLoginOrigin = (input: { · 21 unmodified lines | |||
| 2341 | const store = yield* readCliServerConnectionStore(); | 2359 | const store = yield* readCliServerConnectionStore(); |
| 2342 | const profile = defaultCliServerConnectionProfile(store); | 2360 | const profile = defaultCliServerConnectionProfile(store); |
| 2343 | if (profile) return { origin: profile.connection.origin, profile }; | 2361 | if (profile) return { origin: profile.connection.origin, profile }; |
| 2344 | return yield* Effect.fail( | 2362 | return { origin: DEFAULT_LOGIN_ORIGIN, profile: null }; |
| 2345 | new Error( | ||
| 2346 | "No server to log in to. Pass --base-url <https://your-host> or --server <profile>.", | ||
| 2347 | ), | ||
| 2348 | ); | ||
| 2349 | }); | 2363 | }); |
| 2350 | 2364 | ||
| 2351 | const loginNameOption = Options.string("name").pipe( | 2365 | const loginNameOption = Options.string("name").pipe( |
| @@ -2370,6 +2384,9 @@ const loginCommand = Command.make( · 18 unmodified lines | |||
| 2370 | Effect.gen(function* () { | 2384 | Effect.gen(function* () { |
| 2371 | const target = yield* resolveLoginOrigin({ baseUrl, server }); | 2385 | const target = yield* resolveLoginOrigin({ baseUrl, server }); |
| 2372 | const explicitName = Option.getOrUndefined(name); | 2386 | const explicitName = Option.getOrUndefined(name); |
| 2387 | // The target may have been picked implicitly (default profile, or the | ||
| 2388 | // hosted fallback): say where the login is going before the device flow. | ||
| 2389 | console.log(`Signing in to ${target.origin}`); | ||
| 2373 | const discovery = yield* Effect.tryPromise({ | 2390 | const discovery = yield* Effect.tryPromise({ |
| 2374 | try: () => discoverCliLogin(target.origin), | 2391 | try: () => discoverCliLogin(target.origin), |
| 2375 | catch: toError, | 2392 | catch: toError, |
| @@ -2424,12 +2441,24 @@ const loginCommand = Command.make( · 48 unmodified lines | |||
| 2424 | }); | 2441 | }); |
| 2425 | 2442 | ||
| 2426 | console.log(""); | 2443 | console.log(""); |
| 2427 | console.log(`Logged in to ${target.origin} (profile "${profileName}", now the default).`); | 2444 | // Profile bookkeeping stays backstage unless the user works with named |
| 2445 | // profiles (--name, or a store that already has several). | ||
| 2446 | const mentionProfile = | ||
| 2447 | explicitName !== undefined || (yield* readCliServerConnectionStore()).profiles.length > 1; | ||
| 2448 | console.log( | ||
| 2449 | mentionProfile | ||
| 2450 | ? `Logged in to ${target.origin} (profile "${profileName}", now the default).` | ||
| 2451 | : `Logged in to ${target.origin}.`, | ||
| 2452 | ); | ||
| 2428 | if (email) console.log(`Account: ${email}`); | 2453 | if (email) console.log(`Account: ${email}`); |
| 2429 | if (org) console.log(`Organization: ${org}`); | 2454 | if (org) console.log(`Organization: ${org}`); |
| 2430 | else if (sub) console.log(`User: ${sub}`); | 2455 | else if (sub) console.log(`User: ${sub}`); |
| 2431 | }), | 2456 | }), |
| 2432 | ).pipe(Command.withDescription("Sign in to a hosted Executor server in the browser (device flow)")); | 2457 | ).pipe( |
| 2458 | Command.withDescription( | ||
| 2459 | "Sign in to a hosted Executor server in the browser (device flow). Defaults to https://executor.sh.", | ||
| 2460 | ), | ||
| 2461 | ); | ||
| 2433 | 2462 | ||
| 2434 | const logoutCommand = Command.make( | 2463 | const logoutCommand = Command.make( |
| 2435 | "logout", | 2464 | "logout", |
| @@ -2445,8 +2474,14 @@ const logoutCommand = Command.make( · 9 unmodified lines | |||
| 2445 | console.log(`No stored login for ${target.origin}.`); | 2474 | console.log(`No stored login for ${target.origin}.`); |
| 2446 | return; | 2475 | return; |
| 2447 | } | 2476 | } |
| 2477 | // Profile bookkeeping stays backstage unless the user addressed one. | ||
| 2478 | const mentionProfile = Option.isSome(server) || store.profiles.length > 1; | ||
| 2448 | if (!profile.connection.auth) { | 2479 | if (!profile.connection.auth) { |
| 2449 | console.log(`Profile "${profile.name}" has no stored credentials.`); | 2480 | console.log( |
| 2481 | mentionProfile | ||
| 2482 | ? `Profile "${profile.name}" has no stored credentials.` | ||
| 2483 | : `Not signed in to ${target.origin}.`, | ||
| 2484 | ); | ||
| 2450 | return; | 2485 | return; |
| 2451 | } | 2486 | } |
| 2452 | yield* upsertCliServerConnectionProfile({ | 2487 | yield* upsertCliServerConnectionProfile({ |
| @@ -2459,7 +2494,9 @@ const logoutCommand = Command.make( · 6 unmodified lines | |||
| 2459 | makeDefault: store.defaultProfile === profile.name, | 2494 | makeDefault: store.defaultProfile === profile.name, |
| 2460 | }); | 2495 | }); |
| 2461 | console.log( | 2496 | console.log( |
| 2462 | `Logged out of ${profile.connection.origin} (cleared credentials for "${profile.name}").`, | 2497 | mentionProfile |
| 2498 | ? `Logged out of ${profile.connection.origin} (cleared credentials for "${profile.name}").` | ||
| 2499 | : `Logged out of ${profile.connection.origin}.`, | ||
| 2463 | ); | 2500 | ); |
| 2464 | }), | 2501 | }), |
| 2465 | ).pipe(Command.withDescription("Clear stored credentials for a server profile")); | 2502 | ).pipe(Command.withDescription("Clear stored credentials for a server profile")); |
| @@ -1,8 +1,11 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | ||
| 3 | import { normalizeExecutorServerConnection } from "@executor-js/sdk/shared"; | ||
| 4 | |||
| 3 | import { | 5 | import { |
| 4 | canAutoStartCliServerConnection, | 6 | canAutoStartCliServerConnection, |
| 5 | chooseCliServerConnectionWithActiveLocal, | 7 | chooseCliServerConnectionWithActiveLocal, |
| 8 | describeUnauthorizedCliServer, | ||
| 6 | parseCliExecutorServerConnection, | 9 | parseCliExecutorServerConnection, |
| 7 | withCliServerAuthFallback, | 10 | withCliServerAuthFallback, |
| 8 | } from "./server-connection"; | 11 | } from "./server-connection"; |
| @@ -135,3 +138,84 @@ describe("CLI server connection", () => { · 126 unmodified lines | |||
| 135 | ).toBe("conflict"); | 138 | ).toBe("conflict"); |
| 136 | }); | 139 | }); |
| 137 | }); | 140 | }); |
| 141 | |||
| 142 | describe("describeUnauthorizedCliServer", () => { | ||
| 143 | it("hints plain `executor login` when the server was picked implicitly", () => { | ||
| 144 | const connection = normalizeExecutorServerConnection({ | ||
| 145 | key: "profile:rhys-executor.sh", | ||
| 146 | origin: "https://executor.sh", | ||
| 147 | auth: { kind: "oauth", accessToken: "stale", expiresAt: 1 }, | ||
| 148 | }); | ||
| 149 | |||
| 150 | const message = describeUnauthorizedCliServer({ | ||
| 151 | connection, | ||
| 152 | cliPrefix: "executor", | ||
| 153 | target: {}, | ||
| 154 | }); | ||
| 155 | expect(message).toContain("You're signed out of https://executor.sh"); | ||
| 156 | expect(message).toContain("Run `executor login` to sign in again."); | ||
| 157 | // Profile names are plumbing: never surfaced when the user didn't type one. | ||
| 158 | expect(message).not.toContain("--server"); | ||
| 159 | expect(message).not.toContain("profile"); | ||
| 160 | }); | ||
| 161 | |||
| 162 | it("echoes --server back when the user targeted a named profile", () => { | ||
| 163 | const connection = normalizeExecutorServerConnection({ | ||
| 164 | key: "profile:work", | ||
| 165 | origin: "https://executor.example", | ||
| 166 | auth: { kind: "oauth", accessToken: "stale", expiresAt: 1 }, | ||
| 167 | }); | ||
| 168 | |||
| 169 | const message = describeUnauthorizedCliServer({ | ||
| 170 | connection, | ||
| 171 | cliPrefix: "executor", | ||
| 172 | target: { serverName: "work" }, | ||
| 173 | }); | ||
| 174 | expect(message).toContain("You're signed out of https://executor.example"); | ||
| 175 | expect(message).toContain("executor login --server work"); | ||
| 176 | }); | ||
| 177 | |||
| 178 | it("echoes --base-url back when the user targeted an origin", () => { | ||
| 179 | const connection = normalizeExecutorServerConnection({ | ||
| 180 | origin: "https://executor.example", | ||
| 181 | }); | ||
| 182 | |||
| 183 | const message = describeUnauthorizedCliServer({ | ||
| 184 | connection, | ||
| 185 | cliPrefix: "executor", | ||
| 186 | target: { baseUrl: "https://executor.example" }, | ||
| 187 | }); | ||
| 188 | expect(message).toContain("no credentials are stored"); | ||
| 189 | expect(message).toContain("executor login --base-url https://executor.example"); | ||
| 190 | }); | ||
| 191 | |||
| 192 | it("mentions the env vars when a profile-less bearer key is rejected", () => { | ||
| 193 | const connection = normalizeExecutorServerConnection({ | ||
| 194 | origin: "https://executor.example", | ||
| 195 | auth: { kind: "bearer", token: "key_bad" }, | ||
| 196 | }); | ||
| 197 | |||
| 198 | const message = describeUnauthorizedCliServer({ | ||
| 199 | connection, | ||
| 200 | cliPrefix: "executor", | ||
| 201 | target: {}, | ||
| 202 | }); | ||
| 203 | expect(message).toContain("rejected the stored bearer credentials"); | ||
| 204 | expect(message).toContain("EXECUTOR_API_KEY"); | ||
| 205 | }); | ||
| 206 | |||
| 207 | it("uses the dev entrypoint prefix verbatim", () => { | ||
| 208 | const connection = normalizeExecutorServerConnection({ | ||
| 209 | key: "profile:hosted", | ||
| 210 | origin: "https://executor.example", | ||
| 211 | auth: { kind: "oauth", accessToken: "stale" }, | ||
| 212 | }); | ||
| 213 | |||
| 214 | const message = describeUnauthorizedCliServer({ | ||
| 215 | connection, | ||
| 216 | cliPrefix: "bun run apps/cli/src/main.ts", | ||
| 217 | target: {}, | ||
| 218 | }); | ||
| 219 | expect(message).toContain("bun run apps/cli/src/main.ts login"); | ||
| 220 | }); | ||
| 221 | }); | ||
| @@ -52,6 +52,55 @@ export const canAutoStartCliServerConnection = (connection: ExecutorServerConnec · 51 unmodified lines | |||
| 52 | return url.protocol === "http:" && canAutoStartLocalDaemonForHost(url.hostname); | 52 | return url.protocol === "http:" && canAutoStartLocalDaemonForHost(url.hostname); |
| 53 | }; | 53 | }; |
| 54 | 54 | ||
| 55 | export const profileNameFromConnectionKey = (key: string): string | null => | ||
| 56 | key.startsWith("profile:") ? key.slice("profile:".length) : null; | ||
| 57 | |||
| 58 | // A 401 from the server means the stored credential is stale, revoked, or | ||
| 59 | // missing: a sign-in problem, not a transport problem. Rendered instead of | ||
| 60 | // the raw HTTP client error so the user learns the recovery command, not the | ||
| 61 | // failing endpoint. | ||
| 62 | // | ||
| 63 | // The hint mirrors how the user addressed the server. The common case types | ||
| 64 | // no flags (the default profile, normally executor.sh), and login re-targets | ||
| 65 | // that same default, so the fix is plain `executor login`: profile names are | ||
| 66 | // plumbing and stay invisible. Only an explicit --server / --base-url comes | ||
| 67 | // back in the hint. | ||
| 68 | export const describeUnauthorizedCliServer = (input: { | ||
| 69 | readonly connection: ExecutorServerConnection; | ||
| 70 | readonly cliPrefix: string; | ||
| 71 | readonly target: { readonly baseUrl?: string; readonly serverName?: string }; | ||
| 72 | }): string => { | ||
| 73 | const { connection, cliPrefix, target } = input; | ||
| 74 | const loginCommand = target.serverName | ||
| 75 | ? `${cliPrefix} login --server ${target.serverName}` | ||
| 76 | : target.baseUrl | ||
| 77 | ? `${cliPrefix} login --base-url ${connection.origin}` | ||
| 78 | : `${cliPrefix} login`; | ||
| 79 | |||
| 80 | if (!connection.auth) { | ||
| 81 | return [ | ||
| 82 | `${connection.origin} requires authentication, and no credentials are stored for it.`, | ||
| 83 | `Run \`${loginCommand}\` to sign in.`, | ||
| 84 | ].join("\n"); | ||
| 85 | } | ||
| 86 | |||
| 87 | if (connection.auth.kind === "oauth") { | ||
| 88 | return [ | ||
| 89 | `You're signed out of ${connection.origin}.`, | ||
| 90 | `Run \`${loginCommand}\` to sign in again.`, | ||
| 91 | ].join("\n"); | ||
| 92 | } | ||
| 93 | |||
| 94 | const envHint = | ||
| 95 | connection.auth.kind === "bearer" && !profileNameFromConnectionKey(connection.key) | ||
| 96 | ? " If the key came from EXECUTOR_API_KEY or EXECUTOR_AUTH_TOKEN, check that value." | ||
| 97 | : ""; | ||
| 98 | return [ | ||
| 99 | `${connection.origin} rejected the stored ${connection.auth.kind} credentials (401).${envHint}`, | ||
| 100 | `Run \`${loginCommand}\` to sign in again.`, | ||
| 101 | ].join("\n"); | ||
| 102 | }; | ||
| 103 | |||
| 55 | export type CliServerConnectionSource = | 104 | export type CliServerConnectionSource = |
| 56 | | "explicit" | 105 | | "explicit" |
| 57 | | "default-profile" | 106 | | "default-profile" |