switch-profile 2.3.1 → 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 +8 -3
  2. package/dist/cli.js +364 -43
  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] [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**, **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
@@ -115,7 +116,9 @@ Running `sp` (or `npx switch-profile`) with no arguments opens the home screen:
115
116
  | `logout [--yes]` | Ends every SSO session on this machine (`aws sso logout`, plus `aws logout --all` on AWS CLI 2.32+). |
116
117
  | `add` | Adds profiles: import accounts from an SSO portal (recommended), a single SSO profile, a console sign-in profile, or access keys. |
117
118
  | `add --from-sso [session]` | Imports or re-syncs every account and role of an `[sso-session]`. Options: `--region`, `--prefix`, `--prune`, `--yes`. |
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. |
118
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). |
119
122
  | `settings` | Per-terminal switching, SSO login mode, legacy SSO upgrade. |
120
123
 
121
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.
@@ -155,7 +158,7 @@ sp add # then choose "Accounts from an SSO portal"
155
158
 
156
159
  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.
157
160
  2. Log in if needed.
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`.
161
+ 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`. Rename any of them later with `sp rename` (re-imports still recognize them).
159
162
  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).
160
163
 
161
164
  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.
@@ -241,6 +244,8 @@ The SSO region is wrong. It must be the region of your IAM Identity Center insta
241
244
 
242
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).
243
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
+
244
249
  ### Something failed and the message is not enough
245
250
 
246
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.3.1",
5422
+ version: "2.5.0",
5423
5423
  main: "index.js",
5424
5424
  bin: {
5425
5425
  "switch-profile": "index.js"
@@ -5602,6 +5602,9 @@ var require_ini = __commonJS({
5602
5602
  return str;
5603
5603
  let newline = detectNewline(str);
5604
5604
  return tidy(stringify(blocks.filter((b3) => b3.name !== name), newline), newline);
5605
+ }, renameSection = (str, name, newName) => {
5606
+ let blocks = parse(str), block2 = findBlock(blocks, name);
5607
+ return block2 ? (block2.lines[0] = `[${newName}]`, stringify(blocks, detectNewline(str))) : str;
5605
5608
  }, setKeys = (str, name, changes) => {
5606
5609
  let blocks = parse(str), block2 = findBlock(blocks, name);
5607
5610
  if (!block2)
@@ -5636,6 +5639,7 @@ var require_ini = __commonJS({
5636
5639
  getSection,
5637
5640
  setSection,
5638
5641
  removeSection,
5642
+ renameSection,
5639
5643
  setKeys
5640
5644
  };
5641
5645
  }
@@ -5755,14 +5759,35 @@ var require_transforms = __commonJS({
5755
5759
  for (let name of stale)
5756
5760
  config = ini.removeSection(config, configSectionName(config, name)), removed.push(name);
5757
5761
  return { config, added, addedEntries, existing, stale, removed };
5762
+ }, escapeRegex = (text2) => text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), renameProfile = (configStr, credsStr, from, to) => {
5763
+ let section = from == "default" ? null : configSectionName(configStr, from);
5764
+ if (!section)
5765
+ throw new Error(`Profile ${from} not found in ~/.aws/config.`);
5766
+ if (to == "default" || configSectionName(configStr, to) || ini.getSection(credsStr, to))
5767
+ throw new Error(`Profile ${to} already exists.`);
5768
+ let config = ini.renameSection(configStr, section, `profile ${to}`), creds = ini.renameSection(credsStr, from, to), sources = [], processes = [], mentions = new RegExp(`--profile[\\s=]+["']?${escapeRegex(from)}(?=["'\\s]|$)`);
5769
+ for (let s of ini.listSections(config).filter((s2) => s2 != "default" && !s2.startsWith("sso-session "))) {
5770
+ let params = ini.getSection(config, s), name = s.replace(/^profile\s+/, "");
5771
+ params.source_profile == from && (config = ini.setKeys(config, s, { source_profile: to }), sources.push(name)), params.credential_process && mentions.test(params.credential_process) && processes.push(name);
5772
+ }
5773
+ let defaults = ini.getSection(config, "default") || {}, wasDefault = defaults[NAME_KEY] == from;
5774
+ return wasDefault && (config = ini.setKeys(config, "default", { [NAME_KEY]: to, ...defaults.source_profile == from ? { source_profile: to } : {} })), { config, creds, sources, wasDefault, processes };
5758
5775
  }, 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}`, [
5759
5776
  ["sso_start_url", startUrl],
5760
5777
  ["sso_region", region],
5761
5778
  ["sso_registration_scopes", SSO_SCOPES],
5762
5779
  [VERSION_KEY, version]
5763
- ]);
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
+ };
5764
5788
  module2.exports = {
5765
5789
  GENERATED_KEY,
5790
+ updateSsoSession,
5766
5791
  ACCOUNT_NAME_KEY,
5767
5792
  generatedProfileName,
5768
5793
  populateSsoProfiles,
@@ -5781,7 +5806,8 @@ var require_transforms = __commonJS({
5781
5806
  sessionNameFromUrl,
5782
5807
  upgradeLegacySsoProfiles,
5783
5808
  stampProfile,
5784
- listProfiles
5809
+ listProfiles,
5810
+ renameProfile
5785
5811
  };
5786
5812
  }
5787
5813
  });
@@ -5799,13 +5825,22 @@ var require_login = __commonJS({
5799
5825
  text2 = text2 || "";
5800
5826
  let urls = text2.match(URL_REGEX) || [], code = (text2.match(CODE_REGEX) || [])[1] || null, completeUrl = urls.find((u5) => /user_code=/.test(u5)) || null;
5801
5827
  return { url: urls.find((u5) => u5 !== completeUrl) || completeUrl || null, code, completeUrl };
5802
- }, 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) => {
5803
5836
  let lines = (output || "").split(`
5804
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()}` : "."}`;
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}` };
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}` };
5806
5839
  };
