switch-profile 2.0.0 → 2.1.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 +53 -19
  2. package/dist/cli.js +153 -42
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,32 +1,59 @@
1
1
  # switch-profile
2
2
 
3
- Switch between AWS profiles from your terminal. Type a few letters, press enter, and every AWS tool uses that account. SSO logins work on remote machines too (device code, clipboard over SSH, QR code), and each terminal can stay on its own account.
3
+ <p align="center"><img src="docs/images/hero.webp" width="100%" alt="switch-profile illustration: a girl rides a rope lift up a tower of lit workshops"></p>
4
4
 
5
+ **Jump between AWS accounts from your terminal in seconds, the way AWS recommends.**
6
+
7
+ If you work across many AWS accounts (clients, environments, organizations), you switch accounts dozens of times a day. `switch-profile` turns that into a few keystrokes: type part of a name, account ID or client, press Enter, and every AWS tool in that terminal is on the right account, logged in and verified. It works the same on your laptop and on a remote machine over SSH, and it never asks you to manage access keys.
8
+
9
+ ```shell
10
+ npx switch-profile
5
11
  ```
6
- ┌ switch-profile 2.0.0
12
+
13
+ ```
14
+ ┌ switch-profile 2.1.0
7
15
  │
8
16
  │ ● acme-prod · Admin · 111111111111 · ap-southeast-2 · PROD · SSO ✓ auto-refresh · logged in 3h ago
9
17
  │
10
18
  ◆ Switch to · type a name, account, role or client
11
- │ Search: dev▌
12
- │ ● acme-dev Admin 222222222222 ap-southeast-2 (used 2d ago)
19
+ │ Search: dev█ (3 matches)
20
+ │ ● acme-dev Admin 222222222222 ap-southeast-2 used 2d ago
13
21
  │ ○ globex-dev ReadOnly 333333333333 us-east-1
14
22
  │ ○ initech-dev Admin 444444444444 eu-west-1
15
- └
23
+ │
24
+ │ [Log in] [Add] [Remove] [Log out] [Settings]
25
+ └ ↑↓ choose · enter switch · tab actions · esc quit
16
26
  ```
17
27
 
18
- - **Fast picker.** Fuzzy search across profile name, account id, account name, role, SSO session (client) and region. The profiles you use most come first.
19
- - **One identity check per switch.** `switch-profile` calls `aws sts get-caller-identity` once, logs in if needed, then makes the profile the default.
20
- - **Auto-refresh.** It writes the profile's *settings* into `[default]`, never temporary credentials, so the AWS CLI, SDKs and Terraform refresh credentials on their own.
21
- - **Per-terminal.** With the `sp` shortcut, the current terminal also gets `AWS_PROFILE`, so different terminals can use different accounts. `sp` also adds tab-completion.
22
- - **Remote-friendly.** Over SSH, logins use a device code: the code is copied to your laptop's clipboard and the URL can be shown as a QR code.
23
- - **Import a whole portal.** `sp add` creates one profile per account and role of an IAM Identity Center portal.
24
- - **Script and agent friendly.** Never prompts when there is no terminal, `--json` output, stable exit codes.
28
+ ## Why it's great
29
+
30
+ - **Seconds, not minutes.** Fuzzy search across profile name, account ID, account name, role, client and region, with your most-used accounts first, and every other action one Tab away. `sp acme prod` switches without even opening the home screen.
31
+ - **Logged in when you need it.** If a session has expired, it logs you in on the spot, then never again until the session ends: credentials refresh on their own in the AWS CLI, SDKs and Terraform.
32
+ - **Every terminal on its own account.** With the `sp` shortcut, each terminal keeps its own `AWS_PROFILE`, plus tab-completion of your profile names.
33
+ - **Made for remote machines.** On a server reached over SSH, the login code lands in your laptop's clipboard, or scan a QR code with your phone.
34
+ - **A whole client in one step.** `sp add` creates a profile for every account and role in an IAM Identity Center portal, and re-syncs it later.
35
+ - **Scripts and AI agents welcome.** It never prompts without a terminal, offers `--json` output and stable exit codes.
36
+
37
+ ## Built on AWS best practices
38
+
39
+ `switch-profile` does not invent its own security model. It follows what AWS recommends, lets the official AWS CLI do the sensitive work, and adds the guardrails that make switching accounts all day safe.
40
+
41
+ - **Temporary credentials, not access keys.** AWS recommends that people sign in through an identity provider, such as IAM Identity Center (AWS SSO), and use short-lived credentials instead of long-lived access keys ([IAM best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)). `switch-profile` is built around SSO and console sign-in (`aws login`). Access keys still work, but it warns you before creating new ones.
42
+ - **It never copies credentials.** Switching writes only a profile's *settings* into `[default]`. The AWS CLI and SDKs fetch short-lived credentials themselves and refresh them through `[sso-session]` ([AWS CLI SSO guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html)). There are no secrets in anything `switch-profile` writes, so nothing to leak and nothing to go stale.
43
+ - **The AWS CLI does the logging in.** Logins run through `aws sso login` and `aws login`: browser sign-in on your machine, device code where there is no browser. `switch-profile` stores no tokens and has no telemetry. Its only network calls are the AWS CLI's own and npx fetching the package.
44
+ - **Device codes, handled carefully.** Device-code approvals are a known phishing route, so they are used only where there is no browser, and the login screen tells you to approve only the exact code it shows.
45
+ - **You always know which account you are in.** Every switch confirms the real identity with `aws sts get-caller-identity`. Production accounts are flagged in red. Environment variables that would silently override your choice (such as `AWS_ACCESS_KEY_ID`) are flagged, and `sp` clears them.
46
+ - **One account per terminal.** `AWS_PROFILE` is scoped to a terminal, so switching in one window cannot move a deploy running in another.
47
+ - **Careful with your files.** Edits to `~/.aws` are atomic, upgrades back up your files first, and the shell setup lives in one clearly marked block that you can remove from Settings at any time.
48
+ - **Always the latest version.** Run through `npx`, every run uses the newest release: nothing to install, nothing to update.
25
49
 
