switch-profile 2.0.1 → 2.2.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 +22 -8
  2. package/dist/cli.js +168 -43
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -11,21 +11,23 @@ npx switch-profile
11
11
  ```
12
12
 
13
13
  ```
14
- ┌ switch-profile 2.0.0
14
+ ┌ switch-profile 2.1.0
15
15
  │
16
16
  │ ● acme-prod · Admin · 111111111111 · ap-southeast-2 · PROD · SSO ✓ auto-refresh · logged in 3h ago
17
17
  │
18
18
  ◆ Switch to · type a name, account, role or client
19
- │ Search: dev▌
20
- │ ● 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
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]
25
+ └ ↑↓ choose · enter switch · tab actions · esc quit
24
26
  ```
25
27
 
26
28
  ## Why it's great
27
29
 
28
- - **Seconds, not minutes.** Fuzzy search across profile name, account ID, account name, role, client and region, with your most-used accounts first. `sp acme prod` switches without even opening the picker.
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.
29
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.
30
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.
31
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.
@@ -51,6 +53,7 @@ npx switch-profile
51
53
  - [Why it's great](#why-its-great)
52
54
  - [Built on AWS best practices](#built-on-aws-best-practices)
53
55
  - [Quick start](#quick-start)
56
+ - [The home screen](#the-home-screen)
54
57
  - [Commands](#commands)
55
58
  - [Working on remote machines (SSH)](#working-on-remote-machines-ssh)
56
59
  - [Importing a client's accounts](#importing-a-clients-accounts)
@@ -90,11 +93,20 @@ sp acme prod # fuzzy match: switches if exactly one profile matches
90
93
 
91
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).
92
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
+
93
105
  ## Commands
94
106
 
95
107
  | Command | What it does |
96
108
  |---------|--------------|
97
- | `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). |
98
110
  | `use <profile>` | Switches to a profile by its exact name (no fuzzy matching). |
99
111
  | `status [--json]` | Shows the default profile, its login state, and what this terminal uses. |
100
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. |
@@ -142,7 +154,7 @@ sp add # then choose "Accounts from an SSO portal"
142
154
  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.
143
155
  2. Log in if needed.
144
156
  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`.
145
- 4. A preview shows the new profiles, the ones already set up, and the ones that no longer exist. Confirm to write them.
157
+ 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).
146
158
 
147
159
  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.
148
160
 
@@ -176,6 +188,8 @@ AWS_PROFILE=acme-dev terraform plan
176
188
 
177
189
  ## Settings
178
190
 
191
+ Open **Settings** from the home screen's action bar (Tab, then → to Settings), or run it directly:
192
+
179
193
  ```shell
180
194
  sp settings
181
195
  ```
@@ -243,7 +257,7 @@ The completion registers only if `compinit` ran before the `switch-profile` bloc
243
257
 
244
258
  ### Commands still use the wrong account
245
259
 
246
- `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).
247
261
 
248
262
  ### AWS CLI not found
