switch-profile 2.1.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 +13 -3
  2. package/dist/cli.js +193 -66
  3. package/package.json +2 -2
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
 
@@ -154,7 +156,7 @@ sp add # then choose "Accounts from an SSO portal"
154
156
  1. Pick an existing `[sso-session]` or create one: start URL (e.g. `https://acme.awsapps.com/start`), SSO region (where Identity Center lives, not where you deploy), and a short name, usually the client.
155
157
  2. Log in if needed.
156
158
  3. `switch-profile` lists every account and role you can access, then asks for a default region and a name prefix. Profiles are named `<prefix>-<account-name>-<role>`, for example `acme-prod-workloads-admin`.
157
- 4. A preview shows the new profiles, the ones already set up, and the ones that no longer exist. Confirm to write them.
159
+ 4. A preview shows the new profiles, the ones already set up, and the ones that no longer exist. Then pick the new profiles to add: all are selected, so press Enter to add everything, or untick the ones you don't want (type to search by name, account or role).
158
160
 
159
161
  Run it again later to pick up new accounts. Account/role pairs that already have a profile are never duplicated, whatever its name. Generated profiles that no longer exist can be pruned (after a backup); profiles you wrote by hand are never pruned.
160
162
 
@@ -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.1.0",
5422
+ version: "2.3.0",
5355
5423
  main: "index.js",
5356
5424
  bin: {
5357
5425
  "switch-profile": "index.js"
@@ -5396,7 +5464,7 @@ var require_package = __commonJS({
5396
5464
  eslint: "^10.0.0",
5397
5465
  fuzzysort: "^3.1.0",
5398
5466
  globals: "^17.13.0",
5399
- mocha: "^8.2.1",
5467
+ mocha: "^11.8.0",
5400
5468
  uqr: "^0.1.3"
5401
5469
  },
5402
5470
  files: [
@@ -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
  }
@@ -5640,14 +5726,16 @@ var require_transforms = __commonJS({
5640
5726
  login_session: params.login_session || null
5641
5727
  };
5642
5728
  return p4.isSso = !!p4.sso_start_url, p4.isLegacySso = p4.isSso && !p4.sso_session, p4.kind = p4.isSso ? "sso" : p4.login_session ? "login" : p4.role_arn ? "role" : params.credential_process ? "process" : "keys", !p4.sso_account_id && p4.role_arn && (p4.sso_account_id = (p4.role_arn.match(/^arn:aws[^:]*:iam::(\d{12}):/) || [])[1] || null), p4;
5643
- }), slug = (text2) => String(text2 || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""), generatedProfileName = (prefix, accountName, accountId, roleName) => [slug(prefix), slug(accountName) || accountId, slug(roleName)].filter(Boolean).join("-"), populateSsoProfiles = (configStr, { ssoSession, entries, region, prefix, prune, version }) => {
5644
- let config = configStr, profiles = listProfiles(config), session = ini.getSection(config, `sso-session ${ssoSession}`) || {}, samePortal = (p4) => p4.sso_session == ssoSession || p4.sso_start_url && p4.sso_start_url == session.sso_start_url, key = (accountId, roleName) => `${accountId}/${roleName}`, byKey = new Map(profiles.filter((p4) => p4.isSso && samePortal(p4)).map((p4) => [key(p4.sso_account_id, p4.sso_role_name), p4])), taken = new Set(profiles.map((p4) => p4.name)), wanted = new Set(entries.map((e) => key(e.accountId, e.roleName))), added = [], existing = [];
5729
+ }), slug = (text2) => String(text2 || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""), generatedProfileName = (prefix, accountName, accountId, roleName) => [slug(prefix), slug(accountName) || accountId, slug(roleName)].filter(Boolean).join("-"), ssoEntryKey = (accountId, roleName) => `${accountId}/${roleName}`, populateSsoProfiles = (configStr, { ssoSession, entries, region, prefix, prune, include, version }) => {
5730
+ let config = configStr, profiles = listProfiles(config), session = ini.getSection(config, `sso-session ${ssoSession}`) || {}, samePortal = (p4) => p4.sso_session == ssoSession || p4.sso_start_url && p4.sso_start_url == session.sso_start_url, key = ssoEntryKey, included = include ? new Set(include) : null, byKey = new Map(profiles.filter((p4) => p4.isSso && samePortal(p4)).map((p4) => [key(p4.sso_account_id, p4.sso_role_name), p4])), taken = new Set(profiles.map((p4) => p4.name)), wanted = new Set(entries.map((e) => key(e.accountId, e.roleName))), added = [], addedEntries = [], existing = [];
5645
5731
  for (let e of entries) {
5646
5732
  let found = byKey.get(key(e.accountId, e.roleName));
5647
5733
  if (found) {
5648
5734
  existing.push(found.name);
5649
5735
  continue;
5650
5736
  }
5737
+ if (included && !included.has(key(e.accountId, e.roleName)))
5738
+ continue;
5651
5739
  let base = generatedProfileName(prefix || ssoSession, e.accountName, e.accountId, e.roleName), name = base;
5652
5740
  for (let i2 = 2; taken.has(name); i2++)
5653
5741
  name = `${base}-${i2}`;
@@ -5660,13 +5748,13 @@ var require_transforms = __commonJS({
5660
5748
  ...e.accountName ? [[ACCOUNT_NAME_KEY, e.accountName]] : [],
5661
5749
  [GENERATED_KEY, ssoSession],
5662
5750
  [VERSION_KEY, version]
5663
- ]), added.push(name);
5751
+ ]), added.push(name), addedEntries.push({ name, accountId: e.accountId, accountName: e.accountName, roleName: e.roleName });
5664
5752
  }