5807
5840
  module2.exports = {
5808
5841
  configureSsoFailure,
5842
+ isWrongSsoRegion,
5843
+ portalRegionFromHeaders,
5809
5844
  isRemoteSession,
5810
5845
  resolveLoginMode,
5811
5846
  loginFlags,
@@ -5926,7 +5961,7 @@ var require_regions = __commonJS({
5926
5961
  // src/aws/index.js
5927
5962
  var require_aws = __commonJS({
5928
5963
  "src/aws/index.js"(exports2, module2) {
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 ? `
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 ? `
5930
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 () => {
5931
5966
  if (_awsCliVersion)
5932
5967
  return _awsCliVersion;
@@ -6029,6 +6064,18 @@ var require_aws = __commonJS({
6029
6064
  }, readSsoToken = async ({ ssoSession, startUrl }) => {
6030
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);
6031
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
+ }
6032
6079
  }, listSsoAccounts = async (accessToken, region) => {
6033
6080
  let data = await run2("aws", ["sso", "list-accounts", "--access-token", accessToken, "--region", region, "--output", "json"]);
6034
6081
  return (JSON.parse(data).accountList || []).map((a2) => ({ accountId: a2.accountId, accountName: a2.accountName }));
@@ -6078,6 +6125,16 @@ var require_aws = __commonJS({
6078
6125
  configStr = deleteProfileFromConfig(profile, configStr), credsStr = deleteProfileFromCreds(profile, credsStr);
6079
6126
  }
6080
6127
  updateConfig && await writeAwsFile(AWS_CONFIG_FILE, configStr), updateCreds && await writeAwsFile(AWS_CREDS_FILE, credsStr);
6128
+ })()), renameProfile = (from, to) => catchErrors((async () => {
6129
+ let errMsg = `Fail to rename profile ${from} to ${to}`, [configStrErrors, configStr] = await getConfigFile(), [credsStrErrors, credsStr] = await getCredsFile();
6130
+ if (configStrErrors || credsStrErrors)
6131
+ throw wrapErrors(errMsg, configStrErrors || credsStrErrors);
6132
+ try {
6133
+ let { config, creds, sources, wasDefault, processes } = transforms.renameProfile(configStr, credsStr, from, to);
6134
+ return await writeAwsFiles({ config, creds }), { sources, wasDefault, processes };
6135
+ } catch (err) {
6136
+ throw wrapErrors(errMsg, [err]);
6137
+ }
6081
6138
  })()), createProfile = ({ name, aws_access_key_id, aws_secret_access_key, region }) => catchErrors((async () => {
6082
6139
  await awsCliV2Exists();
6083
6140
  let errMsg = "Fail to create AWS profile";
@@ -6128,6 +6185,7 @@ var require_aws = __commonJS({
6128
6185
  awsLogin,
6129
6186
  logoutAll,
6130
6187
  readSsoToken,
6188
+ detectSsoRegion,
6131
6189
  listSsoAccounts,
6132
6190
  listSsoRoles,
6133
6191
  getSsoLoginStatus,
@@ -6135,6 +6193,7 @@ var require_aws = __commonJS({
6135
6193
  getDefaultProfile,
6136
6194
  setDefaultProfile,
6137
6195
  deleteProfiles,
6196
+ renameProfile,
6138
6197
  createProfile,
6139
6198
  createSsoProfile,
6140
6199
  backupAwsFiles,
@@ -6151,7 +6210,7 @@ var require_aws = __commonJS({
6151
6210
  // src/shell.js
6152
6211
  var require_shell = __commonJS({
6153
6212
  "src/shell.js"(exports2, module2) {
6154
- 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}() {
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}() {
6155
6214
  local __sp_file __sp_status
6156
6215
  __sp_file="$(mktemp "\${TMPDIR:-/tmp}/switch-profile.XXXXXX")" || return 1
6157
6216
  if [ -n "\${SWITCH_PROFILE_DEV_BIN:-}" ]; then
@@ -6171,7 +6230,7 @@ var require_shell = __commonJS({
6171
6230
  items=(\${(f)"$(${PROFILES_SED} "\${AWS_CONFIG_FILE:-$HOME/.aws/config}" 2>/dev/null)"})
6172
6231
  if (( CURRENT == 2 )); then
6173
6232
  compadd -- $items ${SUBCOMMANDS.join(" ")}
6174
- elif [[ $words[2] == (remove|rm) ]] || { (( CURRENT == 3 )) && [[ $words[2] == (use|login) ]] }; then
6233
+ elif [[ $words[2] == (remove|rm) ]] || { (( CURRENT == 3 )) && [[ $words[2] == (use|login|rename|mv) ]] }; then
6175
6234
  compadd -- $items
6176
6235
  fi
6177
6236
  }
@@ -6182,7 +6241,7 @@ fi`, BASH_COMPLETION = `_switch_profile_complete() {
6182
6241
  items="$(${PROFILES_SED} "\${AWS_CONFIG_FILE:-$HOME/.aws/config}" 2>/dev/null)"
6183
6242
  if [ "$COMP_CWORD" -eq 1 ]; then
6184
6243
  items="$items ${SUBCOMMANDS.join(" ")}"
6185
- elif [ "$COMP_CWORD" -lt 2 ] || ! case "\${COMP_WORDS[1]}" in use|login|remove|rm) true ;; *) false ;; esac; then
6244
+ elif [ "$COMP_CWORD" -lt 2 ] || ! case "\${COMP_WORDS[1]}" in use|login|rename|mv|remove|rm) true ;; *) false ;; esac; then
6186
6245
  return 0
6187
6246
  elif [ "$COMP_CWORD" -gt 2 ] && [ "\${COMP_WORDS[1]}" != remove ] && [ "\${COMP_WORDS[1]}" != rm ]; then
6188
6247
  return 0
@@ -6211,7 +6270,7 @@ function __switch_profile_profiles
6211
6270
  end
6212
6271
  for __sp_cmd in ${FUNCTION_NAME} switch-profile
6213
6272
  complete -c $__sp_cmd -f -n 'test (count (commandline -opc)) -eq 1' -a '(__switch_profile_profiles) ${SUBCOMMANDS.join(" ")}'
6214
- complete -c $__sp_cmd -f -n 'test (count (commandline -opc)) -eq 2; and __fish_seen_subcommand_from use login' -a '(__switch_profile_profiles)'
6273
+ complete -c $__sp_cmd -f -n 'test (count (commandline -opc)) -eq 2; and __fish_seen_subcommand_from use login rename mv' -a '(__switch_profile_profiles)'
6215
6274
  complete -c $__sp_cmd -f -n '__fish_seen_subcommand_from remove rm' -a '(__switch_profile_profiles)'
6216
6275
  end`, POWERSHELL_FUNCTION = `function ${FUNCTION_NAME} {
6217
6276
  param([Parameter(ValueFromRemainingArguments = $true)] [string[]] $Rest)
@@ -6467,9 +6526,15 @@ var require_common = __commonJS({
6467
6526
  return { text: null, needsLogin: !1 };
6468
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";
6469
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 };
6470
- };
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
+ }));
6471
6535
  module2.exports = {
6472
6536
  CancelError: CancelError2,
6537
+ chooseRegion,
6473
6538
  ask,
6474
6539
  preflight,
6475
6540
  upgradeLegacySso,
@@ -7484,7 +7549,7 @@ var require_clipboard = __commonJS({
7484
7549
  // src/commands/login-flow.js
7485
7550
  var require_login_flow = __commonJS({
7486
7551
  "src/commands/login-flow.js"(exports2, module2) {
7487
- 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 }) => {
7488
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);
7489
7554
  if (!interactive)
7490
7555
  return plainLogin(args, label);
@@ -7542,12 +7607,12 @@ ${ui2.dim(`If nothing opens, visit: ${parsed.url}`)}`);
7542
7607
  spin && spin.error("Login did not complete");
7543
7608
  let lines = output.trim().split(`
7544
7609
  `).map((l2) => l2.trim()).filter(Boolean), detail = lines.filter((l2) => /error|exception|denied|invalid|expired|unable/i.test(l2)).slice(-2).join(`
7545
- `) || lines.slice(-1)[0] || "";
7546
- 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 ? `
7547
7611
  ${detail}` : ""}`, {
7548
7612
  cause: Object.assign(new Error(`aws ${args.join(" ")} exited with code ${code}`), { output }),
7549
- 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()}`
7550
7614
  });
7615
+ throw error.wrongRegion = wrongRegion, error.target = target, error;
7551
7616
  }, plainLogin = async (args, label) => {
7552
7617
  console.error(`Logging in to ${label}. Approve the request using the URL and code below.`), await new Promise((resolve, reject) => {
7553
7618
  let started = Date.now(), child = spawn("aws", args, { stdio: ["ignore", process.stderr, process.stderr], ...IS_WINDOWS ? { shell: !0 } : {} });
@@ -7568,6 +7633,93 @@ ${detail}` : ""}`, {
7568
7633
  }
7569
7634
  });
7570
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
+
7571
7723
  // src/home.js
7572
7724
  var require_home = __commonJS({
7573
7725
  "src/home.js"(exports2, module2) {
@@ -7632,10 +7784,63 @@ ${gray(S_BAR2)}`;
7632
7784
  }
7633
7785
  });
7634
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
+
7635
7840
  // src/commands/switch.js
7636
7841
  var require_switch = __commonJS({
7637
7842
  "src/commands/switch.js"(exports2, module2) {
7638
- 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 } = {}) => {
7639
7844
  p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7640
7845
  let lines = [];
7641
7846
  if (state.defaultProfile) {
@@ -7661,6 +7866,8 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7661
7866
  }, ACTIONS = [
7662
7867
  { key: "login", label: "Log in" },
7663
7868
  { key: "add", label: "Add" },
7869
+ { key: "portals", label: "Portals" },
7870
+ { key: "rename", label: "Rename" },
7664
7871
  { key: "remove", label: "Remove" },
7665
7872
  { key: "logout", label: "Log out" },
7666
7873
  { key: "settings", label: "Settings" }
@@ -7757,35 +7964,51 @@ ${ui2.dim("Tip: enable per-terminal switching in Settings.")}` : ""}`);
7757
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"));
7758
7965
  }, runAction = async (key, state, opts) => {
7759
7966
  let commands2 = require_commands();
7760
- return key == "login" ? commands2.login(state.defaultName, { ...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 });
7761
7968
  }, routeChoice = (choice) => {
7762
7969
  if (p4.isCancel(choice))
7763
7970
  return { type: "quit" };
7764
7971
  let value = String(choice);
7765
7972
  return value.startsWith(ACTION_PREFIX) ? { type: "page", key: value.slice(ACTION_PREFIX.length) } : { type: "switch", name: value };
7766
- }, afterPage = (result, err) => err ? err instanceof CancelError2 ? "back" : err instanceof ui2.CliError ? "error" : "throw" : result && result.switched ? "exit" : "home", homeLoop = async (state, opts, initialQuery) => {
7767
- 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 } = {}) => {
7768
7979
  for (; ; ) {
7769
- await header(state, { warnings: first });
7770
- let route = routeChoice(await pick(state, first ? initialQuery : void 0));
7771
- if (first = !1, route.type == "quit") {
7772
- p4.outro(ui2.dim(`Bye ${ui2.unicode ? "\u{1F44B}" : ""}`));
7773
- return;
7774
- }
7775
- if (route.type == "switch")
7776
- return switchTo(state.profiles.find((x2) => x2.name == route.name), state, opts);
7777
7980
  let result, error;
7778
7981
  try {
7779
- result = await runAction(route.key, state, opts);
7982
+ result = await run2(state);
7780
7983
  } catch (err) {
7781
7984
  error = err;
7782
7985
  }
7783
7986
  let next = afterPage(result, error);
7784
- if (next == "exit")
7987
+ if (next == "exit" || next == "home")
7785
7988
  return result;
7786
- if (next == "throw")
7989
+ if (next == "throw" || next == "back" && exitOnCancel)
7787
7990
  throw error;
7788
- 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();
7789
8012
  }
7790
8013
  }, switchCommand = async (query, opts) => {
7791
8014
  let interactive = ui2.isInteractive(opts);
@@ -7798,8 +8021,13 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7798
8021
  }
7799
8022
  if (query) {
7800
8023
  let { profile, candidates } = rank.resolveQuery(state.profiles, query, { usage: state.settings.usage, current: state.defaultName });
7801
- if (profile)
7802
- 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
+ }
7803
8031
  if (!interactive)
7804
8032
  throw new ui2.CliError(candidates.length ? `"${query}" matches ${candidates.length} profiles.` : `No profile matches "${query}".`, {
7805
8033
  code: 3,
@@ -7824,7 +8052,7 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7824
8052
  // src/commands/index.js
7825
8053
  var require_commands = __commonJS({
7826
8054
  "src/commands/index.js"(exports2, module2) {
7827
- 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) => {
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) => {
7828
8056
  !opts.fromPicker && !opts.json && p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7829
8057
  }, begin = async (opts, { needsInput } = {}) => {
7830
8058
  let interactive = ui2.isInteractive(opts);
@@ -7879,12 +8107,7 @@ var require_commands = __commonJS({
7879
8107
  return p4.outro(ui2.dim("Nothing changed."));
7880
8108
  }
7881
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.") : ""}`);
7882
- }, chooseRegion = async (message, initialValue) => ask(p4.autocomplete({
7883
- message,
7884
- maxItems: 8,
7885
- initialValue,
7886
- options: aws.regions.map((r2) => ({ value: r2.code, label: r2.code, hint: r2.name }))
7887
- })), validateProfileName = (taken) => (value) => {
8110
+ }, validateProfileName = (taken) => (value) => {
7888
8111
  if (!value)
7889
8112
  return "A name is required.";
7890
8113
  if (!/^[a-z0-9][a-z0-9_-]+$/.test(value))
@@ -7974,7 +8197,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7974
8197
  ]
7975
8198
  })) : "__new__";
7976
8199
  if (choice == "__new__") {
7977
- 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, "-");
7978
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);
7979
8202
  } else
7980
8203
  sessionName = choice;
@@ -7983,7 +8206,17 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7983
8206
  if (!token) {
7984
8207
  if (!interactive)
7985
8208
  throw new ui2.CliError(`Not logged in to ${sessionName}.`, { code: 2, hint: `Run: aws sso login --sso-session ${sessionName}` });
7986
- 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)
7987
8220
  throw new ui2.CliError(`Logged in, but no token was found for ${sessionName}.`);
7988
8221
  }
7989
8222
  let spin = opts.json ? null : ui2.spinner();
@@ -8060,6 +8293,90 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
8060
8293
  if (errors)
8061
8294
  throw ui2.cliErrorFrom(errors);
8062
8295
  p4.outro(`${ui2.ok(ui2.sym.ok)} Removed ${names.join(", ")}`);
8296
+ }, moveKey = (obj, from, to) => {
8297
+ if (!obj || !(from in obj))
8298
+ return obj;
8299
+ let { [from]: value, ...rest } = obj;
8300
+ return { ...rest, [to]: value };
8301
+ }, rename = async (from, to, opts) => {
8302
+ let interactive = await begin(opts), state = await loadState();
8303
+ if (!from) {
8304
+ if (!interactive)
8305
+ throw new ui2.CliError("Which profile?", { code: 3, hint: "Run: switch-profile rename <profile> <new-name>" });
8306
+ from = await ask(p4.autocomplete({
8307
+ message: "Rename which profile?",
8308
+ maxItems: 12,
8309
+ options: state.profiles.map((x2) => ({ value: x2.name, label: x2.name, hint: describe(x2) }))
8310
+ }));
8311
+ }
8312
+ let profile = findProfile(state, from), taken = [...state.profiles.map((x2) => x2.name).filter((n3) => n3 != from), "default"], validate = (v) => v == from ? "That is already its name." : validateProfileName(taken)(v);
8313
+ if (!to) {
8314
+ if (!interactive)
8315
+ throw new ui2.CliError("What is the new name?", { code: 3, hint: `Run: switch-profile rename ${from} <new-name>` });
8316
+ to = await ask(p4.text({ message: `New name for ${from}`, initialValue: from, validate }));
8317
+ }
8318
+ let invalid = validate(to);
8319
+ if (invalid)
8320
+ throw new ui2.CliError(invalid, { code: 3 });
8321
+ let [errors, result] = await aws.renameProfile(from, to);
8322
+ if (errors)
8323
+ throw ui2.cliErrorFrom(errors);
8324
+ let current = await settings2.read();
8325
+ await settings2.update({
8326
+ usage: moveKey(current.usage, from, to),
8327
+ ...loginKey(profile) == from ? { logins: moveKey(current.logins, from, to) } : {}
8328
+ });
8329
+ let inTerminal = process.env.AWS_PROFILE == from, terminal = inTerminal ? await shell.exportProfile(to).catch(log3.tolerated("handing the profile to sp", !1)) : !1;
8330
+ if (opts.json) {
8331
+ process.stdout.write(JSON.stringify({ from, to, default: result.wasDefault, sources: result.sources, processes: result.processes, terminal }) + `
8332
+ `);
8333
+ return;
8334
+ }
8335
+ let lines = [
8336
+ result.wasDefault ? ui2.dim("still the default profile") : null,
8337
+ result.sources.length ? ui2.dim(`source_profile updated in ${result.sources.join(", ")}`) : null,
8338
+ terminal ? ui2.dim("this terminal now uses the new name") : null
8339
+ ].filter(Boolean);
8340
+ p4.log.success([`Renamed ${ui2.accent(from)} to ${ui2.accent(to)}`, ...lines].join(`
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:
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."));
8063
8380
  }, LOGIN_MODE_LABELS = {
8064
8381
  auto: "Auto: device code over SSH, browser otherwise",
8065
8382
  device: "Always device code (approve from any device)",
@@ -8111,6 +8428,8 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
8111
8428
  logout,
8112
8429
  add,
8113
8430
  remove,
8431
+ rename,
8432
+ portals: portalsCommand,
8114
8433
  settings: settingsCommand
8115
8434
  };
8116
8435
  }
@@ -8126,6 +8445,8 @@ Examples:
8126
8445
  $ sp acme prod fuzzy match: switches if only one profile matches
8127
8446
  $ sp login log in again now (device code over SSH)
8128
8447
  $ sp add import every account of an SSO portal
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
8129
8450
  $ switch-profile status --json
8130
8451
 
8131
8452
  'sp' is the shortcut switch-profile installs on first use (or from Settings). It also
@@ -8155,6 +8476,6 @@ Exit codes: 0 ok \xB7 1 error \xB7 2 login required \xB7 3 bad input \xB7 130 ca
8155
8476
  }, main = (argv) => {
8156
8477
  log2.start({ version: CLI_VERSION, argv }), process.on("uncaughtException", crashed), process.on("unhandledRejection", crashed), process.on("exit", (code) => log2.info(`exit ${code}`));
8157
8478
  let program = new Command();
8158
- 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);
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);
8159
8480
  };
8160
8481
  module.exports = { main };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switch-profile",
3
- "version": "2.3.1",
3
+ "version": "2.5.0",
4
4
  "main": "index.js",
5
5
  "bin": {
6
6
  "switch-profile": "index.js"