249
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.1",
5354
+ version: "2.2.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",
@@ -5371,7 +5396,7 @@ var require_package = __commonJS({
5371
5396
  eslint: "^10.0.0",
5372
5397
  fuzzysort: "^3.1.0",
5373
5398
  globals: "^17.13.0",
5374
- mocha: "^8.2.1",
5399
+ mocha: "^11.8.0",
5375
5400
  uqr: "^0.1.3"
5376
5401
  },
5377
5402
  files: [
@@ -5615,14 +5640,16 @@ var require_transforms = __commonJS({
5615
5640
  login_session: params.login_session || null
5616
5641
  };
5617
5642
  return p4.isSso = !!p4.sso_start_url, p4.isLegacySso = p4.isSso && !p4.sso_session, p4.kind = p4.isSso ? "sso" : p4.login_session ? "login" : p4.role_arn ? "role" : params.credential_process ? "process" : "keys", !p4.sso_account_id && p4.role_arn && (p4.sso_account_id = (p4.role_arn.match(/^arn:aws[^:]*:iam::(\d{12}):/) || [])[1] || null), p4;
5618
- }), slug = (text2) => String(text2 || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""), generatedProfileName = (prefix, accountName, accountId, roleName) => [slug(prefix), slug(accountName) || accountId, slug(roleName)].filter(Boolean).join("-"), populateSsoProfiles = (configStr, { ssoSession, entries, region, prefix, prune, version }) => {
5619
- let config = configStr, profiles = listProfiles(config), session = ini.getSection(config, `sso-session ${ssoSession}`) || {}, samePortal = (p4) => p4.sso_session == ssoSession || p4.sso_start_url && p4.sso_start_url == session.sso_start_url, key = (accountId, roleName) => `${accountId}/${roleName}`, byKey = new Map(profiles.filter((p4) => p4.isSso && samePortal(p4)).map((p4) => [key(p4.sso_account_id, p4.sso_role_name), p4])), taken = new Set(profiles.map((p4) => p4.name)), wanted = new Set(entries.map((e) => key(e.accountId, e.roleName))), added = [], existing = [];
5643
+ }), slug = (text2) => String(text2 || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""), generatedProfileName = (prefix, accountName, accountId, roleName) => [slug(prefix), slug(accountName) || accountId, slug(roleName)].filter(Boolean).join("-"), ssoEntryKey = (accountId, roleName) => `${accountId}/${roleName}`, populateSsoProfiles = (configStr, { ssoSession, entries, region, prefix, prune, include, version }) => {
5644
+ let config = configStr, profiles = listProfiles(config), session = ini.getSection(config, `sso-session ${ssoSession}`) || {}, samePortal = (p4) => p4.sso_session == ssoSession || p4.sso_start_url && p4.sso_start_url == session.sso_start_url, key = ssoEntryKey, included = include ? new Set(include) : null, byKey = new Map(profiles.filter((p4) => p4.isSso && samePortal(p4)).map((p4) => [key(p4.sso_account_id, p4.sso_role_name), p4])), taken = new Set(profiles.map((p4) => p4.name)), wanted = new Set(entries.map((e) => key(e.accountId, e.roleName))), added = [], addedEntries = [], existing = [];
5620
5645
  for (let e of entries) {
5621
5646
  let found = byKey.get(key(e.accountId, e.roleName));
5622
5647
  if (found) {
5623
5648
  existing.push(found.name);
5624
5649
  continue;
5625
5650
  }
5651
+ if (included && !included.has(key(e.accountId, e.roleName)))
5652
+ continue;
5626
5653
  let base = generatedProfileName(prefix || ssoSession, e.accountName, e.accountId, e.roleName), name = base;
5627
5654
  for (let i2 = 2; taken.has(name); i2++)
5628
5655
  name = `${base}-${i2}`;
@@ -5635,13 +5662,13 @@ var require_transforms = __commonJS({
5635
5662
  ...e.accountName ? [[ACCOUNT_NAME_KEY, e.accountName]] : [],
5636
5663
  [GENERATED_KEY, ssoSession],
5637
5664
  [VERSION_KEY, version]
5638
- ]), added.push(name);
5665
+ ]), added.push(name), addedEntries.push({ name, accountId: e.accountId, accountName: e.accountName, roleName: e.roleName });
5639
5666
  }
5640
5667
  let stale = profiles.filter((p4) => p4.generated == ssoSession && !wanted.has(key(p4.sso_account_id, p4.sso_role_name))).map((p4) => p4.name), removed = [];
5641
5668
  if (prune)
5642
5669
  for (let name of stale)
5643
5670
  config = ini.removeSection(config, configSectionName(config, name)), removed.push(name);
