switch-profile 2.4.0 → 2.5.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 +6 -2
  2. package/dist/cli.js +281 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,7 +21,7 @@ npx switch-profile
21
21
  │ ○ globex-dev ReadOnly 333333333333 us-east-1
22
22
  │ ○ initech-dev Admin 444444444444 eu-west-1
23
23
  │
24
- │ [Log in] [Add] [Rename] [Remove] [Log out] [Settings]
24
+ │ [Log in] [Add] [Portals] [Rename] [Remove] [Log out] [Settings]
25
25
  └ ↑↓ choose · enter switch · tab actions · esc quit
26
26
  ```
27
27
 
@@ -100,8 +100,9 @@ No profiles yet? `switch-profile` starts the **Add profiles** flow. To import ev
100
100
  Running `sp` (or `npx switch-profile`) with no arguments opens the home screen:
101
101
 
102
102
  - **Type and press Enter** to switch. That's the whole path for everyday use.
103
- - **Tab** moves to the action bar below the list, which is always visible however many profiles you have: **Log in**, **Add**, **Rename**, **Remove**, **Log out**, **Settings**. Use ←→ to choose and Enter to open.
103
+ - **Tab** moves to the action bar below the list, which is always visible however many profiles you have: **Log in**, **Add**, **Portals**, **Rename**, **Remove**, **Log out**, **Settings**. Use ←→ to choose and Enter to open.
104
104
  - **Every action opens as a page and comes back** to the home screen when it's done. **Esc** goes back from a page, and quits from the home screen.
105
+ - **When something fails, it stops and tells you.** An error screen says what failed, what AWS answered, why, and which SSO portal (start URL and region) was involved. Then it asks what to do: fix the portal's region in one step, edit the portal, try again, or go back.
105
106
  - A switch ends the session, which is what lets `sp` set `AWS_PROFILE` in your terminal.
106
107
 
107
108
  ## Commands
@@ -117,6 +118,7 @@ Running `sp` (or `npx switch-profile`) with no arguments opens the home screen:
117
118
  | `add --from-sso [session]` | Imports or re-syncs every account and role of an `[sso-session]`. Options: `--region`, `--prefix`, `--prune`, `--yes`. |
118
119
  | `rename [profile] [new-name]` | Renames a profile (alias `mv`), for example an imported `acme-prod-workloads-administratoraccess` to `acme-prod`. Also updates `source_profile` references, the default profile and the home screen order. SSO logins are kept. Scripts, terminals and tools that use the old name need the new one. |
119
120
  | `remove [profiles...] [--yes]` | Removes profiles (alias `rm`). The current default cannot be removed. |
121
+ | `portals [name]` | Lists your SSO portals (`[sso-session]`) and edits one in place: start URL and SSO region (alias `portal`). Flags a portal whose region differs from the one its portal page names, and fixes it in one step. `--region` / `--start-url` edit without prompting; `--json` lists them (`--check` adds the region read from each portal). |
120
122
  | `settings` | Per-terminal switching, SSO login mode, legacy SSO upgrade. |
121
123
 
122
124
  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.
@@ -242,6 +244,8 @@ The SSO region is wrong. It must be the region of your IAM Identity Center insta
242
244
 
243
245
  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).
244
246
 
247
+ When importing from a portal (`sp add` > Accounts from an SSO portal) or switching to one of its profiles, the same mistake shows as `Invalid request` during the login. `switch-profile` reads the portal's real region from its start page and offers to fix the saved portal. You can also check and fix every portal any time with `sp portals` (or **Portals** on the home screen).
248
+
245
249
  ### Something failed and the message is not enough
246
250
 
247
251
  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.
package/dist/cli.js CHANGED
@@ -5419,7 +5419,7 @@ var require_package = __commonJS({
5419
5419
  "package.json"(exports2, module2) {
5420
5420
  module2.exports = {
5421
5421
  name: "switch-profile",
5422
- version: "2.4.0",
5422
+ version: "2.5.0",
5423
5423
  main: "index.js",
5424
5424
  bin: {
5425
5425
  "switch-profile": "index.js"
@@ -5777,9 +5777,17 @@ var require_transforms = __commonJS({
5777
5777
  ["sso_region", region],
5778
5778
  ["sso_registration_scopes", SSO_SCOPES],
5779
5779
  [VERSION_KEY, version]
5780
- ]);
5780
+ ]), updateSsoSession = (configStr, { name, startUrl, region, version }) => {
5781
+ let section = `sso-session ${name}`;
5782
+ return ini.getSection(configStr, section) ? ini.setKeys(configStr, section, {
5783
+ ...startUrl ? { sso_start_url: startUrl } : {},
5784
+ ...region ? { sso_region: region } : {},
5785
+ [VERSION_KEY]: version
5786
+ }) : configStr;
5787
+ };
5781
5788
  module2.exports = {
5782
5789
  GENERATED_KEY,
5790
+ updateSsoSession,
5783
5791
  ACCOUNT_NAME_KEY,
5784
5792
  generatedProfileName,
5785
5793
  populateSsoProfiles,
@@ -5817,13 +5825,22 @@ var require_login = __commonJS({
5817
5825
  text2 = text2 || "";
5818
5826
  let urls = text2.match(URL_REGEX) || [], code = (text2.match(CODE_REGEX) || [])[1] || null, completeUrl = urls.find((u5) => /user_code=/.test(u5)) || null;
5819
5827
  return { url: urls.find((u5) => u5 !== completeUrl) || completeUrl || null, code, completeUrl };
5820
- }, WRONG_REGION = /Invalid start url|invalid_request|InvalidRequestException/i, configureSsoFailure = (output, sessions) => {
5828
+ }, WRONG_REGION = /Invalid start url|invalid_request|InvalidRequestException/i, isWrongSsoRegion = (output) => WRONG_REGION.test(output || ""), PORTAL_REGION = /portal\.sso\.([a-z]{2}(?:-[a-z]+)+-\d+)\.amazonaws\.com/, portalRegionFromHeaders = (values) => {
5829
+ for (let v of values || []) {
5830
+ let m3 = (v || "").match(PORTAL_REGION);
5831
+ if (m3)
5832
+ return m3[1];
5833
+ }
5834
+ return null;
5835
+ }, configureSsoFailure = (output, sessions) => {
5821
5836
  let lines = (output || "").split(`