5665
5753
  let stale = profiles.filter((p4) => p4.generated == ssoSession && !wanted.has(key(p4.sso_account_id, p4.sso_role_name))).map((p4) => p4.name), removed = [];
5666
5754
  if (prune)
5667
5755
  for (let name of stale)
5668
5756
  config = ini.removeSection(config, configSectionName(config, name)), removed.push(name);
5669
- return { config, added, existing, stale, removed };
5757
+ return { config, added, addedEntries, existing, stale, removed };
5670
5758
  }, listSsoSessions = (configStr) => ini.listSections(configStr).filter((s) => s.startsWith("sso-session ")).map((s) => ({ name: s.replace(/^sso-session\s+/, ""), ...ini.getSection(configStr, s) })), addSsoSession = (configStr, { name, startUrl, region, version }) => ini.getSection(configStr, `sso-session ${name}`) ? configStr : ini.setSection(configStr, `sso-session ${name}`, [
5671
5759
  ["sso_start_url", startUrl],
5672
5760
  ["sso_region", region],
@@ -5678,6 +5766,7 @@ var require_transforms = __commonJS({
5678
5766
  ACCOUNT_NAME_KEY,
5679
5767
  generatedProfileName,
5680
5768
  populateSsoProfiles,
5769
+ ssoEntryKey,
5681
5770
  listSsoSessions,
5682
5771
  addSsoSession,
5683
5772
  NAME_KEY,
@@ -5710,8 +5799,13 @@ var require_login = __commonJS({
5710
5799
  text2 = text2 || "";
5711
5800
  let urls = text2.match(URL_REGEX) || [], code = (text2.match(CODE_REGEX) || [])[1] || null, completeUrl = urls.find((u5) => /user_code=/.test(u5)) || null;
5712
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}` };
5713
5806
  };
5714
5807
  module2.exports = {
5808
+ configureSsoFailure,
5715
5809
  isRemoteSession,
5716
5810
  resolveLoginMode,
5717
5811
  loginFlags,
@@ -5832,12 +5926,12 @@ var require_regions = __commonJS({
5832
5926
  // src/aws/index.js
5833
5927
  var require_aws = __commonJS({
5834
5928
  "src/aws/index.js"(exports2, module2) {
5835
- 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 ? `
5836
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 () => {
5837
5931
  if (_awsCliVersion)
5838
5932
  return _awsCliVersion;
5839
- let path2 = await whichAws(), stat = path2 ? await fs.promises.stat(fs.realpathSync(path2)).catch(() => null) : null, cached = (await settings2.read().catch(() => ({}))).awsCli;
5840
- 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);
5841
5935
  }, awsCliV2Exists = (noFailIfMissing) => catchErrors((async () => {
5842
5936
  let [awsCliErrors] = await awsExists();
5843
5937
  if (awsCliErrors) {
@@ -5929,9 +6023,9 @@ var require_aws = __commonJS({
5929
6023
  if (!atLeast(version, [2, 32, 0]))
5930
6024
  throw new Error(`'aws login' needs AWS CLI 2.32 or later (you have ${version}). Update the AWS CLI and try again.`);
5931
6025
  let remote = isRemoteSession(process.env, process.platform) ? ["--remote"] : [];
5932
- await run2("aws", ["login", "--profile", profile, ...remote], { inherit: !0 });
6026
+ await run2("aws", ["login", "--profile", profile, ...remote], { tee: !0 });
5933
6027
  }, logoutAll = async () => {
5934
- 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));
5935
6029
  }, readSsoToken = async ({ ssoSession, startUrl }) => {
5936
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);
5937
6031
  return !token || !token.accessToken || new Date(token.expiresAt).getTime() < Date.now() + 60 * 1e3 ? null : token;
@@ -6013,9 +6107,9 @@ var require_aws = __commonJS({
6013
6107
  })()), createSsoProfile = (name) => catchErrors((async () => {
6014
6108
  await awsCliV2Exists();
6015
6109
  try {
6016
- await run2("aws", ["configure", "sso", "--profile", name], { inherit: !0 });
6110
+ await run2("aws", ["configure", "sso", "--profile", name], { tee: !0 });
6017
6111
  } catch (err) {
6018
- 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 || "" });
6019
6113
  }
6020
6114
  let [configStrErrors, configStr] = await getConfigFile();
6021
6115
  if (configStrErrors)
@@ -6056,7 +6150,7 @@ var require_aws = __commonJS({
6056
6150
  // src/shell.js
6057
6151
  var require_shell = __commonJS({
6058
6152
  "src/shell.js"(exports2, module2) {
6059
- 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}() {
6060
6154
  local __sp_file __sp_status
6061
6155
  __sp_file="$(mktemp "\${TMPDIR:-/tmp}/switch-profile.XXXXXX")" || return 1
6062
6156
  if [ -n "\${SWITCH_PROFILE_DEV_BIN:-}" ]; then
@@ -6190,7 +6284,7 @@ Register-ArgumentCompleter -CommandName ${FUNCTION_NAME} -ParameterName Rest -Sc
6190
6284
  if (IS_WINDOWS && !shellPath) {
6191
6285
  let files = [];
6192
6286
  for (let exe of ["pwsh", "powershell"]) {
6193
- 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", ""));
6194
6288
  file && !files.includes(file) && files.push(file);
6195
6289
  }
6196
6290
  if (files.length)
@@ -6313,7 +6407,7 @@ var require_migrate = __commonJS({
6313
6407
  // src/commands/common.js
6314
6408
  var require_common = __commonJS({
6315
6409
  "src/commands/common.js"(exports2, module2) {
6316
- 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 {
6317
6411
  }, ask = async (promise) => {
6318
6412
  let value = await promise;
6319
6413
  if (p4.isCancel(value))
@@ -6326,14 +6420,14 @@ var require_common = __commonJS({
6326
6420
  }, preflight = async ({ interactive, quiet }) => {
6327
6421
  let [cliErrors, cliExists] = await aws.awsCliV2Exists(!0);
6328
6422
  if (cliErrors)
6329
- 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." });
6330
6424
  if (!cliExists)
6331
6425
  throw new ui2.CliError("The AWS CLI is not installed.", { hint: INSTALL_HINTS[process.platform] || INSTALL_HINTS.linux });
6332
6426
  let result;
6333
6427
  try {
6334
6428
  result = await migrate.runMigrations();
6335
6429
  } catch (err) {
6336
- 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;
6337
6431
  }
6338
6432
  if (result.migrated && !quiet && p4.log.info([
6339
6433
  `Upgraded your AWS setup for switch-profile ${settings2.CLI_VERSION} ${ui2.unicode ? "\u{1F389}" : ""}`,
@@ -6344,7 +6438,7 @@ var require_common = __commonJS({
6344
6438
  let legacy = await migrate.findLegacySsoProfiles();
6345
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."))));
6346
6440
  }
6347
- 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));
6348
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)")}`);
6349
6443
  }, upgradeLegacySso = async (names) => {
6350
6444
  let { sessions, backups } = await migrate.upgradeLegacySsoProfiles(names);
@@ -6357,10 +6451,10 @@ var require_common = __commonJS({
6357
6451
  }, loadState = async () => {
6358
6452
  let [listErrors, profiles] = await aws.listProfiles();
6359
6453
  if (listErrors)
6360
- throw new ui2.CliError(ui2.errorsMessage(listErrors));
6454
+ throw ui2.cliErrorFrom(listErrors);
6361
6455
  let [defaultErrors, info] = await aws.getDefaultProfile();
6362
6456
  if (defaultErrors)
6363
- throw new ui2.CliError(ui2.errorsMessage(defaultErrors));
6457
+ throw ui2.cliErrorFrom(defaultErrors);
6364
6458
  let defaultProfile = profiles.find((x2) => x2.name == info.profile) || null;
6365
6459
  return { profiles, defaultName: defaultProfile ? defaultProfile.name : null, defaultProfile, settings: await settings2.read() };
6366
6460
  }, loginKey = (profile) => profile.sso_session || profile.sso_start_url || profile.name, describe = (profile, identity) => ui2.join([
@@ -7389,11 +7483,11 @@ var require_clipboard = __commonJS({
7389
7483
  // src/commands/login-flow.js
7390
7484
  var require_login_flow = __commonJS({
7391
7485
  "src/commands/login-flow.js"(exports2, module2) {
7392
- 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 }) => {
7393
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);
7394
7488
  if (!interactive)
7395
7489
  return plainLogin(args, label);
7396
- 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) => {
7397
7491
  if (key === "") {
7398
7492
  child.kill();
7399
7493
  return;
@@ -7438,7 +7532,7 @@ ${ui2.dim(`If nothing opens, visit: ${parsed.url}`)}`);
7438
7532
  }, RAW_OUTPUT_AFTER_MS), code = await new Promise((resolve) => {
7439
7533
  child.on("error", () => resolve(-1)), child.on("close", resolve);
7440
7534
  });
7441
- 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) {
7442
7536
  spin ? spin.stop(`${ui2.unicode ? "\u{1F513} " : ""}Logged in`) : p4.log.success("Logged in"), await rememberLogin(loginKey);
7443
7537
  return;
7444
7538
  }
@@ -7450,12 +7544,15 @@ ${ui2.dim(`If nothing opens, visit: ${parsed.url}`)}`);
7450
7544
  `) || lines.slice(-1)[0] || "";
7451
7545
  throw new ui2.CliError(`SSO login for ${label} failed.${detail ? `
7452
7546
  ${detail}` : ""}`, {
7453
- 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()}`
7454
7549
  });
7455
7550
  }, plainLogin = async (args, label) => {
7456
7551
  console.error(`Logging in to ${label}. Approve the request using the URL and code below.`), await new Promise((resolve, reject) => {
7457
- let child = spawn("aws", args, { stdio: ["ignore", process.stderr, process.stderr], ...IS_WINDOWS ? { shell: !0 } : {} });
7458
- 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
+ });
7459
7556
  });
7460
7557
  }, rememberLogin = async (key) => {
7461
7558
  if (!key)
@@ -7537,7 +7634,7 @@ ${gray(S_BAR2)}`;
7537
7634
  // src/commands/switch.js
7538
7635
  var require_switch = __commonJS({
7539
7636
  "src/commands/switch.js"(exports2, module2) {
7540
- 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 } = {}) => {
7541
7638
  p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7542
7639
  let lines = [];
7543
7640
  if (state.defaultProfile) {
@@ -7598,20 +7695,20 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7598
7695
  return result;
7599
7696
  let loginProfile = profile.kind == "role" && profile.source_profile ? state.profiles.find((x2) => x2.name == profile.source_profile) : profile;
7600
7697
  if (!(loginProfile && (loginProfile.kind == "sso" || loginProfile.kind == "login")))
7601
- 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 });
7602
7699
  if (!interactive && !force)
7603
7700
  throw new ui2.CliError(`${profile.name} needs a login.`, { code: 2, hint: `Run: switch-profile login ${loginProfile.name}` });
7604
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)
7605
- 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 });
7606
7703
  return result;
7607
7704
  }, firstLine = (text2) => String(text2 || "").trim().split(`
7608
7705
  `).filter(Boolean).slice(-1)[0], switchTo = async (profile, state, opts) => (await doSwitch(profile, state, opts), { switched: !0 }), doSwitch = async (profile, state, opts) => {
7609
7706
  let { interactive, json } = opts, identity = await connect(profile, state, { interactive, quiet: json, force: opts.force }), [defaultErrors] = await aws.setDefaultProfile(profile.name);
7610
7707
  if (defaultErrors)
7611
- throw new ui2.CliError(ui2.errorsMessage(defaultErrors));
7708
+ throw ui2.cliErrorFrom(defaultErrors);
7612
7709
  let current = await settings2.read();
7613
7710
  await settings2.update({ usage: rank.recordUsage(current.usage, profile.name) });
7614
- 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));
7615
7712
  if (json) {
7616
7713
  process.stdout.write(JSON.stringify({ profile: profile.name, account: identity.account, arn: identity.arn, region: profile.region || null, default: !0, terminal }) + `
7617
7714
  `);
@@ -7631,7 +7728,7 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7631
7728
  ${ui2.dim("set as the default profile")}`), await offerShortcut(profile);
7632
7729
  }, exportHint = (name) => process.platform == "win32" ? `$env:AWS_PROFILE = "${name}" ${ui2.dim("(PowerShell)")}
7633
7730
  set AWS_PROFILE=${name} ${ui2.dim("(CMD)")}` : `export AWS_PROFILE=${name}`, offerShortcut = async (profile) => {
7634
- 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:")}
7635
7732
  ${ui2.accent(exportHint(profile.name))}`;
7636
7733
  if (status && status.installed) {
7637
7734
  p4.outro(`${hint}
@@ -7687,7 +7784,7 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7687
7784
  return result;
7688
7785
  if (next == "throw")
7689
7786
  throw error;
7690
- 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();
7691
7788
  }
7692
7789
  }, switchCommand = async (query, opts) => {
7693
7790
  let interactive = ui2.isInteractive(opts);
@@ -7726,7 +7823,7 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7726
7823
  // src/commands/index.js
7727
7824
  var require_commands = __commonJS({
7728
7825
  "src/commands/index.js"(exports2, module2) {
7729
- 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) => {
7730
7827
  !opts.fromPicker && !opts.json && p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7731
7828
  }, begin = async (opts, { needsInput } = {}) => {
7732
7829
  let interactive = ui2.isInteractive(opts);
@@ -7745,7 +7842,7 @@ var require_commands = __commonJS({
7745
7842
  return interactive && !opts.json && intro2(opts), switchTo(findProfile(state, name), state, { ...opts, interactive });
7746
7843
  }, status = async (args, opts) => {
7747
7844
  await begin(opts);
7748
- 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 };
7749
7846
  if (opts.json) {
7750
7847
  let at = d ? (state.settings.logins || {})[loginKey(d)] : null;
7751
7848
  process.stdout.write(JSON.stringify({
@@ -7810,21 +7907,31 @@ var require_commands = __commonJS({
7810
7907
  return importFromSso(state, { ...opts, interactive });
7811
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) }));
7812
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);
7813
7914
  p4.log.message([
7814
7915
  "Handing over to aws configure sso. You will be asked for:",
7815
- `${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)")}`,
7816
7917
  `${ui2.dim("2.")} the start URL ${ui2.dim("(e.g. https://acme.awsapps.com/start)")}`,
7817
- `${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.`)] : []
7818
7920
  ].join(`
7819
7921
  `));
7820
7922
  let [errors, upgraded] = await aws.createSsoProfile(name);
7821
- if (errors)
7822
- 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
+ }
7823
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.`);
7824
7931
  } else if (kind == "login") {
7825
7932
  let region = await chooseRegion("Default region", "us-east-1"), [configErrors, configStr] = await aws.getConfigFile();
7826
7933
  if (configErrors)
7827
- throw new ui2.CliError(ui2.errorsMessage(configErrors));
7934
+ throw ui2.cliErrorFrom(configErrors);
7828
7935
  let ini = require_ini();
7829
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);
7830
7937
  } else {
@@ -7833,7 +7940,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7833
7940
  return p4.outro(ui2.dim("Nothing changed."));
7834
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 });
7835
7942
  if (errors)
7836
- throw new ui2.CliError(ui2.errorsMessage(errors));
7943
+ throw ui2.cliErrorFrom(errors);
7837
7944
  }
7838
7945
  p4.log.success(`Profile ${ui2.accent(name)} created`);
7839
7946
  let next = await loadState();
@@ -7851,7 +7958,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7851
7958
  }, importFromSso = async (state, opts) => {
7852
7959
  let { interactive } = opts, [configErrors, configStr] = await aws.getConfigFile();
7853
7960
  if (configErrors)
7854
- throw new ui2.CliError(ui2.errorsMessage(configErrors));
7961
+ throw ui2.cliErrorFrom(configErrors);
7855
7962
  let sessions = transforms.listSsoSessions(configStr), sessionName = opts.fromSso && opts.fromSso !== !0 ? opts.fromSso : null;
7856
7963
  if (sessionName && !sessions.some((s) => s.name == sessionName))
7857
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." });
@@ -7889,7 +7996,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7889
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"}`);
7890
7997
  } catch (err) {
7891
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(`
7892
- `).slice(-1)[0] });
7999
+ `).slice(-1)[0], cause: err });
7893
8000
  }
7894
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 });
7895
8002
  if (!preview.added.length && !preview.stale.length)
@@ -7902,14 +8009,25 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7902
8009
  ].filter(Boolean).join(`
7903
8010
  `);
7904
8011
  opts.json || p4.log.message(summary);
7905
- let prune = !!opts.prune;
8012
+ let prune = !!opts.prune, include;
7906
8013
  if (interactive && !opts.yes) {
7907
- if (preview.added.length && !await ask(p4.confirm({ message: `Add ${preview.added.length} profile${preview.added.length == 1 ? "" : "s"}?` })))
7908
- return p4.outro(ui2.dim("Nothing changed."));
8014
+ if (preview.added.length) {
8015
+ let all = preview.addedEntries.map((e) => transforms.ssoEntryKey(e.accountId, e.roleName)), picked = await ask(p4.autocompleteMultiselect({
8016
+ message: `Add which profiles? ${ui2.dim("(all selected: space to untick, type to search, enter to confirm)")}`,
8017
+ maxItems: 12,
8018
+ initialValues: all,
8019
+ options: preview.addedEntries.map((e, i2) => ({ value: all[i2], label: e.name, hint: [e.accountName, e.accountId, e.roleName].filter(Boolean).join(" \xB7 ") }))
8020
+ }));
8021
+ if (!picked.length && !preview.stale.length)
8022
+ return p4.outro(ui2.dim("Nothing changed."));
8023
+ picked.length < all.length && (include = picked);
8024
+ }
7909
8025
  preview.stale.length && (prune = await ask(p4.confirm({ message: `Remove the ${preview.stale.length} profile${preview.stale.length == 1 ? "" : "s"} that no longer exist${preview.stale.length == 1 ? "s" : ""}?`, initialValue: !1 })));
7910
8026
  }
8027
+ if (include && !include.length && !prune)
8028
+ return p4.outro(ui2.dim("Nothing changed."));
7911
8029
  prune && await aws.backupAwsFiles();
7912
- let result = transforms.populateSsoProfiles(configStr, { ssoSession: sessionName, entries, region, prefix, prune, version: settings2.CLI_VERSION });
8030
+ let result = transforms.populateSsoProfiles(configStr, { ssoSession: sessionName, entries, region, prefix, prune, include, version: settings2.CLI_VERSION });
7913
8031
  if (await aws.writeAwsFile(aws.AWS_CONFIG_FILE, result.config), await settings2.update({}), opts.json) {
7914
8032
  process.stdout.write(JSON.stringify({ added: result.added, existing: result.existing, removed: result.removed, stale: prune ? [] : result.stale }) + `
7915
8033
  `);
@@ -7939,7 +8057,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7939
8057
  }
7940
8058
  let [errors] = await aws.deleteProfiles(names);
7941
8059
  if (errors)
7942
- throw new ui2.CliError(ui2.errorsMessage(errors));
8060
+ throw ui2.cliErrorFrom(errors);
7943
8061
  p4.outro(`${ui2.ok(ui2.sym.ok)} Removed ${names.join(", ")}`);
7944
8062
  }, LOGIN_MODE_LABELS = {
7945
8063
  auto: "Auto: device code over SSH, browser otherwise",
@@ -7998,7 +8116,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7998
8116
  });
7999
8117
 
8000
8118
  // src/cli.js
8001
- 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();
8002
8120
  process.env.AWS_CLI_AGENT_TOOLKIT_HINT_DISABLED = "true";
8003
8121
  var EXAMPLES = `
8004
8122
  Examples:
@@ -8013,19 +8131,28 @@ Examples:
8013
8131
  sets AWS_PROFILE in the current terminal. Without it, use 'npx switch-profile'.
8014
8132
 
8015
8133
  Exit codes: 0 ok \xB7 1 error \xB7 2 login required \xB7 3 bad input \xB7 130 cancelled`, run = (action) => async (...args) => {
8016
- let opts = args[args.length - 1].optsWithGlobals();
8134
+ let cmd = args[args.length - 1], opts = cmd.optsWithGlobals();
8017
8135
  opts.debug && (process.env.SWITCH_PROFILE_DEBUG = "1");
8018
8136
  try {
8019
8137
  await action(...args.slice(0, -2), opts);
8020
8138
  } catch (err) {
8021
8139
  if (err instanceof CancelError) {
8022
- 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;
8023
8141
  return;
8024
8142
  }
8025
- 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 }) + `
8026
8144
  `) : ui.printError(err), process.exitCode = err.exitCode || 1;
8027
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);
8028
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}`));
8029
8156
  let program = new Command();
8030
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);
8031
8158
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switch-profile",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "main": "index.js",
5
5
  "bin": {
6
6
  "switch-profile": "index.js"
@@ -45,7 +45,7 @@
45
45
  "eslint": "^10.0.0",
46
46
  "fuzzysort": "^3.1.0",
47
47
  "globals": "^17.13.0",
48
- "mocha": "^8.2.1",
48
+ "mocha": "^11.8.0",
49
49
  "uqr": "^0.1.3"
50
50
  },
51
51
  "files": [