5644
- return { config, added, existing, stale, removed };
5671
+ return { config, added, addedEntries, existing, stale, removed };
5645
5672
  }, 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}`, [
5646
5673
  ["sso_start_url", startUrl],
5647
5674
  ["sso_region", region],
@@ -5653,6 +5680,7 @@ var require_transforms = __commonJS({
5653
5680
  ACCOUNT_NAME_KEY,
5654
5681
  generatedProfileName,
5655
5682
  populateSsoProfiles,
5683
+ ssoEntryKey,
5656
5684
  listSsoSessions,
5657
5685
  addSsoSession,
5658
5686
  NAME_KEY,
@@ -6288,7 +6316,7 @@ var require_migrate = __commonJS({
6288
6316
  // src/commands/common.js
6289
6317
  var require_common = __commonJS({
6290
6318
  "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 {
6319
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), ui2 = require_ui(), CancelError2 = class extends Error {
6292
6320
  }, ask = async (promise) => {
6293
6321
  let value = await promise;
6294
6322
  if (p4.isCancel(value))
@@ -7364,7 +7392,7 @@ var require_clipboard = __commonJS({
7364
7392
  // src/commands/login-flow.js
7365
7393
  var require_login_flow = __commonJS({
7366
7394
  "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 }) => {
7395
+ var { spawn } = require("child_process"), p4 = (init_dist4(), __toCommonJS(dist_exports2)), { renderUnicodeCompact } = require_dist(), aws = require_aws(), settings2 = require_settings(), { parseLoginOutput, isRemoteSession, atLeast } = require_login(), clipboard = require_clipboard(), ui2 = require_ui(), { CancelError: CancelError2 } = require_common(), IS_WINDOWS = process.platform == "win32", RAW_OUTPUT_AFTER_MS = 1e4, clipboardNote = (result) => result == "local" ? ui2.ok("\u{1F4CB} copied") : result == "terminal" ? ui2.dim("\u{1F4CB} sent to your clipboard") : "", ssoLogin = async (target, { label, loginKey, interactive, loginMode }) => {
7368
7396
  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
7397
  if (!interactive)
7370
7398
  return plainLogin(args, label);
@@ -7445,10 +7473,74 @@ ${detail}` : ""}`, {
7445
7473
  }
7446
7474
  });
7447
7475
 
7476
+ // src/home.js
7477
+ var require_home = __commonJS({
7478
+ "src/home.js"(exports2, module2) {
7479
+ 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 }) => {
7480
+ let { area, index, count, hasRows } = state;
7481
+ 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;
7482
+ }, HomePrompt = class extends AutocompletePrompt {
7483
+ constructor(opts) {
7484
+ super(opts), this.actions = opts.actions, this.area = this.options.length ? "list" : "actions", this.actionIndex = 0, this.on("key", (char, key) => {
7485
+ let next = nextFocus(
7486
+ { area: this.area, index: this.actionIndex, count: this.actions.length, hasRows: this.filteredOptions.length > 0 },
7487
+ { name: key && key.name, char }
7488
+ );
7489
+ this.area = next.area, this.actionIndex = next.index, next.submit && (this.value = ACTION_PREFIX + this.actions[this.actionIndex].key);
7490
+ });
7491
+ }
7492
+ }, actionBar = (actions, focused) => actions.map((a2, i2) => {
7493
+ let label = ` ${a2.label} `;
7494
+ return i2 === focused ? ui2.style(["inverse", "cyan"], label) : ui2.dim(`[${label.trim()}]`);
7495
+ }).join(" "), home = ({ message, rows, actions, initialQuery, maxItems, input, output }) => new HomePrompt({
7496
+ actions,
7497
+ input,
7498
+ output,
7499
+ initialUserInput: initialQuery || void 0,
7500
+ options() {
7501
+ return rows(this.userInput || "");
7502
+ },
7503
+ validate: (value) => value === void 0 ? "Nothing matches: change the search, or press Tab for actions." : void 0,
7504
+ render() {
7505
+ let gray = (s) => ui2.style("gray", s), title = `${gray(S_BAR2)}
7506
+ ${symbol2(this.state)} ${message}`;
7507
+ if (this.state == "submit") {
7508
+ let v = String(this.value || "");
7509
+ if (v.startsWith(ACTION_PREFIX)) {
7510
+ let label = (actions.find((a2) => ACTION_PREFIX + a2.key == v) || {}).label || "";
7511
+ return `${gray(S_BAR2)}
7512
+ ${symbol2(this.state)} ${ui2.dim("switch-profile \u203A")} ${ui2.strong(label)}`;
7513
+ }
7514
+ return `${title}
7515
+ ${gray(S_BAR2)} ${ui2.dim(v)}`;
7516
+ }
7517
+ if (this.state == "cancel")
7518
+ return `${title}
7519
+ ${gray(S_BAR2)}`;
7520
+ 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];
7521
+ return lines.push(`${bar} ${ui2.dim("Search:")} ${this.userInputWithCursor}${this.userInput ? ui2.dim(` (${count} match${count == 1 ? "" : "es"})`) : ""}`), count ? limitOptions2({
7522
+ cursor: this.cursor,
7523
+ options: this.filteredOptions,
7524
+ maxItems,
7525
+ output: this.output,
7526
+ rowPadding: 8,
7527
+ 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)}`
7528
+ }).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(`
7529
+ `);
7530
+ }
7531
+ }).prompt();
7532
+ module2.exports = {
7533
+ ACTION_PREFIX,
7534
+ nextFocus,
7535
+ home
7536
+ };
7537
+ }
7538
+ });
7539
+
7448
7540
  // src/commands/switch.js