5822
5837
  `).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()}` : "."}`;
5823
- 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}` };
5838
+ return isWrongSsoRegion(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}` };
5824
5839
  };
5825
5840
  module2.exports = {
5826
5841
  configureSsoFailure,
5842
+ isWrongSsoRegion,
5843
+ portalRegionFromHeaders,
5827
5844
  isRemoteSession,
5828
5845
  resolveLoginMode,
5829
5846
  loginFlags,
@@ -5944,7 +5961,7 @@ var require_regions = __commonJS({
5944
5961
  // src/aws/index.js
5945
5962
  var require_aws = __commonJS({
5946
5963
  "src/aws/index.js"(exports2, module2) {
5947
- 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 ? `
5964
+ 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, portalRegionFromHeaders } = require_login(), regions = require_regions(), log3 = require_log(), IS_WINDOWS = process.platform === "win32", NL = IS_WINDOWS ? `
5948
5965
  ` : 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 () => {
5949
5966
  if (_awsCliVersion)
5950
5967
  return _awsCliVersion;
@@ -6047,6 +6064,18 @@ var require_aws = __commonJS({
6047
6064
  }, readSsoToken = async ({ ssoSession, startUrl }) => {
6048
6065
  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);
6049
6066
  return !token || !token.accessToken || new Date(token.expiresAt).getTime() < Date.now() + 60 * 1e3 ? null : token;
6067
+ }, detectSsoRegion = async (startUrl, { timeoutMs = 5e3 } = {}) => {
6068
+ let started = Date.now();
6069
+ try {
6070
+ let url = new URL(startUrl);
6071
+ url.hash = "";
6072
+ let res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(timeoutMs) });
6073
+ await res.body?.cancel();
6074
+ let region = portalRegionFromHeaders([res.headers.get("link"), res.headers.get("content-security-policy")]);
6075
+ return log3.info(`SSO region of ${url} \u2192 ${region || "not found"} (HTTP ${res.status} in ${Date.now() - started}ms)`), region;
6076
+ } catch (err) {
6077
+ return log3.tolerated(`SSO region lookup of ${startUrl}`, null)(err);
6078
+ }
6050
6079
  }, listSsoAccounts = async (accessToken, region) => {
6051
6080
  let data = await run2("aws", ["sso", "list-accounts", "--access-token", accessToken, "--region", region, "--output", "json"]);
6052
6081
  return (JSON.parse(data).accountList || []).map((a2) => ({ accountId: a2.accountId, accountName: a2.accountName }));
@@ -6156,6 +6185,7 @@ var require_aws = __commonJS({
6156
6185
  awsLogin,
6157
6186
  logoutAll,
6158
6187
  readSsoToken,
6188
+ detectSsoRegion,
6159
6189
  listSsoAccounts,
6160
6190
  listSsoRoles,
6161
6191
  getSsoLoginStatus,
@@ -6180,7 +6210,7 @@ var require_aws = __commonJS({
6180
6210
  // src/shell.js
6181
6211
  var require_shell = __commonJS({
6182
6212
  "src/shell.js"(exports2, module2) {
6183
- 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", "rename"], PROFILES_SED = "sed -n 's/^\\[profile \\(.*\\)\\]$/\\1/p'", POSIX_FUNCTION = (shell) => `${FUNCTION_NAME}() {
6213
+ 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", "rename", "portals"], PROFILES_SED = "sed -n 's/^\\[profile \\(.*\\)\\]$/\\1/p'", POSIX_FUNCTION = (shell) => `${FUNCTION_NAME}() {
6184
6214
  local __sp_file __sp_status
6185
6215
  __sp_file="$(mktemp "\${TMPDIR:-/tmp}/switch-profile.XXXXXX")" || return 1
6186
6216
  if [ -n "\${SWITCH_PROFILE_DEV_BIN:-}" ]; then
@@ -6496,9 +6526,15 @@ var require_common = __commonJS({
6496
6526
  return { text: null, needsLogin: !1 };
6497
6527
  let [, login] = await aws.getSsoLoginStatus(profile.sso_start_url), at = (state && state.settings.logins || {})[loginKey(profile)], since = at ? ui2.dim(`logged in ${ui2.ago(at)}`) : null, s = login ? login.state : "none";
6498
6528
  return s == "refreshable" ? { text: ui2.join([ui2.ok(`SSO ${ui2.sym.ok}`) + " auto-refresh", since]), needsLogin: !1 } : s == "active" ? { text: ui2.join([ui2.ok(`SSO ${ui2.sym.ok}`) + ` ${Math.round((login.expiresAt - Date.now()) / 6e4)} min left`, since]), needsLogin: !1 } : { text: ui2.warn(s == "expired" ? "SSO login expired" : "not logged in"), needsLogin: !0 };
6499
- };
6529
+ }, chooseRegion = async (message, initialValue) => ask(p4.autocomplete({
6530
+ message,
6531
+ maxItems: 8,
6532
+ initialValue,
6533
+ options: aws.regions.map((r2) => ({ value: r2.code, label: r2.code, hint: r2.name }))
6534
+ }));
6500
6535
  module2.exports = {
6501
6536
  CancelError: CancelError2,
6537
+ chooseRegion,
6502
6538
  ask,
6503
6539
  preflight,
6504
6540
  upgradeLegacySso,
@@ -7513,7 +7549,7 @@ var require_clipboard = __commonJS({
7513
7549
  // src/commands/login-flow.js
7514
7550
  var require_login_flow = __commonJS({
7515
7551
  "src/commands/login-flow.js"(exports2, module2) {
7516
- 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 }) => {
7552
+ var { spawn } = require("child_process"), p4 = (init_dist4(), __toCommonJS(dist_exports2)), { renderUnicodeCompact } = require_dist(), aws = require_aws(), settings2 = require_settings(), { parseLoginOutput, isRemoteSession, atLeast, isWrongSsoRegion } = 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 }) => {
7517
7553
  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);
7518
7554
  if (!interactive)
7519
7555
  return plainLogin(args, label);
@@ -7571,12 +7607,12 @@ ${ui2.dim(`If nothing opens, visit: ${parsed.url}`)}`);
7571
7607
  spin && spin.error("Login did not complete");
7572
7608
  let lines = output.trim().split(`
7573
7609
  `).map((l2) => l2.trim()).filter(Boolean), detail = lines.filter((l2) => /error|exception|denied|invalid|expired|unable/i.test(l2)).slice(-2).join(`
7574
- `) || lines.slice(-1)[0] || "";
7575
- throw new ui2.CliError(`SSO login for ${label} failed.${detail ? `
7610
+ `) || lines.slice(-1)[0] || "", wrongRegion = isWrongSsoRegion(output), error = new ui2.CliError(`SSO login for ${label} failed.${detail ? `
7576
7611
  ${detail}` : ""}`, {
7577
7612
  cause: Object.assign(new Error(`aws ${args.join(" ")} exited with code ${code}`), { output }),
7578
- 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()}`
7613
+ hint: wrongRegion ? "The SSO region is most likely wrong: it must be the region where IAM Identity Center lives, not where you deploy. Check and fix it with: sp portals" : /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()}`
7579
7614
  });
7615
+ throw error.wrongRegion = wrongRegion, error.target = target, error;
7580
7616
  }, plainLogin = async (args, label) => {
7581
7617
  console.error(`Logging in to ${label}. Approve the request using the URL and code below.`), await new Promise((resolve, reject) => {
7582
7618
  let started = Date.now(), child = spawn("aws", args, { stdio: ["ignore", process.stderr, process.stderr], ...IS_WINDOWS ? { shell: !0 } : {} });
@@ -7597,6 +7633,93 @@ ${detail}` : ""}`, {
7597
7633
  }
7598
7634
  });
7599
7635
 
7636
+ // src/commands/portals.js
7637
+ var require_portals = __commonJS({
7638
+ "src/commands/portals.js"(exports2, module2) {
7639
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), transforms = require_transforms(), settings2 = require_settings(), ui2 = require_ui(), { ask, chooseRegion } = require_common(), { ssoLogin } = require_login_flow(), validateStartUrl = (v) => /^https:\/\/\S+$/.test(v || "") ? void 0 : "Enter the https:// start URL of the portal.", validateRegion = (v) => aws.regions.some((r2) => r2.code == v) ? void 0 : `Unknown region ${v}.`, readConfig = async () => {
7640
+ let [errors, configStr] = await aws.getConfigFile();
7641
+ if (errors)
7642
+ throw ui2.cliErrorFrom(errors);
7643
+ return configStr;
7644
+ }, listPortals = async (state, { detect } = {}) => {
7645
+ let portals = transforms.listSsoSessions(await readConfig()).map((s) => ({
7646
+ name: s.name,
7647
+ startUrl: s.sso_start_url || null,
7648
+ region: s.sso_region || null,
7649
+ profiles: state.profiles.filter((x2) => x2.sso_session == s.name).map((x2) => x2.name)
7650
+ }));
7651
+ return detect && await Promise.all(portals.map(async (x2) => {
7652
+ x2.detectedRegion = x2.startUrl ? await aws.detectSsoRegion(x2.startUrl, { timeoutMs: 4e3 }) : null;
7653
+ })), portals;
7654
+ }, findPortal = async (state, name) => {
7655
+ let portal = (await listPortals(state)).find((x2) => x2.name == name);
7656
+ if (!portal)
7657
+ throw new ui2.CliError(`There is no SSO portal named ${name}.`, { code: 3, hint: "List them with: switch-profile portals" });
7658
+ return portal;
7659
+ }, regionMismatch = (region, detectedRegion) => !!(detectedRegion && region && detectedRegion != region), regionNote = ({ region, detectedRegion }) => regionMismatch(region, detectedRegion) ? ui2.join([region, ui2.warn(`${ui2.sym.warn} its portal is in ${detectedRegion}`)]) : detectedRegion ? ui2.join([region, ui2.dim("matches the portal")]) : region || ui2.warn("no region"), portalHint = (x2) => ui2.join([regionNote(x2), `${x2.profiles.length} profile${x2.profiles.length == 1 ? "" : "s"}`, x2.startUrl]), updatePortal = async (name, { startUrl, region }) => {
7660
+ let configStr = await readConfig();
7661
+ await aws.writeAwsFile(aws.AWS_CONFIG_FILE, transforms.updateSsoSession(configStr, { name, startUrl, region, version: settings2.CLI_VERSION }));
7662
+ }, editPortal = async (name, state, { offerLogin = !0, detectedRegion } = {}) => {
7663
+ let changed = !1, detected = detectedRegion;
7664
+ for (; ; ) {
7665
+ let portal = await findPortal(state, name);
7666
+ detected === void 0 && (detected = portal.startUrl ? await aws.detectSsoRegion(portal.startUrl) : null);
7667
+ let mismatch = regionMismatch(portal.region, detected);
7668
+ p4.log.info([
7669
+ ui2.strong(`Portal ${name}`),
7670
+ `${ui2.dim("Start URL ")} ${portal.startUrl || ui2.warn("missing")}`,
7671
+ `${ui2.dim("SSO region")} ${regionNote({ region: portal.region, detectedRegion: detected })}`,
7672
+ detected ? null : ui2.dim(" (could not read the region from the portal page)"),
7673
+ `${ui2.dim("Profiles ")} ${portal.profiles.length ? `${portal.profiles.slice(0, 4).join(", ")}${portal.profiles.length > 4 ? `, +${portal.profiles.length - 4} more` : ""}` : "none"}`
7674
+ ].filter(Boolean).join(`
7675
+ `));
7676
+ let choice = await ask(p4.select({
7677
+ message: `Edit ${name}`,
7678
+ options: [
7679
+ ...mismatch ? [{ value: "fix", label: `Change the SSO region to ${detected}`, hint: "what the portal page says" }] : [],
7680
+ { value: "url", label: "Start URL", hint: portal.startUrl || void 0 },
7681
+ { value: "region", label: "SSO region", hint: portal.region || void 0 },
7682
+ { value: "done", label: changed ? "Done" : "Back" }
7683
+ ]
7684
+ }));
7685
+ if (choice == "done")
7686
+ break;
7687
+ if (choice == "fix")
7688
+ await updatePortal(name, { region: detected }), p4.log.success(`${name} now uses the SSO region ${ui2.accent(detected)}`);
7689
+ else if (choice == "url") {
7690
+ let startUrl = (await ask(p4.text({ message: `Start URL of ${name}`, initialValue: portal.startUrl || "", validate: validateStartUrl }))).trim();
7691
+ if (startUrl == portal.startUrl)
7692
+ continue;
7693
+ await updatePortal(name, { startUrl }), detected = void 0, p4.log.success(`${name} now uses ${ui2.accent(startUrl)}`);
7694
+ } else {
7695
+ let region = await chooseRegion(`SSO region of ${name} ${ui2.dim("(where IAM Identity Center lives)")}`, detected || portal.region);
7696
+ if (region == portal.region)
7697
+ continue;
7698
+ await updatePortal(name, { region }), p4.log.success(`${name} now uses the SSO region ${ui2.accent(region)}`);
7699
+ }
7700
+ changed = !0;
7701
+ }
7702
+ return changed && offerLogin && await ask(p4.confirm({ message: `Log in to ${name} now to check it works?` })) && await ssoLogin({ ssoSession: name }, { label: name, loginKey: name, interactive: !0 }), { changed };
7703
+ }, fixSsoRegion = async (session, loginError) => {
7704
+ let current = session.sso_region, detected = await aws.detectSsoRegion(session.sso_start_url), region = null;
7705
+ if (detected && detected != current ? (p4.log.warn(`${session.name} is set to the SSO region ${current}, but its portal is in ${detected}.`), await ask(p4.confirm({ message: `Change ${session.name} to ${detected} and log in again?` })) && (region = detected)) : p4.log.warn(`AWS rejected the SSO region of ${session.name} (${current}). Pick the region where IAM Identity Center lives (Identity Center console \u203A Settings).`), region || (region = await chooseRegion(`SSO region of ${session.name} ${ui2.dim("(where IAM Identity Center lives)")}`, detected || current)), region == current)
7706
+ throw loginError;
7707
+ return await updatePortal(session.name, { region }), region;
7708
+ };
7709
+ module2.exports = {
7710
+ validateStartUrl,
7711
+ validateRegion,
7712
+ listPortals,
7713
+ findPortal,
7714
+ regionMismatch,
7715
+ portalHint,
7716
+ updatePortal,
7717
+ editPortal,
7718
+ fixSsoRegion
7719
+ };
7720
+ }
7721
+ });
7722
+
7600
7723
  // src/home.js
7601
7724
  var require_home = __commonJS({
7602
7725
  "src/home.js"(exports2, module2) {
@@ -7661,10 +7784,63 @@ ${gray(S_BAR2)}`;
7661
7784
  }
7662
7785
  });
7663
7786
 
7787
+ // src/commands/recovery.js
7788
+ var require_recovery = __commonJS({
7789
+ "src/commands/recovery.js"(exports2, module2) {
7790
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), transforms = require_transforms(), ui2 = require_ui(), log3 = require_log(), { CancelError: CancelError2 } = require_common(), portals = require_portals(), portalOfError = (err, profiles, sessions) => {
7791
+ let target = err && err.target || {}, name = target.ssoSession || target.profile && ((profiles || []).find((x2) => x2.name == target.profile) || {}).sso_session, session = name && (sessions || []).find((s) => s.name == name);
7792
+ return session ? { name: session.name, startUrl: session.sso_start_url || null, region: session.sso_region || null } : null;
7793
+ }, recoveryOptions = ({ portal, wrongRegion, detectedRegion }) => [
7794
+ portal && wrongRegion && portals.regionMismatch(portal.region, detectedRegion) ? { value: "fix-region", label: `Change the SSO region of ${portal.name} to ${detectedRegion}`, hint: `currently ${portal.region}` } : null,
7795
+ portal ? { value: "edit-portal", label: `Edit the portal ${portal.name}`, hint: "start URL and SSO region" } : null,
7796
+ { value: "retry", label: "Try again" },
7797
+ { value: "back", label: "Back to the home screen" }
7798
+ ].filter(Boolean), errorLines = (err, { portal, detectedRegion }) => [
7799
+ ui2.strong(err.message || String(err)),
7800
+ err.hint ? `
7801
+ ${err.hint}` : null,
7802
+ portal ? `
7803
+ ${ui2.dim("Portal ")} ${portal.name} ${ui2.dim(portal.startUrl || "")}` : null,
7804
+ portal ? `${ui2.dim("SSO region")} ${portal.region || "?"}${portals.regionMismatch(portal.region, detectedRegion) ? ` ${ui2.warn(`${ui2.sym.warn} its portal page says ${detectedRegion}`)}` : detectedRegion ? ` ${ui2.dim("(matches its portal page)")}` : ""}` : null,
7805
+ `
7806
+ ${ui2.dim(`Details: ${log3.displayPath()}`)}`
7807
+ ].filter((v) => v !== null).join(`
7808
+ `), recover = async (err, state, what) => {
7809
+ log3.error(`${what} failed (error screen)`, err);
7810
+ let [, configStr] = await aws.getConfigFile(), portal = portalOfError(err, state.profiles, transforms.listSsoSessions(configStr || "")), detectedRegion = portal && err.wrongRegion && portal.startUrl ? await aws.detectSsoRegion(portal.startUrl) : null;
7811
+ p4.log.error(errorLines(err, { portal, detectedRegion }));
7812
+ let choice = await p4.select({ message: "What now?", options: recoveryOptions({ portal, wrongRegion: err.wrongRegion, detectedRegion }) });
7813
+ if (p4.isCancel(choice) || choice == "back")
7814
+ return "back";
7815
+ try {
7816
+ if (choice == "fix-region")
7817
+ await portals.updatePortal(portal.name, { region: detectedRegion }), p4.log.success(`${portal.name} now uses the SSO region ${ui2.accent(detectedRegion)}. Trying again\u2026`);
7818
+ else if (choice == "edit-portal") {
7819
+ let { changed } = await portals.editPortal(portal.name, state, { offerLogin: !1, detectedRegion: err.wrongRegion ? detectedRegion : void 0 });
7820
+ if (!changed)
7821
+ return "back";
7822
+ }
7823
+ } catch (e) {
7824
+ if (e instanceof CancelError2)
7825
+ return "back";
7826
+ if (!(e instanceof ui2.CliError))
7827
+ throw e;
7828
+ return log3.error("fixing the portal failed", e), ui2.printError(e), "back";
7829
+ }
7830
+ return "retry";
7831
+ };
7832
+ module2.exports = {
7833
+ portalOfError,
7834
+ recoveryOptions,
7835
+ recover
7836
+ };
7837
+ }
7838
+ });
7839
+
7664
7840
  // src/commands/switch.js
7665
7841
  var require_switch = __commonJS({
7666
7842
  "src/commands/switch.js"(exports2, module2) {
7667
- 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 } = {}) => {
7843
+ 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(), { recover } = require_recovery(), header = async (state, { warnings } = {}) => {
7668
7844
  p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7669
7845
  let lines = [];
7670
7846
  if (state.defaultProfile) {
@@ -7690,6 +7866,7 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7690
7866
  }, ACTIONS = [
7691
7867
  { key: "login", label: "Log in" },
7692
7868
  { key: "add", label: "Add" },
7869
+ { key: "portals", label: "Portals" },
7693
7870
  { key: "rename", label: "Rename" },
7694
7871
  { key: "remove", label: "Remove" },
7695
7872
  { key: "logout", label: "Log out" },
@@ -7787,35 +7964,51 @@ ${ui2.dim("Tip: enable per-terminal switching in Settings.")}` : ""}`);
7787
7964
  ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name == "powershell" && p4.log.message(ui2.dim("If PowerShell refuses to load your profile: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned"));
7788
7965
  }, runAction = async (key, state, opts) => {
7789
7966
  let commands2 = require_commands();
7790
- return key == "login" ? commands2.login(state.defaultName, { ...opts, fromPicker: !0 }) : key == "rename" ? commands2.rename(null, null, { ...opts, fromPicker: !0 }) : commands2[key](opts.args || [], { ...opts, fromPicker: !0 });
7967
+ return key == "login" ? commands2.login(state.defaultName, { ...opts, fromPicker: !0 }) : key == "rename" ? commands2.rename(null, null, { ...opts, fromPicker: !0 }) : key == "portals" ? commands2.portals(null, { ...opts, fromPicker: !0 }) : commands2[key](opts.args || [], { ...opts, fromPicker: !0 });
7791
7968
  }, routeChoice = (choice) => {
7792
7969
  if (p4.isCancel(choice))
7793
7970
  return { type: "quit" };
7794
7971
  let value = String(choice);
7795
7972
  return value.startsWith(ACTION_PREFIX) ? { type: "page", key: value.slice(ACTION_PREFIX.length) } : { type: "switch", name: value };
7796
- }, afterPage = (result, err) => err ? err instanceof CancelError2 ? "back" : err instanceof ui2.CliError ? "error" : "throw" : result && result.switched ? "exit" : "home", homeLoop = async (state, opts, initialQuery) => {
7797
- let first = !0;
7973
+ }, afterPage = (result, err) => err ? err instanceof CancelError2 ? "back" : err instanceof ui2.CliError ? "error" : "throw" : result && result.switched ? "exit" : "home", findOrFail = (state, name) => {
7974
+ let profile = state.profiles.find((x2) => x2.name == name);
7975
+ if (!profile)
7976
+ throw new ui2.CliError(`There is no profile named ${name} any more.`, { code: 3 });
7977
+ return profile;
7978
+ }, withRecovery = async (run2, state, what, { exitOnCancel } = {}) => {
7798
7979
  for (; ; ) {
7799
- await header(state, { warnings: first });
7800
- let route = routeChoice(await pick(state, first ? initialQuery : void 0));
7801
- if (first = !1, route.type == "quit") {
7802
- p4.outro(ui2.dim(`Bye ${ui2.unicode ? "\u{1F44B}" : ""}`));
7803
- return;
7804
- }
7805
- if (route.type == "switch")
7806
- return switchTo(state.profiles.find((x2) => x2.name == route.name), state, opts);
7807
7980
  let result, error;
7808
7981
  try {
7809
- result = await runAction(route.key, state, opts);
7982
+ result = await run2(state);
7810
7983
  } catch (err) {
7811
7984
  error = err;
7812
7985
  }
7813
7986
  let next = afterPage(result, error);
7814
- if (next == "exit")
7987
+ if (next == "exit" || next == "home")
7815
7988
  return result;
7816
- if (next == "throw")
7989
+ if (next == "throw" || next == "back" && exitOnCancel)
7817
7990
  throw error;
7818
- 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();
7991
+ if (next == "back") {
7992
+ p4.cancel(ui2.dim(`${ui2.unicode ? "\u2190" : "<-"} Back`));
7993
+ return;
7994
+ }
7995
+ if (state = await loadState(), await recover(error, state, what) != "retry")
7996
+ return;
7997
+ state = await loadState();
7998
+ }
7999
+ }, homeLoop = async (state, opts, initialQuery) => {
8000
+ let first = !0;
8001
+ for (; ; ) {
8002
+ await header(state, { warnings: first });
8003
+ let route = routeChoice(await pick(state, first ? initialQuery : void 0));
8004
+ if (first = !1, route.type == "quit") {
8005
+ p4.outro(ui2.dim(`Bye ${ui2.unicode ? "\u{1F44B}" : ""}`));
8006
+ return;
8007
+ }
8008
+ let result = route.type == "switch" ? await withRecovery((s) => switchTo(findOrFail(s, route.name), s, opts), state, `switch to ${route.name}`, { exitOnCancel: !0 }) : await withRecovery((s) => runAction(route.key, s, opts), state, route.key);
8009
+ if (afterPage(result) == "exit")
8010
+ return result;
8011
+ state = await loadState();
7819
8012
  }
7820
8013
  }, switchCommand = async (query, opts) => {
7821
8014
  let interactive = ui2.isInteractive(opts);
@@ -7828,8 +8021,13 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7828
8021
  }
7829
8022
  if (query) {
7830
8023
  let { profile, candidates } = rank.resolveQuery(state.profiles, query, { usage: state.settings.usage, current: state.defaultName });
7831
- if (profile)
7832
- return !opts.json && interactive && p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`), switchTo(profile, state, { ...opts, interactive });
8024
+ if (profile) {
8025
+ if (!interactive || opts.json)
8026
+ return switchTo(profile, state, { ...opts, interactive });
8027
+ p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
8028
+ let result = await withRecovery((s) => switchTo(findOrFail(s, profile.name), s, { ...opts, interactive }), state, `switch to ${profile.name}`, { exitOnCancel: !0 });
8029
+ return result || homeLoop(await loadState(), { ...opts, interactive });
8030
+ }
7833
8031
  if (!interactive)
7834
8032
  throw new ui2.CliError(candidates.length ? `"${query}" matches ${candidates.length} profiles.` : `No profile matches "${query}".`, {
7835
8033
  code: 3,
@@ -7854,7 +8052,7 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7854
8052
  // src/commands/index.js
7855
8053
  var require_commands = __commonJS({
7856
8054
  "src/commands/index.js"(exports2, module2) {
7857
- 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, exportHint } = require_switch(), intro2 = (opts) => {
8055
+ 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, chooseRegion } = require_common(), { ssoLogin, rememberLogin } = require_login_flow(), portals = require_portals(), { switchCommand, switchTo, connect, enableShortcut, exportHint } = require_switch(), intro2 = (opts) => {
7858
8056
  !opts.fromPicker && !opts.json && p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7859
8057
  }, begin = async (opts, { needsInput } = {}) => {
7860
8058
  let interactive = ui2.isInteractive(opts);
@@ -7909,12 +8107,7 @@ var require_commands = __commonJS({
7909
8107
  return p4.outro(ui2.dim("Nothing changed."));
7910
8108
  }
7911
8109
  await aws.logoutAll(), await settings2.update({ logins: {} }), p4.outro(`${ui2.ok(ui2.sym.ok)} Logged out ${ui2.unicode ? "\u{1F44B}" : ""}${isRemoteSession(process.env, process.platform) ? ui2.dim(" This machine no longer has AWS access.") : ""}`);
7912
- }, chooseRegion = async (message, initialValue) => ask(p4.autocomplete({
7913
- message,
7914
- maxItems: 8,
7915
- initialValue,
7916
- options: aws.regions.map((r2) => ({ value: r2.code, label: r2.code, hint: r2.name }))
7917
- })), validateProfileName = (taken) => (value) => {
8110
+ }, validateProfileName = (taken) => (value) => {
7918
8111
  if (!value)
7919
8112
  return "A name is required.";
7920
8113
  if (!/^[a-z0-9][a-z0-9_-]+$/.test(value))
@@ -8004,7 +8197,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
8004
8197
  ]
8005
8198
  })) : "__new__";
8006
8199
  if (choice == "__new__") {
8007
- let startUrl = await ask(p4.text({ message: "SSO start URL", placeholder: "https://acme.awsapps.com/start", validate: (v) => /^https:\/\/\S+$/.test(v || "") ? void 0 : "Enter the https:// start URL of the portal." })), region2 = await chooseRegion(`SSO region ${ui2.dim("(where IAM Identity Center lives)")}`, "us-east-1"), suggested = transforms.sessionNameFromUrl(startUrl).replace(/[^a-zA-Z0-9_-]/g, "-");
8200
+ let startUrl = (await ask(p4.text({ message: "SSO start URL", placeholder: "https://acme.awsapps.com/start", validate: portals.validateStartUrl }))).trim(), detected = await aws.detectSsoRegion(startUrl), region2 = await chooseRegion(`SSO region ${ui2.dim(detected ? `(where IAM Identity Center lives: ${detected}, read from the portal)` : "(where IAM Identity Center lives)")}`, detected || "us-east-1"), suggested = transforms.sessionNameFromUrl(startUrl).replace(/[^a-zA-Z0-9_-]/g, "-");
8008
8201
  sessionName = await ask(p4.text({ message: `Short name for this portal ${ui2.dim("(usually the client)")}`, initialValue: suggested, validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v || "") ? sessions.some((s) => s.name == v) ? `${v} already exists.` : void 0 : "Letters, numbers, - and _ only." })), configStr = transforms.addSsoSession(configStr, { name: sessionName, startUrl, region: region2, version: settings2.CLI_VERSION }), await aws.writeAwsFile(aws.AWS_CONFIG_FILE, configStr);
8009
8202
  } else
8010
8203
  sessionName = choice;
@@ -8013,7 +8206,17 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
8013
8206
  if (!token) {
8014
8207
  if (!interactive)
8015
8208
  throw new ui2.CliError(`Not logged in to ${sessionName}.`, { code: 2, hint: `Run: aws sso login --sso-session ${sessionName}` });
8016
- if (await ssoLogin({ ssoSession: sessionName }, { label: sessionName, loginKey: sessionName, interactive }), token = await aws.readSsoToken({ ssoSession: sessionName }), !token)
8209
+ for (; ; )
8210
+ try {
8211
+ await ssoLogin({ ssoSession: sessionName }, { label: sessionName, loginKey: sessionName, interactive });
8212
+ break;
8213
+ } catch (err) {
8214
+ if (!err.wrongRegion)
8215
+ throw err;
8216
+ let region2 = await portals.fixSsoRegion(session, err);
8217
+ configStr = (await aws.getConfigFile())[1] || configStr, session = { ...session, sso_region: region2 };
8218
+ }
8219
+ if (token = await aws.readSsoToken({ ssoSession: sessionName }), !token)
8017
8220
  throw new ui2.CliError(`Logged in, but no token was found for ${sessionName}.`);
8018
8221
  }
8019
8222
  let spin = opts.json ? null : ui2.spinner();
@@ -8137,6 +8340,43 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
8137
8340
  p4.log.success([`Renamed ${ui2.accent(from)} to ${ui2.accent(to)}`, ...lines].join(`
8138
8341
  `)), result.processes.length && p4.log.warn(`The credential_process of ${result.processes.join(", ")} still uses --profile ${from}. Update ${result.processes.length == 1 ? "it" : "them"} in ~/.aws/config.`), inTerminal && !terminal && p4.log.warn(`This terminal still uses ${from}. Run:
8139
8342
  ${ui2.accent(exportHint(to))}`), p4.outro(ui2.dim(`Other terminals, scripts and tools that use ${from} (AWS_PROFILE or --profile) need the new name.`));
8343
+ }, portalsCommand = async (name, opts) => {
8344
+ let interactive = await begin(opts), state = await loadState();
8345
+ if (opts.region || opts.startUrl) {
8346
+ if (!name)
8347
+ throw new ui2.CliError("Which portal?", { code: 3, hint: "Run: switch-profile portals <name> --region <region>" });
8348
+ let invalid = opts.region && portals.validateRegion(opts.region) || opts.startUrl && portals.validateStartUrl(opts.startUrl);
8349
+ if (invalid)
8350
+ throw new ui2.CliError(invalid, { code: 3 });
8351
+ await portals.findPortal(state, name), await portals.updatePortal(name, { region: opts.region, startUrl: opts.startUrl });
8352
+ let portal = await portals.findPortal(state, name);
8353
+ return opts.json ? void process.stdout.write(JSON.stringify(portal) + `
8354
+ `) : (p4.log.success(`Saved ${ui2.accent(name)}: ${ui2.join([portal.startUrl, portal.region])}`), p4.outro(ui2.dim(`Log in again: switch-profile login <a ${name} profile>`)));
8355
+ }
8356
+ if (!interactive || opts.json) {
8357
+ let list = await portals.listPortals(state, { detect: !!opts.check }), selected = name ? list.filter((x2) => x2.name == name) : list;
8358
+ if (name && !selected.length && await portals.findPortal(state, name), opts.json)
8359
+ return void process.stdout.write(JSON.stringify({ portals: selected }) + `
8360
+ `);
8361
+ for (let x2 of selected)
8362
+ console.log(`${x2.name} ${x2.region || "-"} ${x2.startUrl || "-"} ${x2.profiles.length} profiles${opts.check ? ` portal says ${x2.detectedRegion || "?"}` : ""}`);
8363
+ return;
8364
+ }
8365
+ if (!name) {
8366
+ let spin = ui2.spinner();
8367
+ spin.start("Reading the portals");
8368
+ let list = await portals.listPortals(state, { detect: !0 });
8369
+ if (spin.stop(`${list.length} SSO portal${list.length == 1 ? "" : "s"}`), !list.length)
8370
+ return p4.outro(ui2.dim("No SSO portals yet. Add one with Add \u203A Accounts from an SSO portal."));
8371
+ let picked = await ask(p4.select({
8372
+ message: "Which portal?",
8373
+ options: list.map((x2) => ({ value: x2.name, label: x2.name, hint: portals.portalHint(x2) }))
8374
+ })), { changed: changed2 } = await portals.editPortal(picked, state, { detectedRegion: list.find((x2) => x2.name == picked).detectedRegion });
8375
+ return p4.outro(ui2.dim(changed2 ? "Saved." : "Nothing changed."));
8376
+ }
8377
+ await portals.findPortal(state, name);
8378
+ let { changed } = await portals.editPortal(name, state);
8379
+ p4.outro(ui2.dim(changed ? "Saved." : "Nothing changed."));
8140
8380
  }, LOGIN_MODE_LABELS = {
8141
8381
  auto: "Auto: device code over SSH, browser otherwise",
8142
8382
  device: "Always device code (approve from any device)",
@@ -8189,6 +8429,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
8189
8429
  add,
8190
8430
  remove,
8191
8431
  rename,
8432
+ portals: portalsCommand,
8192
8433
  settings: settingsCommand
8193
8434
  };
8194
8435
  }
@@ -8205,6 +8446,7 @@ Examples:
8205
8446
  $ sp login log in again now (device code over SSH)
8206
8447
  $ sp add import every account of an SSO portal
8207
8448
  $ sp rename <profile> <new> give a profile a friendlier name
8449
+ $ sp portals check or fix an SSO portal's start URL and region
8208
8450
  $ switch-profile status --json
8209
8451
 
8210
8452
  'sp' is the shortcut switch-profile installs on first use (or from Settings). It also
@@ -8234,6 +8476,6 @@ Exit codes: 0 ok \xB7 1 error \xB7 2 login required \xB7 3 bad input \xB7 130 ca
8234
8476
  }, main = (argv) => {
8235
8477
  log2.start({ version: CLI_VERSION, argv }), process.on("uncaughtException", crashed), process.on("unhandledRejection", crashed), process.on("exit", (code) => log2.info(`exit ${code}`));
8236
8478
  let program = new Command();
8237
- 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("rename").alias("mv").argument("[profile]").argument("[new-name]").description("rename a profile").action(run(commands.rename)), 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);
8479
+ 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("rename").alias("mv").argument("[profile]").argument("[new-name]").description("rename a profile").action(run(commands.rename)), program.command("portals").alias("portal").argument("[name]", "the [sso-session] to edit").description("list SSO portals, check their region, and edit the start URL or region").option("--region <region>", "set the SSO region (where IAM Identity Center lives)").option("--start-url <url>", "set the start URL").option("--check", "with --json or no terminal: also read each portal's region from its start page").action(run(commands.portals)), 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);
8238
8480
  };
8239
8481
  module.exports = { main };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switch-profile",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "main": "index.js",
5
5
  "bin": {
6
6
  "switch-profile": "index.js"