switch-profile 2.2.0 → 2.3.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.
Files changed (3) hide show
  1. package/README.md +12 -2
  2. package/dist/cli.js +170 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -64,6 +64,8 @@ npx switch-profile
64
64
  - [Requirements](#requirements)
65
65
  - [Troubleshooting](#troubleshooting)
66
66
  - [`invalid_grant` during an SSO login](#invalid_grant-during-an-sso-login)
67
+ - [`Invalid start url provided` when adding an SSO profile](#invalid-start-url-provided-when-adding-an-sso-profile)
68
+ - [Something failed and the message is not enough](#something-failed-and-the-message-is-not-enough)
67
69
  - [`Error loading SSO Token` or "session has expired"](#error-loading-sso-token-or-session-has-expired)
68
70
  - [SSO login hangs on a remote machine](#sso-login-hangs-on-a-remote-machine)
69
71
  - [The code is not in my clipboard over SSH](#the-code-is-not-in-my-clipboard-over-ssh)
@@ -116,7 +118,7 @@ Running `sp` (or `npx switch-profile`) with no arguments opens the home screen:
116
118
  | `remove [profiles...] [--yes]` | Removes profiles (alias `rm`). The current default cannot be removed. |
117
119
  | `settings` | Per-terminal switching, SSO login mode, legacy SSO upgrade. |
118
120
 
119
- Global options: `--json`, `--no-input` (never prompt), `--debug` (show stack traces), `-v, --version`. In examples, `sp` and `switch-profile` are interchangeable; only `sp` changes the current terminal.
121
+ Global options: `--json`, `--no-input` (never prompt), `--debug` (show stack traces), `-v, --version`. Every run, AWS CLI call and error is logged to `~/.switch-profile/switch-profile.log` (secrets redacted). In examples, `sp` and `switch-profile` are interchangeable; only `sp` changes the current terminal.
120
122
 
121
123
  See [CLI Interface](docs/cli-interface.md) for every screen.
122
124
 
@@ -235,6 +237,14 @@ The published package has no runtime dependencies: everything is bundled into on
235
237
 
236
238
  The SSO region is wrong. It must be the region of your IAM Identity Center instance, not the region you deploy to. Fix `sso_region` in the `[sso-session]` section of `~/.aws/config` (or remove the portal and add it again).
237
239
 
240
+ ### `Invalid start url provided` when adding an SSO profile
241
+
242
+ The start URL is usually fine: AWS sends this error when the **SSO region** is wrong. Use the region where IAM Identity Center lives (Identity Center console › Settings), not where you deploy. If the portal is already set up, type its existing session name instead (`sp add` lists them with their regions).
243
+
244
+ ### Something failed and the message is not enough
245
+
246
+ Open `~/.switch-profile/switch-profile.log`. It has every run, every AWS CLI call with its exit code and output, and every error with its full chain. Secrets are redacted, so you can share it.
247
+
238
248
  ### `Error loading SSO Token` or "session has expired"
239
249
 
240
250
  The SSO login is missing or expired. Run `sp login` (or just switch to the profile again: `switch-profile` logs in when needed). There is no need to recreate the profile.
@@ -271,7 +281,7 @@ The completion registers only if `compinit` ran before the `switch-profile` bloc
271
281
  - [Architecture](docs/architecture.md) — Module map, data flow and bundling of the switch-profile CLI.
272
282
  - [AWS Profile Management](docs/aws-profile-management.md) — Profile kinds, switching, SSO logins, SSO import, migrations and the sp shell function.
273
283
  - [CLI Interface](docs/cli-interface.md) — Walkthrough of every command and screen of the CLI.
274
- - [Configuration Files](docs/configuration-files.md) — Exact formats of the AWS files, the settings file and the shell block written by switch-profile.
284
+ - [Configuration Files](docs/configuration-files.md) — Exact formats of the AWS files, the settings and log files and the shell block written by switch-profile.
275
285
  - [Development Guide](docs/development-guide.md) — Build, lint, tests, manual testing and releases.
276
286
  - [Gotchas](docs/gotchas.md) — Non-obvious pitfalls for users and maintainers.
277
287
  <!-- END doc-index -->
package/dist/cli.js CHANGED
@@ -5235,10 +5235,77 @@ ${i2}
5235
5235
  }
5236
5236
  });
5237
5237
 
5238
+ // src/log.js
5239
+ var require_log = __commonJS({
5240
+ "src/log.js"(exports2, module2) {
5241
+ var fs = require("fs"), { homedir } = require("os"), { join: join2, dirname: dirname2 } = require("path"), logFile = () => process.env.SWITCH_PROFILE_LOG_FILE || join2(homedir(), ".switch-profile", "switch-profile.log"), MAX_BYTES = 1024 * 1024, MAX_OUTPUT = 4e3, SECRET_FLAGS = ["--access-token", "--client-secret", "--otp"], SECRET_PATTERNS = [
5242
+ /((?:aws_secret_access_key|aws_session_token|aws_security_token)\s*[=:]\s*)\S+/gi,
5243
+ /("?(?:accessToken|refreshToken|clientSecret|secretAccessKey|sessionToken)"?\s*[=:]\s*"?)[^\s",}]+/gi,
5244
+ /(--(?:access-token|client-secret|otp)[=\s]+)\S+/gi
5245
+ ], redact = (text2) => SECRET_PATTERNS.reduce((s, re) => s.replace(re, "$1***"), String(text2 ?? "")), redactArgs = (args) => (args || []).map((a2, i2) => SECRET_FLAGS.includes(args[i2 - 1]) ? "***" : redact(a2)), truncate = (text2, max = MAX_OUTPUT) => {
5246
+ let s = String(text2 ?? "").trim();
5247
+ return s.length > max ? `\u2026${s.slice(-max)}` : s;
5248
+ }, indent = (text2) => String(text2).split(`
5249
+ `).map((l2) => ` ${l2}`).join(`
5250
+ `), describeError = (err, depth = 0) => {
5251
+ if (depth > 8)
5252
+ return " (cause chain truncated)";
5253
+ if (!(err instanceof Error))
5254
+ return ` ${redact(typeof err == "string" ? err : JSON.stringify(err))}`;
5255
+ let lines = [` ${err.name || "Error"}: ${redact(err.message)}`];
5256
+ err.hint && lines.push(` hint: ${redact(err.hint)}`), err.exitCode != null && lines.push(` exit code: ${err.exitCode}`), err.command && lines.push(` command: ${redact(err.command)}`), err.code != null && err.exitCode == null && lines.push(` code: ${err.code}`), err.output && lines.push(" output:", indent(redact(truncate(err.output)))), err.stack && lines.push(indent(redact(err.stack.split(`
5257
+ `).slice(1).join(`
5258
+ `).replace(/^\s+/gm, ""))));
5259
+ for (let e of err.errors || [])
5260
+ lines.push(" wrapped:", describeError(e, depth + 1));
5261
+ return err.cause && lines.push(" caused by:", describeError(err.cause, depth + 1)), lines.join(`
5262
+ `);
5263
+ }, rotated = !1, write = (level, message, details) => {
5264
+ try {
5265
+ let file = logFile();
5266
+ if (fs.existsSync(dirname2(file)) || fs.mkdirSync(dirname2(file), { recursive: !0, mode: 448 }), !rotated) {
5267
+ rotated = !0;
5268
+ let stat = fs.statSync(file, { throwIfNoEntry: !1 });
5269
+ stat && stat.size > MAX_BYTES && fs.renameSync(file, `${file}.1`);
5270
+ }
5271
+ let line = `${(/* @__PURE__ */ new Date()).toISOString()} [${process.pid}] ${level.padEnd(5)} ${redact(message)}${details ? `
5272
+ ${details}` : ""}
5273
+ `;
5274
+ fs.appendFileSync(file, line, { mode: 384 });
5275
+ } catch {
5276
+ }
5277
+ }, info = (message, details) => write("INFO", message, details && redact(details)), warn = (message, details) => write("WARN", message, details && redact(details)), error = (context, err) => write("ERROR", context, describeError(err)), tolerated = (context, fallback) => (err) => (write("WARN", `${context} failed (ignored)`, describeError(err)), fallback), command = ({ cmd, args, code, ms, output, inherit }) => {
5278
+ let line = `${cmd} ${redactArgs(args).join(" ")}`.trim(), status = code === 0 ? "ok" : `exit ${code}`, details = code !== 0 && output ? indent(redact(truncate(output))) : null;
5279
+ write(code === 0 ? "INFO" : "WARN", `exec ${line} \u2192 ${status} in ${ms}ms${inherit ? " (interactive)" : ""}`, details);
5280
+ }, start = ({ version, argv }) => write("INFO", `run switch-profile ${version} ${redactArgs((argv || []).slice(2)).join(" ")}`.trim(), indent([
5281
+ `node ${process.version} \xB7 ${process.platform} ${process.arch} \xB7 ${process.stdout.isTTY ? "tty" : "no tty"}${process.env.SSH_CONNECTION ? " \xB7 ssh" : ""}`,
5282
+ `shell ${process.env.SWITCH_PROFILE_SHELL || process.env.SHELL || "?"}${process.env.SWITCH_PROFILE_ENV_FILE ? " (via sp)" : ""}`
5283
+ ].join(`
5284
+ `))), displayPath = () => {
5285
+ let file = logFile(), home = homedir();
5286
+ return file.startsWith(home + "/") || file.startsWith(home + "\\") ? `~${file.slice(home.length)}` : file;
5287
+ };
5288
+ module2.exports = {
5289
+ logFile,
5290
+ MAX_BYTES,
5291
+ redact,
5292
+ redactArgs,
5293
+ describeError,
5294
+ displayPath,
5295
+ start,
5296
+ info,
5297
+ warn,
5298
+ error,
5299
+ tolerated,
5300
+ command
5301
+ };
5302
+ }
5303
+ });
5304
+
5238
5305
  // src/ui.js
5239
5306
  var require_ui = __commonJS({
5240
5307
  "src/ui.js"(exports2, module2) {
5241
- var { styleText: styleText3 } = require("node:util"), env = process.env, colorEnabled = () => env.FORCE_COLOR && env.FORCE_COLOR !== "0" ? !0 : env.NO_COLOR || env.TERM == "dumb" ? !1 : !!process.stdout.isTTY, style = (format, text2) => colorEnabled() ? styleText3(format, String(text2)) : String(text2), unicode2 = process.platform !== "win32" || !!env.WT_SESSION || env.TERM_PROGRAM == "vscode" || !!env.TERMINAL_EMULATOR, sym = {
5308
+ var { styleText: styleText3 } = require("node:util"), log3 = require_log(), env = process.env, colorEnabled = () => env.FORCE_COLOR && env.FORCE_COLOR !== "0" ? !0 : env.NO_COLOR || env.TERM == "dumb" ? !1 : !!process.stdout.isTTY, style = (format, text2) => colorEnabled() ? styleText3(format, String(text2)) : String(text2), unicode2 = process.platform !== "win32" || !!env.WT_SESSION || env.TERM_PROGRAM == "vscode" || !!env.TERMINAL_EMULATOR, sym = {
5242
5309
  ok: unicode2 ? "\u2713" : "v",
5243
5310
  err: unicode2 ? "\u2717" : "x",
5244
5311
  warn: unicode2 ? "\u25B2" : "!",
@@ -5265,13 +5332,13 @@ var require_ui = __commonJS({
5265
5332
  fit: (text2, width) => (text2 = String(text2 || ""), text2.length > width ? width > 1 ? text2.slice(0, width - 1) + (unicode2 ? "\u2026" : "~") : text2.slice(0, width) : text2 + " ".repeat(width - text2.length)),
5266
5333
  columns: () => process.stdout.columns || 100
5267
5334
  }, isInteractive = (options) => options && options.input === !1 || env.CI && env.CI !== "false" ? !1 : !!(process.stdin.isTTY && process.stdout.isTTY), CliError = class extends Error {
5268
- constructor(message, { hint, code, cause } = {}) {
5269
- super(message), this.hint = hint, this.exitCode = code || 1, this.cause = cause;
5335
+ constructor(message, { hint, code, cause, errors } = {}) {
5336
+ super(message), this.name = "CliError", this.hint = hint, this.exitCode = code || 1, this.cause = cause, this.errors = errors;
5270
5337
  }
5271
- }, debugEnabled = () => !!(env.SWITCH_PROFILE_DEBUG || env.DEBUG && /switch-profile|\*/.test(env.DEBUG)), errorsMessage = (errors) => (errors || []).map((e) => e && e.message || String(e)).filter(Boolean).join(`
5338
+ }, cliErrorFrom = (errors, options) => new CliError(errorsMessage(errors), { ...options, errors }), debugEnabled = () => !!(env.SWITCH_PROFILE_DEBUG || env.DEBUG && /switch-profile|\*/.test(env.DEBUG)), errorsMessage = (errors) => (errors || []).map((e) => e && e.message || String(e)).filter(Boolean).join(`
5272
5339
  `), printError = (err) => {
5273
5340
  let lines = [`${ui2.bad(sym.err)} ${err.message || String(err)}`];
5274
- err.hint && lines.push(` ${ui2.dim(err.hint)}`), debugEnabled() && err.stack ? lines.push(ui2.dim(err.stack)) : err instanceof CliError || lines.push(` ${ui2.dim("Run with --debug for details.")}`), console.error(lines.join(`
5341
+ err.hint && lines.push(` ${ui2.dim(err.hint)}`), debugEnabled() && err.stack && lines.push(ui2.dim(err.stack)), (!(err instanceof CliError) || err.exitCode == 1) && lines.push(` ${ui2.dim(`Details: ${log3.displayPath()}`)}`), console.error(lines.join(`
5275
5342
  `));
5276
5343
  }, ago = (date2) => {
5277
5344
  let ms = Date.now() - new Date(date2).getTime();
@@ -5341,6 +5408,7 @@ var require_ui = __commonJS({
5341
5408
  debugEnabled,
5342
5409
  errorsMessage,
5343
5410
  printError,
5411
+ cliErrorFrom,
5344
5412
  ago
5345
5413
  };
5346
5414
  }
@@ -5351,7 +5419,7 @@ var require_package = __commonJS({
5351
5419
  "package.json"(exports2, module2) {
5352
5420
  module2.exports = {
5353
5421
  name: "switch-profile",
5354
- version: "2.2.0",
5422
+ version: "2.3.0",
5355
5423
  main: "index.js",
5356
5424
  bin: {
5357
5425
  "switch-profile": "index.js"
@@ -5451,23 +5519,40 @@ var require_settings = __commonJS({
5451
5519
  // src/core.js
5452
5520
  var require_core = __commonJS({
5453
5521
  "src/core.js"(exports2, module2) {
5454
- var { exec: exec2, spawn } = require("child_process"), WrappedError = class extends Error {
5522
+ var { exec: exec2, spawn } = require("child_process"), log3 = require_log(), WrappedError = class extends Error {
5455
5523
  constructor(message, errors) {
5456
5524
  super(message), this.errors = errors || [];
5457
5525
  }
5458
5526
  }, wrapErrors = (message, errors) => new WrappedError(message, errors), flattenErrors = (err) => err instanceof WrappedError ? [new Error(err.message), ...err.errors.flatMap(flattenErrors)] : [err instanceof Error ? err : new Error(String(err))], catchErrors = (promise) => Promise.resolve(promise).then((value) => [null, value], (err) => [flattenErrors(err)]), IS_WINDOWS = process.platform == "win32", _exec = (cmd) => new Promise((next, fail) => {
5527
+ let started = Date.now();
5459
5528
  exec2(cmd, (error, stdout2, stderr) => {
5460
- error || stderr ? fail(error || stderr) : next(stdout2);
5529
+ log3.command({ cmd, args: [], code: error ? error.code ?? 1 : stderr ? "stderr" : 0, ms: Date.now() - started, output: stderr || error && error.message }), error || stderr ? fail(error || stderr) : next(stdout2);
5461
5530
  });
5462
- }), run2 = (cmd, args, options) => new Promise((next, fail) => {
5463
- let { inherit } = options || {}, child = spawn(cmd, args || [], {
5464
- stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"],
5531
+ }), commandError = (message, { command, exitCode, output }) => Object.assign(new Error(message), { command, code: exitCode, output }), run2 = (cmd, args, options) => new Promise((next, fail) => {
5532
+ let { inherit, tee } = options || {}, started = Date.now(), command = `${cmd} ${log3.redactArgs(args).join(" ")}`.trim(), child = spawn(cmd, args || [], {
5533
+ stdio: tee ? ["inherit", "inherit", "pipe"] : inherit ? "inherit" : ["ignore", "pipe", "pipe"],
5465
5534
  ...IS_WINDOWS ? { shell: !0 } : {}
5466
5535
  }), stdout2 = "", stderr = "";
5467
- inherit || (child.stdout.on("data", (d) => stdout2 += d), child.stderr.on("data", (d) => stderr += d)), child.on("error", fail), child.on("close", (code) => {
5468
- code === 0 ? next(stdout2) : fail(new Error((stderr || stdout2 || "").trim() || `'${cmd} ${(args || []).join(" ")}' exited with code ${code}`));
5536
+ tee ? child.stderr.on("data", (d) => {
5537
+ stderr += d, process.stderr.write(d);
5538
+ }) : inherit || (child.stdout.on("data", (d) => stdout2 += d), child.stderr.on("data", (d) => stderr += d)), child.on("error", (err) => {
5539
+ log3.command({ cmd, args, code: err.code || "spawn error", ms: Date.now() - started, output: err.message, inherit: inherit || tee }), fail(Object.assign(err, { command }));
5540
+ }), child.on("close", (code) => {
5541
+ let output = (stderr || stdout2 || "").trim();
5542
+ log3.command({ cmd, args, code, ms: Date.now() - started, output, inherit: inherit || tee }), code === 0 ? next(stdout2) : fail(commandError(lastErrorLine(output) || `'${command}' exited with code ${code}`, { command, exitCode: code, output }));
5469
5543
  });
5470
- }), _commandExistsResult = {}, isCommandExist = (cmd, errorMsg) => () => catchErrors((async () => {
5544
+ }), lastErrorLine = (output) => {
5545
+ let text2 = (output || "").trim();
5546
+ if (!text2)
5547
+ return "";
5548
+ let lines = text2.split(`
5549
+ `).map((l2) => l2.trim()).filter(Boolean);
5550
+ if (lines.length <= 3)
5551
+ return text2;
5552
+ let telling = lines.filter((l2) => /error|exception|denied|invalid|expired|unable|not found|failed/i.test(l2));
5553
+ return telling.length ? telling.slice(-2).join(`
5554
+ `) : text2;
5555
+ }, _commandExistsResult = {}, isCommandExist = (cmd, errorMsg) => () => catchErrors((async () => {
5471
5556
  if (!cmd)
5472
5557
  return !1;
5473
5558
  if (_commandExistsResult[cmd] !== void 0)
@@ -5481,6 +5566,7 @@ var require_core = __commonJS({
5481
5566
  wrapErrors,
5482
5567
  exec: _exec,
5483
5568
  run: run2,
5569
+ lastErrorLine,
5484
5570
  isCommandExist
5485
5571
  };
5486
5572
  }
@@ -5713,8 +5799,13 @@ var require_login = __commonJS({
5713
5799
  text2 = text2 || "";
5714
5800
  let urls = text2.match(URL_REGEX) || [], code = (text2.match(CODE_REGEX) || [])[1] || null, completeUrl = urls.find((u5) => /user_code=/.test(u5)) || null;
5715
5801
  return { url: urls.find((u5) => u5 !== completeUrl) || completeUrl || null, code, completeUrl };
5802
+ }, WRONG_REGION = /Invalid start url|invalid_request|InvalidRequestException/i, configureSsoFailure = (output, sessions) => {
5803
+ let lines = (output || "").split(`
5804
+ `).map((l2) => l2.trim()).filter(Boolean), detail = (lines.filter((l2) => /error/i.test(l2)).slice(-1)[0] || lines.slice(-1)[0] || "").replace(/^aws:\s*\[ERROR\]:\s*/i, ""), description = ((output || "").match(/error_description:\s*(.+)/) || [])[1], known = (sessions || []).filter((s) => s.sso_region).map((s) => `${s.name} (${s.sso_region}, ${s.sso_start_url})`), reuse = known.length ? ` Portals already set up here: ${known.join(", ")}. Type one of these names as the session name to reuse it.` : "", message = `aws configure sso failed${description || detail ? `: ${(description || detail).trim()}` : "."}`;
5805
+ return WRONG_REGION.test(output || "") ? { message, hint: `The SSO region is most likely wrong: it must be the region where IAM Identity Center lives (Identity Center console \u203A Settings), not where you deploy. AWS reports a wrong region as an invalid start URL.${reuse}` } : { message, hint: `Run sp add to try again.${reuse}` };
5716
5806
  };
5717
5807
  module2.exports = {
5808
+ configureSsoFailure,
5718
5809
  isRemoteSession,
5719
5810
  resolveLoginMode,
5720
5811
  loginFlags,
@@ -5835,12 +5926,12 @@ var require_regions = __commonJS({
5835
5926
  // src/aws/index.js
5836
5927
  var require_aws = __commonJS({
5837
5928
  "src/aws/index.js"(exports2, module2) {
5838
- var fs = require("fs"), crypto = require("crypto"), { homedir, EOL } = require("os"), { join: join2, dirname: dirname2, basename } = require("path"), { exec: exec2, run: run2, isCommandExist, catchErrors, wrapErrors } = require_core(), settings2 = require_settings(), { CLI_VERSION: CLI_VERSION2 } = settings2, transforms = require_transforms(), { loginFlags, isRemoteSession, atLeast } = require_login(), regions = require_regions(), IS_WINDOWS = process.platform === "win32", NL = IS_WINDOWS ? `
5929
+ var fs = require("fs"), crypto = require("crypto"), { homedir, EOL } = require("os"), { join: join2, dirname: dirname2, basename } = require("path"), { exec: exec2, run: run2, isCommandExist, catchErrors, wrapErrors } = require_core(), settings2 = require_settings(), { CLI_VERSION: CLI_VERSION2 } = settings2, transforms = require_transforms(), { loginFlags, isRemoteSession, atLeast } = require_login(), regions = require_regions(), log3 = require_log(), IS_WINDOWS = process.platform === "win32", NL = IS_WINDOWS ? `
5839
5930
  ` : EOL, AWS_CONFIG_FILE = join2(homedir(), ".aws", "config"), AWS_CREDS_FILE = join2(homedir(), ".aws", "credentials"), AWS_SSO_FOLDER = join2(homedir(), ".aws", "sso", "cache"), awsExists = isCommandExist("aws"), _awsCliVersion = null, whichAws = () => exec2(`${process.platform == "win32" ? "where" : "which"} aws`).then((out) => (out || "").split(/\r?\n/)[0].trim(), () => null), getAwsCliVersion = async () => {
5840
5931
  if (_awsCliVersion)
5841
5932
  return _awsCliVersion;
5842
- let path2 = await whichAws(), stat = path2 ? await fs.promises.stat(fs.realpathSync(path2)).catch(() => null) : null, cached = (await settings2.read().catch(() => ({}))).awsCli;
5843
- return stat && cached && cached.path == path2 && cached.mtimeMs == stat.mtimeMs && cached.version ? _awsCliVersion = cached.version : (_awsCliVersion = ((await exec2("aws --version") || "").match(/aws-cli\/(\S+)/) || [])[1] || null, stat && _awsCliVersion && await settings2.update({ awsCli: { path: path2, mtimeMs: stat.mtimeMs, version: _awsCliVersion } }).catch(() => null), _awsCliVersion);
5933
+ let path2 = await whichAws(), stat = path2 ? await fs.promises.stat(fs.realpathSync(path2)).catch(() => null) : null, cached = (await settings2.read().catch(log3.tolerated("reading the settings", {}))).awsCli;
5934
+ return stat && cached && cached.path == path2 && cached.mtimeMs == stat.mtimeMs && cached.version ? _awsCliVersion = cached.version : (_awsCliVersion = ((await exec2("aws --version") || "").match(/aws-cli\/(\S+)/) || [])[1] || null, stat && _awsCliVersion && await settings2.update({ awsCli: { path: path2, mtimeMs: stat.mtimeMs, version: _awsCliVersion } }).catch(log3.tolerated("caching the AWS CLI version", null)), _awsCliVersion);
5844
5935
  }, awsCliV2Exists = (noFailIfMissing) => catchErrors((async () => {
5845
5936
  let [awsCliErrors] = await awsExists();
5846
5937
  if (awsCliErrors) {
@@ -5932,9 +6023,9 @@ var require_aws = __commonJS({
5932
6023
  if (!atLeast(version, [2, 32, 0]))
5933
6024
  throw new Error(`'aws login' needs AWS CLI 2.32 or later (you have ${version}). Update the AWS CLI and try again.`);
5934
6025
  let remote = isRemoteSession(process.env, process.platform) ? ["--remote"] : [];
5935
- await run2("aws", ["login", "--profile", profile, ...remote], { inherit: !0 });
6026
+ await run2("aws", ["login", "--profile", profile, ...remote], { tee: !0 });
5936
6027
  }, logoutAll = async () => {
5937
- await run2("aws", ["sso", "logout"]), atLeast(await getAwsCliVersion(), [2, 32, 0]) && await run2("aws", ["logout", "--all"]).catch(() => null);
6028
+ await run2("aws", ["sso", "logout"]), atLeast(await getAwsCliVersion(), [2, 32, 0]) && await run2("aws", ["logout", "--all"]).catch(log3.tolerated("aws logout --all", null));
5938
6029
  }, readSsoToken = async ({ ssoSession, startUrl }) => {
5939
6030
  let key = crypto.createHash("sha1").update(ssoSession || startUrl).digest("hex"), token = await fs.promises.readFile(join2(AWS_SSO_FOLDER, `${key}.json`), "utf8").then(JSON.parse).catch(() => null);
5940
6031
  return !token || !token.accessToken || new Date(token.expiresAt).getTime() < Date.now() + 60 * 1e3 ? null : token;
@@ -6016,9 +6107,9 @@ var require_aws = __commonJS({
6016
6107
  })()), createSsoProfile = (name) => catchErrors((async () => {
6017
6108
  await awsCliV2Exists();
6018
6109
  try {
6019
- await run2("aws", ["configure", "sso", "--profile", name], { inherit: !0 });
6110
+ await run2("aws", ["configure", "sso", "--profile", name], { tee: !0 });
6020
6111
  } catch (err) {
6021
- throw new Error(`'aws configure sso' failed (${err.message}). The SSO profile may not have been created correctly.`, { cause: err });
6112
+ throw Object.assign(new Error(`'aws configure sso' failed: ${err.message}`, { cause: err }), { output: err.output || "" });
6022
6113
  }
6023
6114
  let [configStrErrors, configStr] = await getConfigFile();
6024
6115
  if (configStrErrors)
@@ -6059,7 +6150,7 @@ var require_aws = __commonJS({
6059
6150
  // src/shell.js
6060
6151
  var require_shell = __commonJS({
6061
6152
  "src/shell.js"(exports2, module2) {
6062
- var fs = require("fs"), { homedir } = require("os"), { join: join2, dirname: dirname2, basename } = require("path"), { run: run2 } = require_core(), IS_WINDOWS = process.platform === "win32", IS_MAC = process.platform === "darwin", FUNCTION_NAME = "sp", BLOCK_START_PREFIX = "# >>> switch-profile", BLOCK_END = "# <<< switch-profile <<<", BLOCK_REGEX = /(^|\r?\n)# >>> switch-profile[^\n]*\r?\n[\s\S]*?# <<< switch-profile <<<[^\n]*(\r?\n)?/, blockStart = (version) => `${BLOCK_START_PREFIX} v${version} (managed by switch-profile, do not edit) >>>`, CLEARED_VARS = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_DEFAULT_PROFILE"], SUBCOMMANDS = ["status", "login", "logout", "add", "remove", "settings", "use"], PROFILES_SED = "sed -n 's/^\\[profile \\(.*\\)\\]$/\\1/p'", POSIX_FUNCTION = (shell) => `${FUNCTION_NAME}() {
6153
+ var fs = require("fs"), { homedir } = require("os"), { join: join2, dirname: dirname2, basename } = require("path"), { run: run2 } = require_core(), log3 = require_log(), IS_WINDOWS = process.platform === "win32", IS_MAC = process.platform === "darwin", FUNCTION_NAME = "sp", BLOCK_START_PREFIX = "# >>> switch-profile", BLOCK_END = "# <<< switch-profile <<<", BLOCK_REGEX = /(^|\r?\n)# >>> switch-profile[^\n]*\r?\n[\s\S]*?# <<< switch-profile <<<[^\n]*(\r?\n)?/, blockStart = (version) => `${BLOCK_START_PREFIX} v${version} (managed by switch-profile, do not edit) >>>`, CLEARED_VARS = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_DEFAULT_PROFILE"], SUBCOMMANDS = ["status", "login", "logout", "add", "remove", "settings", "use"], PROFILES_SED = "sed -n 's/^\\[profile \\(.*\\)\\]$/\\1/p'", POSIX_FUNCTION = (shell) => `${FUNCTION_NAME}() {
6063
6154
  local __sp_file __sp_status
6064
6155
  __sp_file="$(mktemp "\${TMPDIR:-/tmp}/switch-profile.XXXXXX")" || return 1
6065
6156
  if [ -n "\${SWITCH_PROFILE_DEV_BIN:-}" ]; then
@@ -6193,7 +6284,7 @@ Register-ArgumentCompleter -CommandName ${FUNCTION_NAME} -ParameterName Rest -Sc
6193
6284
  if (IS_WINDOWS && !shellPath) {
6194
6285
  let files = [];
6195
6286
  for (let exe of ["pwsh", "powershell"]) {
6196
- let file = await run2(exe, ["-NoProfile", "-NonInteractive", "-Command", "$PROFILE.CurrentUserAllHosts"]).then((out) => (out || "").trim()).catch(() => "");
6287
+ let file = await run2(exe, ["-NoProfile", "-NonInteractive", "-Command", "$PROFILE.CurrentUserAllHosts"]).then((out) => (out || "").trim()).catch(log3.tolerated("reading the PowerShell profile path", ""));
6197
6288
  file && !files.includes(file) && files.push(file);
6198
6289
  }
6199
6290
  if (files.length)
@@ -6316,7 +6407,7 @@ var require_migrate = __commonJS({
6316
6407
  // src/commands/common.js
6317
6408
  var require_common = __commonJS({
6318
6409
  "src/commands/common.js"(exports2, module2) {
6319
- var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), ui2 = require_ui(), CancelError2 = class extends Error {
6410
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), log3 = require_log(), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), ui2 = require_ui(), CancelError2 = class extends Error {
6320
6411
  }, ask = async (promise) => {
6321
6412
  let value = await promise;
6322
6413
  if (p4.isCancel(value))
@@ -6329,14 +6420,14 @@ var require_common = __commonJS({
6329
6420
  }, preflight = async ({ interactive, quiet }) => {
6330
6421
  let [cliErrors, cliExists] = await aws.awsCliV2Exists(!0);
6331
6422
  if (cliErrors)
6332
- throw new ui2.CliError(ui2.errorsMessage(cliErrors), { hint: "switch-profile needs AWS CLI v2." });
6423
+ throw ui2.cliErrorFrom(cliErrors, { hint: "switch-profile needs AWS CLI v2." });
6333
6424
  if (!cliExists)
6334
6425
  throw new ui2.CliError("The AWS CLI is not installed.", { hint: INSTALL_HINTS[process.platform] || INSTALL_HINTS.linux });
6335
6426
  let result;
6336
6427
  try {
6337
6428
  result = await migrate.runMigrations();
6338
6429
  } catch (err) {
6339
- throw err instanceof migrate.NewerFormatError ? new ui2.CliError(err.message) : err;
6430
+ throw err instanceof migrate.NewerFormatError ? new ui2.CliError(err.message, { cause: err }) : err;
6340
6431
  }
6341
6432
  if (result.migrated && !quiet && p4.log.info([
6342
6433
  `Upgraded your AWS setup for switch-profile ${settings2.CLI_VERSION} ${ui2.unicode ? "\u{1F389}" : ""}`,
@@ -6347,7 +6438,7 @@ var require_common = __commonJS({
6347
6438
  let legacy = await migrate.findLegacySsoProfiles();
6348
6439
  legacy.length && (p4.log.warn(`${legacy.length > 1 ? `${legacy.length} profiles use` : "1 profile uses"} the old SSO format, which never auto-refreshes: ${legacy.join(", ")}`), await ask(p4.confirm({ message: "Upgrade them now? You'll log in once per SSO portal." })) ? await upgradeLegacySso(legacy) : (await settings2.update({ ssoUpgradeDeclined: !0 }), p4.log.message(ui2.dim("No problem. You can upgrade later from Settings."))));
6349
6440
  }
6350
- let status = await shell.getStatus(settings2.CLI_VERSION).catch(() => null);
6441
+ let status = await shell.getStatus(settings2.CLI_VERSION).catch(log3.tolerated("reading the sp shortcut status", null));
6351
6442
  status && status.installed && status.outdated && await shell.install(settings2.CLI_VERSION).then(() => !0, () => !1) && !quiet && p4.log.info(`Updated the ${ui2.accent(shell.FUNCTION_NAME)} shortcut in ${status.shell.files.map(shell.tilde).join(", ")} ${ui2.dim("(new terminals pick it up)")}`);
6352
6443
  }, upgradeLegacySso = async (names) => {
6353
6444
  let { sessions, backups } = await migrate.upgradeLegacySsoProfiles(names);
@@ -6360,10 +6451,10 @@ var require_common = __commonJS({
6360
6451
  }, loadState = async () => {
6361
6452
  let [listErrors, profiles] = await aws.listProfiles();
6362
6453
  if (listErrors)
6363
- throw new ui2.CliError(ui2.errorsMessage(listErrors));
6454
+ throw ui2.cliErrorFrom(listErrors);
6364
6455
  let [defaultErrors, info] = await aws.getDefaultProfile();
6365
6456
  if (defaultErrors)
6366
- throw new ui2.CliError(ui2.errorsMessage(defaultErrors));
6457
+ throw ui2.cliErrorFrom(defaultErrors);
6367
6458
  let defaultProfile = profiles.find((x2) => x2.name == info.profile) || null;
6368
6459
  return { profiles, defaultName: defaultProfile ? defaultProfile.name : null, defaultProfile, settings: await settings2.read() };
6369
6460
  }, loginKey = (profile) => profile.sso_session || profile.sso_start_url || profile.name, describe = (profile, identity) => ui2.join([
@@ -7392,11 +7483,11 @@ var require_clipboard = __commonJS({
7392
7483
  // src/commands/login-flow.js
7393
7484
  var require_login_flow = __commonJS({
7394
7485
  "src/commands/login-flow.js"(exports2, module2) {
7395
- var { spawn } = require("child_process"), p4 = (init_dist4(), __toCommonJS(dist_exports2)), { renderUnicodeCompact } = require_dist(), aws = require_aws(), settings2 = require_settings(), { parseLoginOutput, isRemoteSession, atLeast } = require_login(), clipboard = require_clipboard(), ui2 = require_ui(), { CancelError: CancelError2 } = require_common(), IS_WINDOWS = process.platform == "win32", RAW_OUTPUT_AFTER_MS = 1e4, clipboardNote = (result) => result == "local" ? ui2.ok("\u{1F4CB} copied") : result == "terminal" ? ui2.dim("\u{1F4CB} sent to your clipboard") : "", ssoLogin = async (target, { label, loginKey, interactive, loginMode }) => {
7486
+ var { spawn } = require("child_process"), p4 = (init_dist4(), __toCommonJS(dist_exports2)), { renderUnicodeCompact } = require_dist(), aws = require_aws(), settings2 = require_settings(), { parseLoginOutput, isRemoteSession, atLeast } = require_login(), clipboard = require_clipboard(), ui2 = require_ui(), log3 = require_log(), { CancelError: CancelError2 } = require_common(), IS_WINDOWS = process.platform == "win32", RAW_OUTPUT_AFTER_MS = 1e4, clipboardNote = (result) => result == "local" ? ui2.ok("\u{1F4CB} copied") : result == "terminal" ? ui2.dim("\u{1F4CB} sent to your clipboard") : "", ssoLogin = async (target, { label, loginKey, interactive, loginMode }) => {
7396
7487
  let current = await settings2.read(), args = await aws.ssoLoginArgs(target, loginMode || settings2.getLoginMode(current)), legacyCli = !atLeast(await aws.getAwsCliVersion(), [2, 22, 0]), device = args.includes("--use-device-code") || legacyCli, remote = isRemoteSession(process.env, process.platform);
7397
7488
  if (!interactive)
7398
7489
  return plainLogin(args, label);
7399
- let child = spawn("aws", args, { stdio: ["ignore", "pipe", "pipe"], ...IS_WINDOWS ? { shell: !0 } : {} }), output = "", shown = !1, rawMode = !1, spin = null, parsed = {}, stdin2 = process.stdin, onKey = async (key) => {
7490
+ let started = Date.now(), child = spawn("aws", args, { stdio: ["ignore", "pipe", "pipe"], ...IS_WINDOWS ? { shell: !0 } : {} }), output = "", shown = !1, rawMode = !1, spin = null, parsed = {}, stdin2 = process.stdin, onKey = async (key) => {
7400
7491
  if (key === "") {
7401
7492
  child.kill();
7402
7493
  return;
@@ -7441,7 +7532,7 @@ ${ui2.dim(`If nothing opens, visit: ${parsed.url}`)}`);
7441
7532
  }, RAW_OUTPUT_AFTER_MS), code = await new Promise((resolve) => {
7442
7533
  child.on("error", () => resolve(-1)), child.on("close", resolve);
7443
7534
  });
7444
- if (clearTimeout(fallback), cleanup(), code === 0) {
7535
+ if (clearTimeout(fallback), cleanup(), log3.command({ cmd: "aws", args, code: child.killed ? "killed (cancelled)" : code, ms: Date.now() - started, output }), code === 0) {
7445
7536
  spin ? spin.stop(`${ui2.unicode ? "\u{1F513} " : ""}Logged in`) : p4.log.success("Logged in"), await rememberLogin(loginKey);
7446
7537
  return;
7447
7538
  }
@@ -7453,12 +7544,15 @@ ${ui2.dim(`If nothing opens, visit: ${parsed.url}`)}`);
7453
7544
  `) || lines.slice(-1)[0] || "";
7454
7545
  throw new ui2.CliError(`SSO login for ${label} failed.${detail ? `
7455
7546
  ${detail}` : ""}`, {
7456
- hint: /invalid_grant|InvalidGrant/i.test(output) ? "Check the SSO region of this profile: it must be the region of your IAM Identity Center, not where your resources live." : "Try again, or run with --debug."
7547
+ cause: Object.assign(new Error(`aws ${args.join(" ")} exited with code ${code}`), { output }),
7548
+ hint: /invalid_grant|InvalidGrant/i.test(output) ? "Check the SSO region of this profile: it must be the region of your IAM Identity Center, not where your resources live." : `Try again. Details: ${log3.displayPath()}`
7457
7549
  });
7458
7550
  }, plainLogin = async (args, label) => {
7459
7551
  console.error(`Logging in to ${label}. Approve the request using the URL and code below.`), await new Promise((resolve, reject) => {
7460
- let child = spawn("aws", args, { stdio: ["ignore", process.stderr, process.stderr], ...IS_WINDOWS ? { shell: !0 } : {} });
7461
- child.on("error", reject), child.on("close", (code) => code === 0 ? resolve() : reject(new ui2.CliError(`SSO login for ${label} failed.`, { code: 2 })));
7552
+ let started = Date.now(), child = spawn("aws", args, { stdio: ["ignore", process.stderr, process.stderr], ...IS_WINDOWS ? { shell: !0 } : {} });
7553
+ child.on("error", reject), child.on("close", (code) => {
7554
+ log3.command({ cmd: "aws", args, code, ms: Date.now() - started, output: "(output passed through to stderr)" }), code === 0 ? resolve() : reject(new ui2.CliError(`SSO login for ${label} failed.`, { code: 2 }));
7555
+ });
7462
7556
  });
7463
7557
  }, rememberLogin = async (key) => {
7464
7558
  if (!key)
@@ -7540,7 +7634,7 @@ ${gray(S_BAR2)}`;
7540
7634
  // src/commands/switch.js
7541
7635
  var require_switch = __commonJS({
7542
7636
  "src/commands/switch.js"(exports2, module2) {
7543
- var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), rank = require_rank(), ui2 = require_ui(), { ask, preflight, loadState, loginKey, describe, loginState, CancelError: CancelError2 } = require_common(), { ssoLogin } = require_login_flow(), { home, ACTION_PREFIX } = require_home(), header = async (state, { warnings } = {}) => {
7637
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), rank = require_rank(), ui2 = require_ui(), log3 = require_log(), { ask, preflight, loadState, loginKey, describe, loginState, CancelError: CancelError2 } = require_common(), { ssoLogin } = require_login_flow(), { home, ACTION_PREFIX } = require_home(), header = async (state, { warnings } = {}) => {
7544
7638
  p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7545
7639
  let lines = [];
7546
7640
  if (state.defaultProfile) {
@@ -7601,20 +7695,20 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7601
7695
  return result;
7602
7696
  let loginProfile = profile.kind == "role" && profile.source_profile ? state.profiles.find((x2) => x2.name == profile.source_profile) : profile;
7603
7697
  if (!(loginProfile && (loginProfile.kind == "sso" || loginProfile.kind == "login")))
7604
- throw new ui2.CliError(`Could not get credentials for ${profile.name}.`, { hint: firstLine(result.message) });
7698
+ throw new ui2.CliError(`Could not get credentials for ${profile.name}.`, { hint: firstLine(result.message), cause: result });
7605
7699
  if (!interactive && !force)
7606
7700
  throw new ui2.CliError(`${profile.name} needs a login.`, { code: 2, hint: `Run: switch-profile login ${loginProfile.name}` });
7607
7701
  if (loginProfile.kind == "sso" ? await ssoLogin({ profile: loginProfile.name }, { label: loginProfile.sso_session || loginProfile.name, loginKey: loginKey(loginProfile), interactive }) : await aws.awsLogin(loginProfile.name), result = await tryIdentity(), result instanceof Error)
7608
- throw new ui2.CliError(`Logged in, but ${profile.name} still has no access.`, { hint: firstLine(result.message) });
7702
+ throw new ui2.CliError(`Logged in, but ${profile.name} still has no access.`, { hint: firstLine(result.message), cause: result });
7609
7703
  return result;
7610
7704
  }, firstLine = (text2) => String(text2 || "").trim().split(`
7611
7705
  `).filter(Boolean).slice(-1)[0], switchTo = async (profile, state, opts) => (await doSwitch(profile, state, opts), { switched: !0 }), doSwitch = async (profile, state, opts) => {
7612
7706
  let { interactive, json } = opts, identity = await connect(profile, state, { interactive, quiet: json, force: opts.force }), [defaultErrors] = await aws.setDefaultProfile(profile.name);
7613
7707
  if (defaultErrors)
7614
- throw new ui2.CliError(ui2.errorsMessage(defaultErrors));
7708
+ throw ui2.cliErrorFrom(defaultErrors);
7615
7709
  let current = await settings2.read();
7616
7710
  await settings2.update({ usage: rank.recordUsage(current.usage, profile.name) });
7617
- let terminal = await shell.exportProfile(profile.name).catch(() => !1);
7711
+ let terminal = await shell.exportProfile(profile.name).catch(log3.tolerated("handing the profile to sp", !1));
7618
7712
  if (json) {
7619
7713
  process.stdout.write(JSON.stringify({ profile: profile.name, account: identity.account, arn: identity.arn, region: profile.region || null, default: !0, terminal }) + `
7620
7714
  `);
@@ -7634,7 +7728,7 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7634
7728
  ${ui2.dim("set as the default profile")}`), await offerShortcut(profile);
7635
7729
  }, exportHint = (name) => process.platform == "win32" ? `$env:AWS_PROFILE = "${name}" ${ui2.dim("(PowerShell)")}
7636
7730
  set AWS_PROFILE=${name} ${ui2.dim("(CMD)")}` : `export AWS_PROFILE=${name}`, offerShortcut = async (profile) => {
7637
- let status = await shell.getStatus(settings2.CLI_VERSION).catch(() => null), hint = `${ui2.dim("To use it in this terminal only, run:")}
7731
+ let status = await shell.getStatus(settings2.CLI_VERSION).catch(log3.tolerated("reading the sp shortcut status", null)), hint = `${ui2.dim("To use it in this terminal only, run:")}
7638
7732
  ${ui2.accent(exportHint(profile.name))}`;
7639
7733
  if (status && status.installed) {
7640
7734
  p4.outro(`${hint}
@@ -7690,7 +7784,7 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7690
7784
  return result;
7691
7785
  if (next == "throw")
7692
7786
  throw error;
7693
- next == "back" && p4.cancel(ui2.dim(`${ui2.unicode ? "\u2190" : "<-"} Back`)), next == "error" && ui2.printError(error), state = await loadState();
7787
+ next == "back" && p4.cancel(ui2.dim(`${ui2.unicode ? "\u2190" : "<-"} Back`)), next == "error" && (log3.error(`${route.key} failed (back to the home screen)`, error), ui2.printError(error)), state = await loadState();
7694
7788
  }
7695
7789
  }, switchCommand = async (query, opts) => {
7696
7790
  let interactive = ui2.isInteractive(opts);
@@ -7729,7 +7823,7 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7729
7823
  // src/commands/index.js
7730
7824
  var require_commands = __commonJS({
7731
7825
  "src/commands/index.js"(exports2, module2) {
7732
- var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), transforms = require_transforms(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), rank = require_rank(), { resolveLoginMode, isRemoteSession } = require_login(), ui2 = require_ui(), { ask, preflight, loadState, loginKey, describe, loginState, upgradeLegacySso } = require_common(), { ssoLogin, rememberLogin } = require_login_flow(), { switchCommand, switchTo, connect, enableShortcut } = require_switch(), intro2 = (opts) => {
7826
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), log3 = require_log(), aws = require_aws(), transforms = require_transforms(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), rank = require_rank(), { resolveLoginMode, isRemoteSession, configureSsoFailure } = require_login(), ui2 = require_ui(), { ask, preflight, loadState, loginKey, describe, loginState, upgradeLegacySso } = require_common(), { ssoLogin, rememberLogin } = require_login_flow(), { switchCommand, switchTo, connect, enableShortcut } = require_switch(), intro2 = (opts) => {
7733
7827
  !opts.fromPicker && !opts.json && p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7734
7828
  }, begin = async (opts, { needsInput } = {}) => {
7735
7829
  let interactive = ui2.isInteractive(opts);
@@ -7748,7 +7842,7 @@ var require_commands = __commonJS({
7748
7842
  return interactive && !opts.json && intro2(opts), switchTo(findProfile(state, name), state, { ...opts, interactive });
7749
7843
  }, status = async (args, opts) => {
7750
7844
  await begin(opts);
7751
- let state = await loadState(), terminal = process.env.AWS_PROFILE || null, shortcut = await shell.getStatus(settings2.CLI_VERSION).catch(() => null), d = state.defaultProfile, login2 = d ? await loginState(d, state) : { text: null, needsLogin: !1 };
7845
+ let state = await loadState(), terminal = process.env.AWS_PROFILE || null, shortcut = await shell.getStatus(settings2.CLI_VERSION).catch(log3.tolerated("reading the sp shortcut status", null)), d = state.defaultProfile, login2 = d ? await loginState(d, state) : { text: null, needsLogin: !1 };
7752
7846
  if (opts.json) {
7753
7847
  let at = d ? (state.settings.logins || {})[loginKey(d)] : null;
7754
7848
  process.stdout.write(JSON.stringify({
@@ -7813,21 +7907,31 @@ var require_commands = __commonJS({
7813
7907
  return importFromSso(state, { ...opts, interactive });
7814
7908
  let taken = state.profiles.map((x2) => x2.name), name = await ask(p4.text({ message: "Profile name", placeholder: "e.g. acme-prod", validate: validateProfileName(taken) }));
7815
7909
  if (kind == "sso") {
7910
+ let [configErrors, configStr] = await aws.getConfigFile();
7911
+ if (configErrors)
7912
+ throw ui2.cliErrorFrom(configErrors);
7913
+ let sessions = transforms.listSsoSessions(configStr);
7816
7914
  p4.log.message([
7817
7915
  "Handing over to aws configure sso. You will be asked for:",
7818
- `${ui2.dim("1.")} a session name ${ui2.dim("(reuse the same one for every profile of a portal: one login covers them all)")}`,
7916
+ `${ui2.dim("1.")} a session name ${ui2.dim("(a short name such as acme, not the URL; reuse the same one for every profile of a portal: one login covers them all)")}`,
7819
7917
  `${ui2.dim("2.")} the start URL ${ui2.dim("(e.g. https://acme.awsapps.com/start)")}`,
7820
- `${ui2.dim("3.")} the SSO region ${ui2.dim("(where IAM Identity Center lives, not where you deploy)")}`
7918
+ `${ui2.dim("3.")} the SSO region ${ui2.dim("(where IAM Identity Center lives, not where you deploy)")}`,
7919
+ ...sessions.length ? [ui2.dim(`Portals already set up: ${sessions.map((x2) => `${x2.name} (${x2.sso_region || "?"})`).join(", ")}. Type one of these names to reuse it.`)] : []
7821
7920
  ].join(`
7822
7921
  `));
7823
7922
  let [errors, upgraded] = await aws.createSsoProfile(name);
7824
- if (errors)
7825
- throw new ui2.CliError(ui2.errorsMessage(errors));
7923
+ if (errors) {
7924
+ let failed = errors.find((e) => e.output != null);
7925
+ if (!failed)
7926
+ throw ui2.cliErrorFrom(errors);
7927
+ let { message, hint } = configureSsoFailure(failed.output, sessions);
7928
+ throw new ui2.CliError(message, { hint, errors });
7929
+ }
7826
7930
  upgraded && p4.log.info(`No session name was given, so ${name} now uses an [sso-session] for auto-refresh. You'll log in once more.`);
7827
7931
  } else if (kind == "login") {
7828
7932
  let region = await chooseRegion("Default region", "us-east-1"), [configErrors, configStr] = await aws.getConfigFile();
7829
7933
  if (configErrors)
7830
- throw new ui2.CliError(ui2.errorsMessage(configErrors));
7934
+ throw ui2.cliErrorFrom(configErrors);
7831
7935
  let ini = require_ini();
7832
7936
  await aws.writeAwsFile(aws.AWS_CONFIG_FILE, ini.setSection(configStr, `profile ${name}`, [["region", region], [transforms.VERSION_KEY, settings2.CLI_VERSION]])), p4.log.step("Signing in with aws login\u2026"), await aws.awsLogin(name);
7833
7937
  } else {
@@ -7836,7 +7940,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7836
7940
  return p4.outro(ui2.dim("Nothing changed."));
7837
7941
  let aws_access_key_id = await ask(p4.text({ message: "Access key ID", validate: (v) => v ? void 0 : "Required." })), aws_secret_access_key = await ask(p4.password({ message: "Secret access key", validate: (v) => v ? void 0 : "Required." })), region = await chooseRegion("Default region", "us-east-1"), [errors] = await aws.createProfile({ name, aws_access_key_id, aws_secret_access_key, region });
7838
7942
  if (errors)
7839
- throw new ui2.CliError(ui2.errorsMessage(errors));
7943
+ throw ui2.cliErrorFrom(errors);
7840
7944
  }
7841
7945
  p4.log.success(`Profile ${ui2.accent(name)} created`);
7842
7946
  let next = await loadState();
@@ -7854,7 +7958,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7854
7958
  }, importFromSso = async (state, opts) => {
7855
7959
  let { interactive } = opts, [configErrors, configStr] = await aws.getConfigFile();
7856
7960
  if (configErrors)
7857
- throw new ui2.CliError(ui2.errorsMessage(configErrors));
7961
+ throw ui2.cliErrorFrom(configErrors);
7858
7962
  let sessions = transforms.listSsoSessions(configStr), sessionName = opts.fromSso && opts.fromSso !== !0 ? opts.fromSso : null;
7859
7963
  if (sessionName && !sessions.some((s) => s.name == sessionName))
7860
7964
  throw new ui2.CliError(`There is no [sso-session ${sessionName}] in ~/.aws/config.`, { code: 3, hint: sessions.length ? `Known sessions: ${sessions.map((s) => s.name).join(", ")}` : "Run switch-profile add in a terminal to add a portal." });
@@ -7892,7 +7996,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7892
7996
  entries = accounts.flatMap((a2, i2) => roles[i2].map((roleName) => ({ ...a2, roleName }))), spin && spin.stop(`Found ${accounts.length} account${accounts.length == 1 ? "" : "s"} and ${entries.length} role${entries.length == 1 ? "" : "s"}`);
7893
7997
  } catch (err) {
7894
7998
  throw spin && spin.error("Could not read the accounts"), new ui2.CliError(`Could not list the accounts of ${sessionName}.`, { hint: String(err.message).trim().split(`
7895
- `).slice(-1)[0] });
7999
+ `).slice(-1)[0], cause: err });
7896
8000
  }
7897
8001
  let sessionRegions = state.profiles.filter((x2) => x2.sso_session == sessionName && x2.region).map((x2) => x2.region), region = opts.region || (interactive ? await chooseRegion("Default region for these profiles", sessionRegions[0] || session.sso_region) : session.sso_region), prefix = opts.prefix || (interactive ? await ask(p4.text({ message: `Name prefix ${ui2.dim("(profiles become <prefix>-<account>-<role>)")}`, initialValue: sessionName, validate: (v) => /^[a-z0-9_-]*$/.test(v || "") ? void 0 : "Lowercase letters, numbers, - and _ only." })) : sessionName), preview = transforms.populateSsoProfiles(configStr, { ssoSession: sessionName, entries, region, prefix, prune: !1, version: settings2.CLI_VERSION });
7898
8002
  if (!preview.added.length && !preview.stale.length)
@@ -7953,7 +8057,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7953
8057
  }
7954
8058
  let [errors] = await aws.deleteProfiles(names);
7955
8059
  if (errors)
7956
- throw new ui2.CliError(ui2.errorsMessage(errors));
8060
+ throw ui2.cliErrorFrom(errors);
7957
8061
  p4.outro(`${ui2.ok(ui2.sym.ok)} Removed ${names.join(", ")}`);
7958
8062
  }, LOGIN_MODE_LABELS = {
7959
8063
  auto: "Auto: device code over SSH, browser otherwise",
@@ -8012,7 +8116,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
8012
8116
  });
8013
8117
 
8014
8118
  // src/cli.js
8015
- var { Command } = require_commander(), p3 = (init_dist4(), __toCommonJS(dist_exports2)), ui = require_ui(), { CLI_VERSION } = require_settings(), { CancelError } = require_common(), commands = require_commands();
8119
+ var { Command } = require_commander(), p3 = (init_dist4(), __toCommonJS(dist_exports2)), ui = require_ui(), log2 = require_log(), { CLI_VERSION } = require_settings(), { CancelError } = require_common(), commands = require_commands();
8016
8120
  process.env.AWS_CLI_AGENT_TOOLKIT_HINT_DISABLED = "true";
8017
8121
  var EXAMPLES = `
8018
8122
  Examples:
@@ -8027,19 +8131,28 @@ Examples:
8027
8131
  sets AWS_PROFILE in the current terminal. Without it, use 'npx switch-profile'.
8028
8132
 
8029
8133
  Exit codes: 0 ok \xB7 1 error \xB7 2 login required \xB7 3 bad input \xB7 130 cancelled`, run = (action) => async (...args) => {
8030
- let opts = args[args.length - 1].optsWithGlobals();
8134
+ let cmd = args[args.length - 1], opts = cmd.optsWithGlobals();
8031
8135
  opts.debug && (process.env.SWITCH_PROFILE_DEBUG = "1");
8032
8136
  try {
8033
8137
  await action(...args.slice(0, -2), opts);
8034
8138
  } catch (err) {
8035
8139
  if (err instanceof CancelError) {
8036
- p3.cancel(`Cancelled ${ui.unicode ? "\u{1F44B}" : ""}`), process.exitCode = 130;
8140
+ log2.info("cancelled by the user"), p3.cancel(`Cancelled ${ui.unicode ? "\u{1F44B}" : ""}`), process.exitCode = 130;
8037
8141
  return;
8038
8142
  }
8039
- opts.json && err instanceof ui.CliError ? process.stderr.write(JSON.stringify({ error: err.message, hint: err.hint || null, code: err.exitCode }) + `
8143
+ log2.error(`${cmd.name()} failed`, err), opts.json && err instanceof ui.CliError ? process.stderr.write(JSON.stringify({ error: err.message, hint: err.hint || null, code: err.exitCode }) + `
8040
8144
  `) : ui.printError(err), process.exitCode = err.exitCode || 1;
8041
8145
  }
8146
+ }, crashed = (err) => {
8147
+ log2.error("unexpected crash", err);
8148
+ try {
8149
+ ui.printError(err instanceof Error ? err : new Error(String(err)));
8150
+ } catch {
8151
+ console.error(err);
8152
+ }
8153
+ process.exit(1);
8042
8154
  }, main = (argv) => {
8155
+ log2.start({ version: CLI_VERSION, argv }), process.on("uncaughtException", crashed), process.on("unhandledRejection", crashed), process.on("exit", (code) => log2.info(`exit ${code}`));
8043
8156
  let program = new Command();
8044
8157
  return program.name("switch-profile").description("Switch between AWS profiles, log in with SSO, and keep every terminal on the right account.").version(CLI_VERSION, "-v, --version").argument("[profile...]", "profile name or search text").option("--json", "machine-readable output").option("--no-input", "never prompt (fails with a hint instead)").option("--debug", "show error details").addHelpText("after", EXAMPLES).showSuggestionAfterError().action(run((words, opts) => commands.switch((words || []).join(" "), opts))), program.command("use").argument("<profile>").description("switch to a profile by its exact name").action(run(commands.use)), program.command("status").description("show the current profile and login state").action(run((opts) => commands.status([], opts))), program.command("login").argument("[profile]", "defaults to this terminal's profile, then the default profile").description("log in again now, even if the session is still valid").option("--device", "use a device code (approve from any device)").option("--browser", "use the browser on this machine").action(run(commands.login)), program.command("logout").description("end every SSO session on this machine").option("-y, --yes", "do not ask for confirmation").action(run((opts) => commands.logout([], opts))), program.command("add").description("add profiles: import an SSO portal, or create one").option("--from-sso [session]", "import (or re-sync) every account and role of an [sso-session]").option("--region <region>", "default region of imported profiles").option("--prefix <prefix>", "name prefix of imported profiles").option("--prune", "remove imported profiles that no longer exist").option("-y, --yes", "do not ask for confirmation").action(run((opts) => commands.add([], opts))), program.command("remove").alias("rm").argument("[profiles...]").description("remove profiles").option("-y, --yes", "do not ask for confirmation").action(run(commands.remove)), program.command("settings").description("per-terminal switching, login mode and more").action(run((opts) => commands.settings([], opts))), program.command("switch", { hidden: !0 }).action(run((opts) => commands.switch("", opts))), program.parseAsync(argv);
8045
8158
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switch-profile",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "main": "index.js",
5
5
  "bin": {
6
6
  "switch-profile": "index.js"