7449
7541
  var require_switch = __commonJS({
7450
7542
  "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(), { ask, preflight, loadState, loginKey, describe, loginState } = require_common(), { ssoLogin } = require_login_flow(), ACTION = "__action__:", header = async (state) => {
7543
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), settings2 = require_settings(), shell = require_shell(), rank = require_rank(), ui2 = require_ui(), { ask, preflight, loadState, loginKey, describe, loginState, CancelError: CancelError2 } = require_common(), { ssoLogin } = require_login_flow(), { home, ACTION_PREFIX } = require_home(), header = async (state, { warnings } = {}) => {
7452
7544
  p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7453
7545
  let lines = [];
7454
7546
  if (state.defaultProfile) {
@@ -7463,7 +7555,7 @@ var require_switch = __commonJS({
7463
7555
  lines.push(ui2.dim("No default profile yet. Pick one below."));
7464
7556
  let terminal = process.env.AWS_PROFILE;
7465
7557
  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();
7558
+ `)), warnings && warnEnv();
7467
7559
  }, warnEnv = () => {
7468
7560
  let set = shell.CLEARED_VARS.filter((v) => process.env[v]);
7469
7561
  if (!set.length)
@@ -7472,11 +7564,11 @@ var require_switch = __commonJS({
7472
7564
  p4.log.warn(`${set.join(", ")} ${set.length > 1 ? "are" : "is"} set in this terminal and override${set.length > 1 ? "" : "s"} profiles.
7473
7565
  ${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
7566
  }, 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" }
7567
+ { key: "login", label: "Log in" },
7568
+ { key: "add", label: "Add" },
7569
+ { key: "remove", label: "Remove" },
7570
+ { key: "logout", label: "Log out" },
7571
+ { key: "settings", label: "Settings" }
7480
7572
  ], buildRows = (profiles, state) => {
7481
7573
  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
7574
  return profiles.map((x2) => {
@@ -7489,20 +7581,13 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7489
7581
  ].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
7582
  return { value: x2.name, label, hint: hint || void 0 };
7491
7583
  });
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 }) => {
7584
+ }, pick = (state, initialQuery) => home({
7585
+ message: `Switch to ${ui2.dim("\xB7 type a name, account, role or client")}`,
7586
+ maxItems: Math.max(5, Math.min(14, (process.stdout.rows || 24) - 14)),
7587
+ initialQuery,
7588
+ actions: ACTIONS,
7589
+ rows: (query) => buildRows(rank.rankProfiles(state.profiles, { query, usage: state.settings.usage, current: state.defaultName }), state)
7590
+ }), connect = async (profile, state, { interactive, quiet, force }) => {
7506
7591
  let spin = quiet ? null : ui2.spinner(), tryIdentity = async () => {
7507
7592
  spin && spin.start(`Connecting to ${profile.name}`);
7508
7593
  try {
@@ -7523,7 +7608,7 @@ ${ui2.dim(viaShortcut ? "Switching clears them from this terminal." : `Unset ${s
7523
7608
  throw new ui2.CliError(`Logged in, but ${profile.name} still has no access.`, { hint: firstLine(result.message) });
7524
7609
  return result;
7525
7610
  }, firstLine = (text2) => String(text2 || "").trim().split(`
7526
- `).filter(Boolean).slice(-1)[0], switchTo = async (profile, state, opts) => {
7611
+ `).filter(Boolean).slice(-1)[0], switchTo = async (profile, state, opts) => (await doSwitch(profile, state, opts), { switched: !0 }), doSwitch = async (profile, state, opts) => {
7527
7612
  let { interactive, json } = opts, identity = await connect(profile, state, { interactive, quiet: json, force: opts.force }), [defaultErrors] = await aws.setDefaultProfile(profile.name);
7528
7613
  if (defaultErrors)
7529
7614
  throw new ui2.CliError(ui2.errorsMessage(defaultErrors));
@@ -7578,6 +7663,35 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7578
7663
  }, runAction = async (key, state, opts) => {
7579
7664
  let commands2 = require_commands();
7580
7665
  return key == "login" ? commands2.login(state.defaultName, { ...opts, fromPicker: !0 }) : commands2[key](opts.args || [], { ...opts, fromPicker: !0 });
7666
+ }, routeChoice = (choice) => {
7667
+ if (p4.isCancel(choice))
7668
+ return { type: "quit" };
7669
+ let value = String(choice);
7670
+ return value.startsWith(ACTION_PREFIX) ? { type: "page", key: value.slice(ACTION_PREFIX.length) } : { type: "switch", name: value };
7671
+ }, afterPage = (result, err) => err ? err instanceof CancelError2 ? "back" : err instanceof ui2.CliError ? "error" : "throw" : result && result.switched ? "exit" : "home", homeLoop = async (state, opts, initialQuery) => {
7672
+ let first = !0;
7673
+ for (; ; ) {
7674
+ await header(state, { warnings: first });
7675
+ let route = routeChoice(await pick(state, first ? initialQuery : void 0));
7676
+ if (first = !1, route.type == "quit") {
7677
+ p4.outro(ui2.dim(`Bye ${ui2.unicode ? "\u{1F44B}" : ""}`));
7678
+ return;
7679
+ }
7680
+ if (route.type == "switch")
7681
+ return switchTo(state.profiles.find((x2) => x2.name == route.name), state, opts);
7682
+ let result, error;
7683
+ try {
7684
+ result = await runAction(route.key, state, opts);
7685
+ } catch (err) {
7686
+ error = err;
7687
+ }
7688
+ let next = afterPage(result, error);
7689
+ if (next == "exit")
7690
+ return result;
7691
+ if (next == "throw")
7692
+ throw error;
7693
+ next == "back" && p4.cancel(ui2.dim(`${ui2.unicode ? "\u2190" : "<-"} Back`)), next == "error" && ui2.printError(error), state = await loadState();
7694
+ }
7581
7695
  }, switchCommand = async (query, opts) => {
7582
7696
  let interactive = ui2.isInteractive(opts);
7583
7697
  await preflight({ interactive, quiet: opts.json });
@@ -7598,11 +7712,11 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7598
7712
  });
7599
7713
  } else if (!interactive)
7600
7714
  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 });
7715
+ return homeLoop(state, { ...opts, interactive }, query);
7604
7716
  };
7605
7717
  module2.exports = {
7718
+ routeChoice,
7719
+ afterPage,
7606
7720
  switchCommand,
7607
7721
  switchTo,
7608
7722
  connect,
@@ -7615,7 +7729,7 @@ ${ui2.dim(`Open a new terminal or run: ${installed.reload}`)}`), installed.name
7615
7729
  // src/commands/index.js
7616
7730
  var require_commands = __commonJS({
7617
7731
  "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) => {
7732
+ var p4 = (init_dist4(), __toCommonJS(dist_exports2)), aws = require_aws(), transforms = require_transforms(), settings2 = require_settings(), shell = require_shell(), migrate = require_migrate(), rank = require_rank(), { resolveLoginMode, isRemoteSession } = require_login(), ui2 = require_ui(), { ask, preflight, loadState, loginKey, describe, loginState, upgradeLegacySso } = require_common(), { ssoLogin, rememberLogin } = require_login_flow(), { switchCommand, switchTo, connect, enableShortcut } = require_switch(), intro2 = (opts) => {
7619
7733
  !opts.fromPicker && !opts.json && p4.intro(`${ui2.badge("switch-profile")} ${ui2.dim(settings2.CLI_VERSION)}`);
7620
7734
  }, begin = async (opts, { needsInput } = {}) => {
7621
7735
  let interactive = ui2.isInteractive(opts);
@@ -7791,14 +7905,25 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7791
7905
  ].filter(Boolean).join(`
7792
7906
  `);
7793
7907
  opts.json || p4.log.message(summary);
7794
- let prune = !!opts.prune;
7908
+ let prune = !!opts.prune, include;
7795
7909
  if (interactive && !opts.yes) {
7796
- if (preview.added.length && !await ask(p4.confirm({ message: `Add ${preview.added.length} profile${preview.added.length == 1 ? "" : "s"}?` })))
7797
- return p4.outro(ui2.dim("Nothing changed."));
7910
+ if (preview.added.length) {
7911
+ let all = preview.addedEntries.map((e) => transforms.ssoEntryKey(e.accountId, e.roleName)), picked = await ask(p4.autocompleteMultiselect({
7912
+ message: `Add which profiles? ${ui2.dim("(all selected: space to untick, type to search, enter to confirm)")}`,
7913
+ maxItems: 12,
7914
+ initialValues: all,
7915
+ options: preview.addedEntries.map((e, i2) => ({ value: all[i2], label: e.name, hint: [e.accountName, e.accountId, e.roleName].filter(Boolean).join(" \xB7 ") }))
7916
+ }));
7917
+ if (!picked.length && !preview.stale.length)
7918
+ return p4.outro(ui2.dim("Nothing changed."));
7919
+ picked.length < all.length && (include = picked);
7920
+ }
7798
7921
  preview.stale.length && (prune = await ask(p4.confirm({ message: `Remove the ${preview.stale.length} profile${preview.stale.length == 1 ? "" : "s"} that no longer exist${preview.stale.length == 1 ? "s" : ""}?`, initialValue: !1 })));
7799
7922
  }
7923
+ if (include && !include.length && !prune)
7924
+ return p4.outro(ui2.dim("Nothing changed."));
7800
7925
  prune && await aws.backupAwsFiles();
7801
- let result = transforms.populateSsoProfiles(configStr, { ssoSession: sessionName, entries, region, prefix, prune, version: settings2.CLI_VERSION });
7926
+ let result = transforms.populateSsoProfiles(configStr, { ssoSession: sessionName, entries, region, prefix, prune, include, version: settings2.CLI_VERSION });
7802
7927
  if (await aws.writeAwsFile(aws.AWS_CONFIG_FILE, result.config), await settings2.update({}), opts.json) {
7803
7928
  process.stdout.write(JSON.stringify({ added: result.added, existing: result.existing, removed: result.removed, stale: prune ? [] : result.stale }) + `
7804
7929
  `);
@@ -7887,7 +8012,7 @@ AWS recommends SSO or console sign-in instead.`), !await ask(p4.confirm({ messag
7887
8012
  });
7888
8013
 
7889
8014
  // 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();
8015
+ var { Command } = require_commander(), p3 = (init_dist4(), __toCommonJS(dist_exports2)), ui = require_ui(), { CLI_VERSION } = require_settings(), { CancelError } = require_common(), commands = require_commands();
7891
8016
  process.env.AWS_CLI_AGENT_TOOLKIT_HINT_DISABLED = "true";
7892
8017
  var EXAMPLES = `
7893
8018
  Examples:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switch-profile",
3
- "version": "2.0.1",
3
+ "version": "2.2.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",
@@ -44,7 +45,7 @@
44
45
  "eslint": "^10.0.0",
45
46
  "fuzzysort": "^3.1.0",
46
47
  "globals": "^17.13.0",
47
- "mocha": "^8.2.1",
48
+ "mocha": "^11.8.0",
48
49
  "uqr": "^0.1.3"
49
50
  },
50
51
  "files": [