26
50
  ## Table of Contents
27
51
 
28
52
  <!-- BEGIN toc -->
53
+ - [Why it's great](#why-its-great)
54
+ - [Built on AWS best practices](#built-on-aws-best-practices)
29
55
  - [Quick start](#quick-start)
56
+ - [The home screen](#the-home-screen)
30
57
  - [Commands](#commands)
31
58
  - [Working on remote machines (SSH)](#working-on-remote-machines-ssh)
32
59
  - [Importing a client's accounts](#importing-a-clients-accounts)
@@ -62,19 +89,24 @@ sp acme-prod # switch straight to a profile
62
89
  sp acme prod # fuzzy match: switches if exactly one profile matches
63
90
  ```
64
91
 
65
- For speed, install it globally. `npx` checks the npm registry on every run, which adds a noticeable delay; `sp` uses the global command when it exists and falls back to `npx` otherwise.
66
-
67
- ```shell
68
- npm install -g switch-profile
69
- ```
92
+ **Why npx?** It always runs the latest release, so you never have to update anything. `sp` uses `npx switch-profile@latest` under the hood. The cost is a second or two while npm checks for a newer version, and the tool needs a connection to AWS anyway. A global install (`npm install -g switch-profile`) also works, but then updating is up to you, and `sp` keeps using npx regardless.
70
93
 
71
94
  No profiles yet? `switch-profile` starts the **Add profiles** flow. To import every account of your company's or client's SSO portal, see [Importing a client's accounts](#importing-a-clients-accounts).
72
95
 
96
+ ## The home screen
97
+
98
+ Running `sp` (or `npx switch-profile`) with no arguments opens the home screen:
99
+
100
+ - **Type and press Enter** to switch. That's the whole path for everyday use.
101
+ - **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.
102
+ - **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.
103
+ - A switch ends the session, which is what lets `sp` set `AWS_PROFILE` in your terminal.
104
+
73
105
  ## Commands
74
106
 
75
107
  | Command | What it does |
76
108
  |---------|--------------|
77
- | `switch-profile [profile...]` | Opens the picker. With words: an exact name or a unique fuzzy match switches directly; otherwise the picker opens pre-filtered (or exits 3 when non-interactive). |
109
+ | `switch-profile [profile...]` | Opens the home screen (search and switch, plus the action bar). With words: an exact name or a unique fuzzy match switches directly; otherwise the home screen opens pre-filtered (or exits 3 when non-interactive). |
78
110
  | `use <profile>` | Switches to a profile by its exact name (no fuzzy matching). |
79
111
  | `status [--json]` | Shows the default profile, its login state, and what this terminal uses. |
80
112
  | `login [profile] [--device\|--browser]` | Starts a fresh login now, even if the session is still valid. Defaults to this terminal's `AWS_PROFILE`, then the default profile. |
@@ -156,6 +188,8 @@ AWS_PROFILE=acme-dev terraform plan
156
188
 
157
189
  ## Settings
158
190
 
191
+ Open **Settings** from the home screen's action bar (Tab, then → to Settings), or run it directly:
192
+
159
193
  ```shell
160
194
  sp settings
161
195
  ```
@@ -223,7 +257,7 @@ The completion registers only if `compinit` ran before the `switch-profile` bloc
223
257
 
224
258
  ### Commands still use the wrong account
225
259
 
226
- `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` in the environment override every profile, and `AWS_REGION` overrides the profile's region. The picker warns about them; switching with `sp` clears the credential variables (not the region).
260
+ `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` in the environment override every profile, and `AWS_REGION` overrides the profile's region. The home screen warns about them; switching with `sp` clears the credential variables (not the region).
227
261
 
228
262
  ### AWS CLI not found
229
263
 
package/dist/cli.js CHANGED
@@ -2836,6 +2836,30 @@ var require_src = __commonJS({
2836
2836
  });
2837
2837
 
2838
2838
  // node_modules/@clack/core/dist/index.mjs
2839
+ var dist_exports = {};
2840
+ __export(dist_exports, {
2841
+ AutocompletePrompt: () => T$1,
2842
+ CANCEL_SYMBOL: () => CANCEL_SYMBOL,
2843
+ ConfirmPrompt: () => r,
2844
+ DatePrompt: () => U,
2845
+ GroupMultiSelectPrompt: () => u$2,
2846
+ MultiLinePrompt: () => h,
2847
+ MultiSelectPrompt: () => a,
2848
+ PasswordPrompt: () => u$1,
2849
+ Prompt: () => y,
2850
+ SelectKeyPrompt: () => u3,
2851
+ SelectPrompt: () => n$1,
2852
+ TextPrompt: () => n2,
2853
+ block: () => block,
2854
+ getColumns: () => getColumns,
2855
+ getRows: () => getRows,
2856
+ isAccessible: () => isAccessible,
2857
+ isCancel: () => isCancel,
2858
+ runValidation: () => runValidation,
2859
+ settings: () => settings,
2860
+ updateSettings: () => updateSettings,
2861
+ wrapTextWithPrefix: () => wrapTextWithPrefix
2862
+ });
2839
2863
  function findCursor(s, o2, l2) {
2840
2864
  if (!l2.some((r2) => !r2.disabled))
2841
2865
  return s;
@@ -3805,8 +3829,8 @@ ${i2}` : `${s}${(0, import_node_util.styleText)("inverse", r2)}${i2}`;
3805
3829
  });
3806
3830
 
3807
3831
  // node_modules/@clack/prompts/dist/index.mjs
3808
- var dist_exports = {};
3809
- __export(dist_exports, {
3832
+ var dist_exports2 = {};
3833
+ __export(dist_exports2, {
3810
3834
  CANCEL_SYMBOL: () => CANCEL_SYMBOL,
3811
3835
  MULTISELECT_INSTRUCTIONS: () => MULTISELECT_INSTRUCTIONS,
3812
3836
  SELECT_INSTRUCTIONS: () => SELECT_INSTRUCTIONS,
@@ -5261,7 +5285,7 @@ var require_ui = __commonJS({
5261
5285
  let h2 = Math.floor(m3 / 60);
5262
5286
  return h2 < 48 ? `${h2}h ago` : `${Math.floor(h2 / 24)}d ago`;
5263
5287
  }, spinner2 = (options) => {
5264
- let p4 = (init_dist4(), __toCommonJS(dist_exports));
5288
+ let p4 = (init_dist4(), __toCommonJS(dist_exports2));
5265
5289
  if (process.stdout.isTTY && !env.ACCESSIBLE)
5266
5290
  return p4.spinner(options);
5267
5291
  let accessible = process.stdout.isTTY;
@@ -5277,7 +5301,7 @@ var require_ui = __commonJS({
5277
5301
  isCancelled: !1
5278
5302
  };
5279
5303
  }, timerSpinner = () => {
5280
- let p4 = (init_dist4(), __toCommonJS(dist_exports));
5304
+ let p4 = (init_dist4(), __toCommonJS(dist_exports2));
5281
5305
  if (!process.stdout.isTTY || env.ACCESSIBLE)
5282
5306
  return spinner2();
5283
5307
  let frames = unicode2 ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["-", "\\", "|", "/"], timer = null, message = "", started = 0, i2 = 0, clear = () => process.stdout.write("\r\x1B[2K"), render = () => {
@@ -5327,7 +5351,7 @@ var require_package = __commonJS({
5327
5351
  "package.json"(exports2, module2) {
5328
5352
  module2.exports = {
5329
5353
  name: "switch-profile",
5330
- version: "2.0.0",
5354
+ version: "2.1.0",
5331
5355
  main: "index.js",
5332
5356
  bin: {
5333
5357
  "switch-profile": "index.js"
@@ -5363,6 +5387,7 @@ var require_package = __commonJS({
5363
5387
  author: "Nicolas Dao",
5364
5388
  license: "BSD-3-Clause",
5365
5389
  devDependencies: {
5390
+ "@clack/core": "^1.5.1",
5366
5391
  "@clack/prompts": "^1.8.1",
5367
5392
  "@eslint/js": "^10.0.1",
5368
5393
  chai: "^4.1.2",
@@ -6034,10 +6059,10 @@ var require_shell = __commonJS({
6034
6059
  var fs = require("fs"), { homedir } = require("os"), { join: join2, dirname: dirname2, basename } = require("path"), { run: run2 } = require_core(), IS_WINDOWS = process.platform === "win32", IS_MAC = process.platform === "darwin", FUNCTION_NAME = "sp", BLOCK_START_PREFIX = "# >>> switch-profile", BLOCK_END = "# <<< switch-profile <<<", BLOCK_REGEX = /(^|\r?\n)# >>> switch-profile[^\n]*\r?\n[\s\S]*?# <<< switch-profile <<<[^\n]*(\r?\n)?/, blockStart = (version) => `${BLOCK_START_PREFIX} v${version} (managed by switch-profile, do not edit) >>>`, CLEARED_VARS = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_DEFAULT_PROFILE"], SUBCOMMANDS = ["status", "login", "logout", "add", "remove", "settings", "use"], PROFILES_SED = "sed -n 's/^\\[profile \\(.*\\)\\]$/\\1/p'", POSIX_FUNCTION = (shell) => `${FUNCTION_NAME}() {
6035
6060
  local __sp_file __sp_status
6036
6061
  __sp_file="$(mktemp "\${TMPDIR:-/tmp}/switch-profile.XXXXXX")" || return 1
6037
- if command -v switch-profile >/dev/null 2>&1; then
6038
- SWITCH_PROFILE_SHELL=${shell} SWITCH_PROFILE_ENV_FILE="$__sp_file" switch-profile "$@"
6062
+ if [ -n "\${SWITCH_PROFILE_DEV_BIN:-}" ]; then
6063
+ SWITCH_PROFILE_SHELL=${shell} SWITCH_PROFILE_ENV_FILE="$__sp_file" "$SWITCH_PROFILE_DEV_BIN" "$@"
6039
6064
  else
6040
- SWITCH_PROFILE_SHELL=${shell} SWITCH_PROFILE_ENV_FILE="$__sp_file" npx --yes switch-profile "$@"
6065
+ SWITCH_PROFILE_SHELL=${shell} SWITCH_PROFILE_ENV_FILE="$__sp_file" npx --yes switch-profile@latest "$@"
6041
6066
  fi
6042
6067
  __sp_status=$?
6043
6068
  if [ -s "$__sp_file" ]; then
@@ -6071,10 +6096,10 @@ fi`, BASH_COMPLETION = `_switch_profile_complete() {
6071
6096
  }
6072
6097
  complete -F _switch_profile_complete ${FUNCTION_NAME} switch-profile`, FISH_FUNCTION = `function ${FUNCTION_NAME}
6073
6098
  set -l __sp_file (mktemp)
6074
- if type -q switch-profile
6075
- env SWITCH_PROFILE_SHELL=fish SWITCH_PROFILE_ENV_FILE=$__sp_file switch-profile $argv
6099
+ if set -q SWITCH_PROFILE_DEV_BIN
6100
+ env SWITCH_PROFILE_SHELL=fish SWITCH_PROFILE_ENV_FILE=$__sp_file $SWITCH_PROFILE_DEV_BIN $argv
6076
6101
  else
6077
- env SWITCH_PROFILE_SHELL=fish SWITCH_PROFILE_ENV_FILE=$__sp_file npx --yes switch-profile $argv
6102
+ env SWITCH_PROFILE_SHELL=fish SWITCH_PROFILE_ENV_FILE=$__sp_file npx --yes switch-profile@latest $argv
6078
6103
  end
6079
6104
  set -l __sp_status $status
6080
6105
  if test -s $__sp_file
@@ -6099,7 +6124,7 @@ end`, POWERSHELL_FUNCTION = `function ${FUNCTION_NAME} {
6099
6124
  $env:SWITCH_PROFILE_SHELL = 'powershell'
6100
6125
  $env:SWITCH_PROFILE_ENV_FILE = $spFile
6101
6126
  try {
6102
- if (Get-Command switch-profile -ErrorAction SilentlyContinue) { switch-profile @Rest } else { npx --yes switch-profile @Rest }
6127
+ if ($env:SWITCH_PROFILE_DEV_BIN) { & $env:SWITCH_PROFILE_DEV_BIN @Rest } else { npx --yes switch-profile@latest @Rest }
6103
6128
  } finally {
6104
6129
  Remove-Item Env:\\SWITCH_PROFILE_SHELL -ErrorAction SilentlyContinue
6105
6130
  Remove-Item Env:\\SWITCH_PROFILE_ENV_FILE -ErrorAction SilentlyContinue
@@ -6288,7 +6313,7 @@ var require_migrate = __commonJS({
6288
6313
  // src/commands/common.js
6289
6314
  var require_common = __commonJS({
6290
6315
  "src/commands/common.js"(exports2, module2) {
6291
- var p4 = (init_dist4(), __toCommonJS(dist_exports)), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), ui2 = require_ui(), CancelError2 = class extends Error {
6316
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), ui2 = require_ui(), CancelError2 = class extends Error {
6292
6317
  }, ask = async (promise) => {
6293
6318
  let value = await promise;
6294
6319
  if (p4.isCancel(value))
@@ -7364,7 +7389,7 @@ var require_clipboard = __commonJS({
7364
7389
  // src/commands/login-flow.js
7365
7390
  var require_login_flow = __commonJS({
7366
7391
  "src/commands/login-flow.js"(exports2, module2) {
7367
- var { spawn } = require("child_process"), p4 = (init_dist4(), __toCommonJS(dist_exports)), { renderUnicodeCompact } = require_dist(), aws = require_aws(), settings2 = require_settings(), { parseLoginOutput, isRemoteSession, atLeast } = require_login(), clipboard = require_clipboard(), ui2 = require_ui(), { CancelError: CancelError2 } = require_common(), IS_WINDOWS = process.platform == "win32", RAW_OUTPUT_AFTER_MS = 1e4, clipboardNote = (result) => result == "local" ? ui2.ok("\u{1F4CB} copied") : result == "terminal" ? ui2.dim("\u{1F4CB} sent to your clipboard") : "", ssoLogin = async (target, { label, loginKey, interactive, loginMode }) => {
7392
+ var { spawn } = require("child_process"), p4 = (init_dist4(), __toCommonJS(dist_exports2)), { renderUnicodeCompact } = require_dist(), aws = require_aws(), settings2 = require_settings(), { parseLoginOutput, isRemoteSession, atLeast } = require_login(), clipboard = require_clipboard(), ui2 = require_ui(), { CancelError: CancelError2 } = require_common(), IS_WINDOWS = process.platform == "win32", RAW_OUTPUT_AFTER_MS = 1e4, clipboardNote = (result) => result == "local" ? ui2.ok("\u{1F4CB} copied") : result == "terminal" ? ui2.dim("\u{1F4CB} sent to your clipboard") : "", ssoLogin = async (target, { label, loginKey, interactive, loginMode }) => {
7368
7393
  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);
7369
7394
  if (!interactive)
7370
7395
  return plainLogin(args, label);
@@ -7445,10 +7470,74 @@ ${detail}` : ""}`, {
7445
7470
  }
7446
7471
  });
7447
7472
 
7473
+ // src/home.js
7474
+ var require_home = __commonJS({
7475
+ "src/home.js"(exports2, module2) {
7476
+ var { AutocompletePrompt } = (init_dist3(), __toCommonJS(dist_exports)), { S_BAR: S_BAR2, S_BAR_END: S_BAR_END2, S_RADIO_ACTIVE: S_RADIO_ACTIVE2, S_RADIO_INACTIVE: S_RADIO_INACTIVE2, symbol: symbol2, limitOptions: limitOptions2 } = (init_dist4(), __toCommonJS(dist_exports2)), ui2 = require_ui(), ACTION_PREFIX = "__action__:", nextFocus = (state, { name, char }) => {
7477
+ let { area, index, count, hasRows } = state;
7478
+ return name == "tab" ? { ...state, area: area == "list" || !hasRows ? "actions" : "list" } : area != "actions" ? state : name == "left" ? { ...state, index: (index - 1 + count) % count } : name == "right" ? { ...state, index: (index + 1) % count } : name == "return" ? { ...state, submit: !0 } : (name == "up" || name == "down") && hasRows ? { ...state, area: "list" } : char && char.length == 1 && char >= " " && hasRows ? { ...state, area: "list" } : state;
7479
+ }, HomePrompt = class extends AutocompletePrompt {
7480
+ constructor(opts) {
7481
+ super(opts), this.actions = opts.actions, this.area = this.options.length ? "list" : "actions", this.actionIndex = 0, this.on("key", (char, key) => {
7482
+ let next = nextFocus(
7483
+ { area: this.area, index: this.actionIndex, count: this.actions.length, hasRows: this.filteredOptions.length > 0 },
7484
+ { name: key && key.name, char }
7485
+ );
7486
+ this.area = next.area, this.actionIndex = next.index, next.submit && (this.value = ACTION_PREFIX + this.actions[this.actionIndex].key);
7487
+ });
7488
+ }
7489
+ }, actionBar = (actions, focused) => actions.map((a2, i2) => {
7490
+ let label = ` ${a2.label} `;
7491
+ return i2 === focused ? ui2.style(["inverse", "cyan"], label) : ui2.dim(`[${label.trim()}]`);
7492
+ }).join(" "), home = ({ message, rows, actions, initialQuery, maxItems, input, output }) => new HomePrompt({
7493
+ actions,
7494
+ input,
7495
+ output,
7496
+ initialUserInput: initialQuery || void 0,
7497
+ options() {
7498
+ return rows(this.userInput || "");
7499
+ },
7500
+ validate: (value) => value === void 0 ? "Nothing matches: change the search, or press Tab for actions." : void 0,
7501
+ render() {
7502
+ let gray = (s) => ui2.style("gray", s), title = `${gray(S_BAR2)}
7503
+ ${symbol2(this.state)} ${message}`;
7504
+ if (this.state == "submit") {
7505
+ let v = String(this.value || "");
7506
+ if (v.startsWith(ACTION_PREFIX)) {
7507
+ let label = (actions.find((a2) => ACTION_PREFIX + a2.key == v) || {}).label || "";
7508
+ return `${gray(S_BAR2)}
7509
+ ${symbol2(this.state)} ${ui2.dim("switch-profile \u203A")} ${ui2.strong(label)}`;
7510
+ }
7511
+ return `${title}
7512
+ ${gray(S_BAR2)} ${ui2.dim(v)}`;
7513
+ }
7514
+ if (this.state == "cancel")
7515
+ return `${title}
7516
+ ${gray(S_BAR2)}`;
7517
+ let bar = ui2.style(this.state == "error" ? "yellow" : "cyan", S_BAR2), end = ui2.style(this.state == "error" ? "yellow" : "cyan", S_BAR_END2), onList = this.area == "list", count = this.filteredOptions.length, lines = [title];
7518
+ return lines.push(`${bar} ${ui2.dim("Search:")} ${this.userInputWithCursor}${this.userInput ? ui2.dim(` (${count} match${count == 1 ? "" : "es"})`) : ""}`), count ? limitOptions2({
7519
+ cursor: this.cursor,
7520
+ options: this.filteredOptions,
7521
+ maxItems,
7522
+ output: this.output,
7523
+ rowPadding: 8,
7524
+ style: (option, active) => active && onList ? `${ui2.style("green", S_RADIO_ACTIVE2)} ${option.label}${option.hint ? ui2.dim(` ${option.hint}`) : ""}` : `${ui2.dim(S_RADIO_INACTIVE2)} ${ui2.dim(option.label)}`
7525
+ }).forEach((line) => lines.push(`${bar} ${line}`)) : lines.push(`${bar} ${ui2.warn(this.options.length ? "No matches" : "No profiles yet: Tab to Add")}`), lines.push(bar), lines.push(`${bar} ${actionBar(actions, onList ? -1 : this.actionIndex)}`), this.state == "error" && lines.push(`${bar} ${ui2.warn(this.error)}`), lines.push(`${end} ${ui2.dim(onList ? "\u2191\u2193 choose \xB7 enter switch \xB7 tab actions \xB7 esc quit" : "\u2190\u2192 choose \xB7 enter open \xB7 tab back to the list \xB7 esc quit")}`), lines.join(`
7526
+ `);
7527
+ }
7528
+ }).prompt();
7529
+ module2.exports = {
7530
+ ACTION_PREFIX,
7531
+ nextFocus,
7532
+ home
7533
+ };
7534
+ }
7535
+ });
7536
+
7448
7537
  // src/commands/switch.js
7449
7538
  var require_switch = __commonJS({
7450
7539
  "src/commands/switch.js"(exports2, module2) {
7451
- var p4 = (init_dist4(), __toCommonJS(dist_exports)), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), rank = require_rank(), ui2 = require_ui(), { exec: exec2 } = require_core(), { ask, preflight, loadState, loginKey, describe, loginState } = require_common(), { ssoLogin } = require_login_flow(), ACTION = "__action__:", header = async (state) => {
7540
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), rank = require_rank(), ui2 = require_ui(), { ask, preflight, loadState, loginKey, describe, loginState, CancelError: CancelError2 } = require_common(), { ssoLogin } = require_login_flow(), { home, ACTION_PREFIX } = require_home(), header = async (state, { warnings } = {}) => {
7452
7541
  p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7453
7542
  let lines = [];
7454
7543
  if (state.defaultProfile) {
@@ -7463,7 +7552,7 @@ var require_switch = __commonJS({
7463
7552
  lines.push(ui2.dim("No default profile yet. Pick one below."));
7464
7553
  let terminal = process.env.AWS_PROFILE;
7465
7554
  terminal && terminal != state.defaultName && lines.push(`${ui2.dim("This terminal uses")} ${ui2.accent(terminal)} ${ui2.dim("(AWS_PROFILE)")}`), p4.log.message(lines.join(`
7466
- `)), warnEnv();
7555
+ `)), warnings && warnEnv();
7467
7556
  }, warnEnv = () => {
7468
7557
  let set = shell.CLEARED_VARS.filter((v) => process.env[v]);
7469
7558
  if (!set.length)
@@ -7472,11 +7561,11 @@ var require_switch = __commonJS({
7472
7561
  p4.log.warn(`${set.join(", ")} ${set.length > 1 ? "are" : "is"} set in this terminal and override${set.length > 1 ? "" : "s"} profiles.
7473
7562
  ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${set.length > 1 ? "them" : "it"}, or switch with ${shell.FUNCTION_NAME} to clear ${set.length > 1 ? "them" : "it"} automatically.`)}`);
7474
7563
  }, ACTIONS = [
7475
- { key: "add", icon: "+", label: "Add profiles", hint: "import accounts from an SSO portal, or create one" },
7476
- { key: "login", icon: "\u21BB", label: "Log in again", hint: "start a fresh SSO session for the current profile" },
7477
- { key: "remove", icon: "\u2212", label: "Remove profiles" },
7478
- { key: "logout", icon: "\u23FB", label: "Log out", hint: "end every SSO session on this machine" },
7479
- { key: "settings", icon: "\u2699", label: "Settings" }
7564
+ { key: "login", label: "Log in" },
7565
+ { key: "add", label: "Add" },
7566
+ { key: "remove", label: "Remove" },
7567
+ { key: "logout", label: "Log out" },
7568
+ { key: "settings", label: "Settings" }
7480
7569
  ], buildRows = (profiles, state) => {
7481
7570
  let cols = ui2.columns(), showRole = cols >= 80, showRegion = cols >= 100, roleOf = (x2) => x2.sso_role_name || (x2.kind == "keys" ? "access keys" : x2.kind == "login" ? "console sign-in" : x2.kind), all = state.profiles, nameWidth = Math.min(Math.max(...all.map((x2) => x2.name.length), 8), 32), roleWidth = Math.min(Math.max(0, ...all.map((x2) => roleOf(x2).length)), 22), usage = state.settings.usage || {};
7482
7571
  return profiles.map((x2) => {
@@ -7489,20 +7578,13 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7489
7578
  ].filter((v) => v !== null).join(" ").trimEnd(), hint = x2.name == state.defaultName ? "current default" : [x2.accountName, usage[x2.name] ? `used ${ui2.ago(usage[x2.name].last)}` : null].filter(Boolean).join(" \xB7 ");
7490
7579
  return { value: x2.name, label, hint: hint || void 0 };
7491
7580
  });
7492
- }, pick = async (state, initialQuery) => {
7493
- let actions = ACTIONS.map((a2) => ({ value: ACTION + a2.key, label: `${ui2.accent(ui2.unicode ? a2.icon : ">")} ${a2.label}`, hint: a2.hint, text: a2.label.toLowerCase() })), maxItems = Math.max(5, Math.min(14, (process.stdout.rows || 24) - 10));
7494
- return ask(p4.autocomplete({
7495
- message: `Switch to ${ui2.dim("\xB7 type a name, account, role or client")}`,
7496
- maxItems,
7497
- initialUserInput: initialQuery || void 0,
7498
- // Rows are already filtered and ranked below; clack would otherwise re-filter them by substring.
7499
- filter: () => !0,
7500
- options() {
7501
- let query = this.userInput || "", ranked = rank.rankProfiles(state.profiles, { query, usage: state.settings.usage, current: state.defaultName }), q = query.trim().toLowerCase(), matchingActions = actions.filter((a2) => !q || a2.text.includes(q)).map((a2) => ({ value: a2.value, label: a2.label, hint: a2.hint }));
7502
- return [...buildRows(ranked, state), ...matchingActions];
7503
- }
7504
- }));
7505
- }, connect = async (profile, state, { interactive, quiet, force }) => {
7581
+ }, pick = (state, initialQuery) => home({
7582
+ message: `Switch to ${ui2.dim("\xB7 type a name, account, role or client")}`,
7583
+ maxItems: Math.max(5, Math.min(14, (process.stdout.rows || 24) - 14)),
7584
+ initialQuery,
7585
+ actions: ACTIONS,
7586
+ rows: (query) => buildRows(rank.rankProfiles(state.profiles, { query, usage: state.settings.usage, current: state.defaultName }), state)
7587
+ }), connect = async (profile, state, { interactive, quiet, force }) => {
7506
7588
  let spin = quiet ? null : ui2.spinner(), tryIdentity = async () => {
7507
7589
  spin && spin.start(`Connecting to ${profile.name}`);
7508
7590
  try {
@@ -7523,7 +7605,7 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7523
7605
  throw new ui2.CliError(`Logged in, but ${profile.name} still has no access.`, { hint: firstLine(result.message) });
7524
7606
  return result;
7525
7607
  }, firstLine = (text2) => String(text2 || "").trim().split(`
7526
- `).filter(Boolean).slice(-1)[0], switchTo = async (profile, state, opts) => {
7608
+ `).filter(Boolean).slice(-1)[0], switchTo = async (profile, state, opts) => (await doSwitch(profile, state, opts), { switched: !0 }), doSwitch = async (profile, state, opts) => {
7527
7609
  let { interactive, json } = opts, identity = await connect(profile, state, { interactive, quiet: json, force: opts.force }), [defaultErrors] = await aws.setDefaultProfile(profile.name);
7528
7610
  if (defaultErrors)
7529
7611
  throw new ui2.CliError(ui2.errorsMessage(defaultErrors));
@@ -7574,10 +7656,39 @@ ${ui2.dim("Tip: enable per-terminal switching in Settings.")}` : ""}`);
7574
7656
  }, enableShortcut = async () => {
7575
7657
  let installed = await shell.install(settings2.CLI_VERSION);
7576
7658
  p4.log.success(`Added ${ui2.accent(shell.FUNCTION_NAME)} to ${installed.files.map(shell.tilde).join(", ")}
7577
- ${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")), await exec2(`${process.platform == "win32" ? "where" : "which"} switch-profile`).then(() => !0, () => !1) || p4.log.message(ui2.dim(`Tip: npm install -g switch-profile makes ${shell.FUNCTION_NAME} start instantly (npx checks the registry on every run).`));
7659
+ ${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"));
7578
7660
  }, runAction = async (key, state, opts) => {
7579
7661
  let commands2 = require_commands();
7580
7662
  return key == "login" ? commands2.login(state.defaultName, { ...opts, fromPicker: !0 }) : commands2[key](opts.args || [], { ...opts, fromPicker: !0 });
7663
+ }, routeChoice = (choice) => {
7664
+ if (p4.isCancel(choice))
7665
+ return { type: "quit" };
7666
+ let value = String(choice);
7667
+ return value.startsWith(ACTION_PREFIX) ? { type: "page", key: value.slice(ACTION_PREFIX.length) } : { type: "switch", name: value };
7668
+ }, afterPage = (result, err) => err ? err instanceof CancelError2 ? "back" : err instanceof ui2.CliError ? "error" : "throw" : result && result.switched ? "exit" : "home", homeLoop = async (state, opts, initialQuery) => {
7669
+ let first = !0;
7670
+ for (; ; ) {
7671
+ await header(state, { warnings: first });
7672
+ let route = routeChoice(await pick(state, first ? initialQuery : void 0));
7673
+ if (first = !1, route.type == "quit") {
7674
+ p4.outro(ui2.dim(`Bye ${ui2.unicode ? "\u{1F44B}" : ""}`));
7675
+ return;
7676
+ }
7677
+ if (route.type == "switch")
7678
+ return switchTo(state.profiles.find((x2) => x2.name == route.name), state, opts);
7679
+ let result, error;
7680
+ try {
7681
+ result = await runAction(route.key, state, opts);
7682
+ } catch (err) {
7683
+ error = err;
7684
+ }
7685
+ let next = afterPage(result, error);
7686
+ if (next == "exit")
7687
+ return result;
7688
+ if (next == "throw")
7689
+ throw error;
7690
+ next == "back" && p4.cancel(ui2.dim(`${ui2.unicode ? "\u2190" : "<-"} Back`)), next == "error" && ui2.printError(error), state = await loadState();
7691
+ }
7581
7692
  }, switchCommand = async (query, opts) => {
7582
7693
  let interactive = ui2.isInteractive(opts);
7583
7694
  await preflight({ interactive, quiet: opts.json });
@@ -7598,11 +7709,11 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7598
7709
  });
7599
7710
  } else if (!interactive)
7600
7711
  throw new ui2.CliError("Which profile? Pass its name.", { code: 3, hint: `e.g. switch-profile ${state.profiles[0].name}` });
7601
- await header(state);
7602
- let choice = await pick(state, query);
7603
- return choice.startsWith(ACTION) ? runAction(choice.slice(ACTION.length), state, { ...opts, interactive }) : switchTo(state.profiles.find((x2) => x2.name == choice), state, { ...opts, interactive });
7712
+ return homeLoop(state, { ...opts, interactive }, query);
7604
7713
  };
7605
7714
  module2.exports = {
7715
+ routeChoice,
7716
+ afterPage,
7606
7717
  switchCommand,
7607
7718
  switchTo,
7608
7719
  connect,
@@ -7615,7 +7726,7 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7615
7726
  // src/commands/index.js
7616
7727
  var require_commands = __commonJS({
7617
7728
  "src/commands/index.js"(exports2, module2) {
7618
- var p4 = (init_dist4(), __toCommonJS(dist_exports)), aws = require_aws(), transforms = require_transforms(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), rank = require_rank(), { resolveLoginMode, isRemoteSession } = require_login(), ui2 = require_ui(), { ask, preflight, loadState, loginKey, describe, loginState, upgradeLegacySso } = require_common(), { ssoLogin, rememberLogin } = require_login_flow(), { switchCommand, switchTo, connect, enableShortcut } = require_switch(), intro2 = (opts) => {
7729
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), transforms = require_transforms(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), rank = require_rank(), { resolveLoginMode, isRemoteSession } = require_login(), ui2 = require_ui(), { ask, preflight, loadState, loginKey, describe, loginState, upgradeLegacySso } = require_common(), { ssoLogin, rememberLogin } = require_login_flow(), { switchCommand, switchTo, connect, enableShortcut } = require_switch(), intro2 = (opts) => {
7619
7730
  !opts.fromPicker && !opts.json && p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7620
7731
  }, begin = async (opts, { needsInput } = {}) => {
7621
7732
  let interactive = ui2.isInteractive(opts);
@@ -7887,7 +7998,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7887
7998
  });
7888
7999
 
7889
8000
  // src/cli.js
7890
- var { Command } = require_commander(), p3 = (init_dist4(), __toCommonJS(dist_exports)), ui = require_ui(), { CLI_VERSION } = require_settings(), { CancelError } = require_common(), commands = require_commands();
8001
+ var { Command } = require_commander(), p3 = (init_dist4(), __toCommonJS(dist_exports2)), ui = require_ui(), { CLI_VERSION } = require_settings(), { CancelError } = require_common(), commands = require_commands();
7891
8002
  process.env.AWS_CLI_AGENT_TOOLKIT_HINT_DISABLED = "true";
7892
8003
  var EXAMPLES = `
7893
8004
  Examples:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switch-profile",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "main": "index.js",
5
5
  "bin": {
6
6
  "switch-profile": "index.js"
@@ -36,6 +36,7 @@
36
36
  "author": "Nicolas Dao",
37
37
  "license": "BSD-3-Clause",
38
38
  "devDependencies": {
39
+ "@clack/core": "^1.5.1",
39
40
  "@clack/prompts": "^1.8.1",
40
41
  "@eslint/js": "^10.0.1",
41
42
  "chai": "^4.1.2",