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.
- package/README.md +53 -19
- package/dist/cli.js +153 -42
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,32 +1,59 @@
|
|
|
1
1
|
# switch-profile
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
3809
|
-
__export(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
6038
|
-
SWITCH_PROFILE_SHELL=${shell} SWITCH_PROFILE_ENV_FILE="$__sp_file"
|
|
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
|
|
6075
|
-
env SWITCH_PROFILE_SHELL=fish SWITCH_PROFILE_ENV_FILE=$__sp_file
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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: "
|
|
7476
|
-
{ key: "
|
|
7477
|
-
{ key: "remove",
|
|
7478
|
-
{ key: "logout",
|
|
7479
|
-
{ key: "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 =
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
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"))
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|