switch-profile 1.1.0 → 2.0.1

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.
@@ -1,199 +0,0 @@
1
- # Architecture
2
-
3
- This document describes the project structure, source files, and how the components fit together.
4
-
5
- ## Project Structure
6
-
7
- ```
8
- switch-profile/
9
- ├── index.js # CLI entry point (executable)
10
- ├── package.json # Project manifest
11
- ├── CHANGELOG.md # Auto-generated release notes
12
- ├── LICENSE # BSD 3-Clause
13
- ├── src/
14
- │ ├── core.js # Shell execution and error formatting
15
- │ ├── fileHelper.js # File system operations
16
- │ └── aws/
17
- │ ├── index.js # AWS profile and credential management
18
- │ └── regions.js # AWS region definitions
19
- └── test/
20
- └── index.js # Test suite
21
- ```
22
-
23
- ## Component Overview
24
-
25
- ### `index.js` - CLI Entry Point
26
-
27
- The main executable file (with `#!/usr/bin/env node` shebang). Registered as the `bin` target in `package.json`, so it runs when you invoke `switch-profile` or `npx switch-profile`.
28
-
29
- **Responsibilities:**
30
- - Defines the CLI command structure using [Commander.js](https://www.npmjs.com/package/commander).
31
- - Renders interactive prompts using [Inquirer.js](https://www.npmjs.com/package/inquirer).
32
- - Orchestrates the user flow: profile listing, selection, creation, deletion, and refresh.
33
- - Displays colored status output (expiry warnings, success messages).
34
-
35
- **Key functions:**
36
-
37
- | Function | Purpose |
38
- |----------|---------|
39
- | `switchCmd()` | Main command handler. Checks AWS CLI, shows current profile status, lists profiles, renders the selection menu. |
40
- | `setProfileToDefault(name, list, msg)` | Gets credentials for a profile, writes them as the `[default]` profile. Shows a clear error if the profile is not found in the list. Displays the `export AWS_PROFILE` hint on success. |
41
- | `printExportHint(profileName)` | Displays a cyan box with the profile isolation command. On Linux/macOS shows `export AWS_PROFILE=<name>`. On Windows shows both PowerShell (`$env:AWS_PROFILE`) and CMD (`set AWS_PROFILE`) variants. Box width adjusts dynamically. |
42
- | `createNewProfile(profiles, makeItDefault)` | Walks the user through creating a standard or SSO profile. Displays an SSO Setup Guide before SSO creation and checks for errors before reporting success. |
43
- | `chooseProfileName(denyList)` | Validates profile name input (lowercase alphanumeric, dashes, underscores, min 2 chars, no duplicates). |
44
- | `chooseRegions()` | Autocomplete region picker from the 24 supported AWS regions. |
45
- | `chooseNonEmpty(prop, message)` | Generic required-field input validator. |
46
-
47
- **Default command behavior:** If no command is passed (`process.argv.length == 2`), it automatically injects the `switch` command.
48
-
49
- ### `src/aws/index.js` - AWS Profile Management
50
-
51
- The core module. Handles all interactions with AWS configuration files and the AWS CLI.
52
-
53
- **Responsibilities:**
54
- - Reading and writing `~/.aws/config` and `~/.aws/credentials`.
55
- - Parsing INI-style profile sections.
56
- - Managing SSO sessions and credential caching.
57
- - Creating, deleting, and listing profiles.
58
- - Refreshing expired SSO sessions (including browser redirect).
59
-
60
- **Windows compatibility:** Uses `IS_WINDOWS` (`process.platform === 'win32'`) to select platform-appropriate line separator for parsing (`LINE_SEP`: regex `/\r?\n/` on Windows, `os.EOL` otherwise), newline character for writing (`NL`: `'\n'` on Windows, `os.EOL` otherwise), and `shell: true` for `spawn` on Windows (required because `aws` is a `.cmd` batch file).
61
-
62
- See [AWS Profile Management](aws-profile-management.md) for a detailed breakdown of every function.
63
-
64
- ### `src/aws/regions.js` - Region Definitions
65
-
66
- A static array of 24 AWS region objects, each with `name` and `code` properties:
67
-
68
- ```javascript
69
- { name: 'US East (N. Virginia)', code: 'us-east-1' }
70
- ```
71
-
72
- Regions covered: 4 US, 8 Asia Pacific, 6 EU, 2 China, 2 GovCloud, 1 Africa, 1 Canada, 1 Middle East, 1 South America.
73
-
74
- ### `src/core.js` - Shell Execution and Error Formatting
75
-
76
- Low-level utilities shared across the project.
77
-
78
- | Function | Purpose |
79
- |----------|---------|
80
- | `exec(cmd)` | Wraps `child_process.exec()` in a Promise. Rejects on error or stderr. |
81
- | `isCommandExist(cmd, errorMsg)` | Returns a function that checks if `cmd` exists in PATH (uses `which` on Unix, `where` on Windows). Results are cached. |
82
- | `printErrors(errors, options)` | Prints an array of `Error` objects in red. |
83
- | `printAWSerrors(errors, options)` | Like `printErrors` but detects missing AWS CLI and appends installation hints. |
84
- | `formatErrorMsg(errors, options)` | Formats error arrays into a single message string. Supports `noStack` option. |
85
-
86
- ### `src/fileHelper.js` - File System Operations
87
-
88
- A general-purpose file I/O library. Only a subset of its functions are used by `switch-profile`:
89
-
90
- **Used by the project:**
91
-
92
- | Function | Exported As | Purpose |
93
- |----------|-------------|---------|
94
- | `fileExists(path)` | `exists` | Check if a file or folder exists |
95
- | `readFile(path)` | `read` | Read file contents into a Buffer |
96
- | `writeToFile(path, content, opts)` | `write` | Create or overwrite a file |
97
- | `listFiles(folder, opts)` | `list` | Glob-based file listing |
98
- | `getJSON(path, default)` | `json.get` | Read and parse a JSON file |
99
-
100
- **Available but unused:**
101
-
102
- Functions for folder creation/deletion, MIME type detection, ZIP/TAR archiving, and more. These exist because `fileHelper.js` is a shared utility library.
103
-
104
- ## Dependency Graph
105
-
106
- ```
107
- index.js
108
- ├── commander (CLI framework)
109
- ├── inquirer (interactive prompts)
110
- ├── inquirer-autocomplete-prompt
111
- ├── colors (terminal colors)
112
- ├── src/core.js
113
- │ ├── colors
114
- │ ├── puffy (error handling: catchErrors, wrapErrors)
115
- │ └── child_process (Node built-in)
116
- ├── src/aws/index.js
117
- │ ├── puffy (error handling + delay)
118
- │ ├── src/core.js
119
- │ ├── src/fileHelper.js
120
- │ │ ├── core-async (co-routine support)
121
- │ │ ├── fast-glob (file pattern matching)
122
- │ │ ├── rimraf (recursive delete)
123
- │ │ ├── mime-types (MIME detection)
124
- │ │ ├── archiver (ZIP creation - unused)
125
- │ │ ├── tar-stream (TAR creation - unused)
126
- │ │ └── convert-stream (stream utils - unused)
127
- │ ├── src/aws/regions.js
128
- │ └── child_process (for spawn)
129
- └── package.json (version number)
130
- ```
131
-
132
- ## Error Handling Pattern
133
-
134
- All async functions in the AWS module use the `catchErrors` wrapper from the `puffy` library. This converts thrown errors into a `[errors, result]` tuple:
135
-
136
- ```javascript
137
- const [errors, profiles] = await listProfiles()
138
- if (errors) {
139
- printAWSerrors([new Error('Fail to list profiles'), ...errors])
140
- return
141
- }
142
- // Use profiles safely
143
- ```
144
-
145
- Errors are composed using `wrapErrors(message, errorArray)` to build layered error contexts:
146
-
147
- ```
148
- Error: Fail to get credentials for profile sso-dev
149
- → Error: Fail to refresh the SSO session
150
- → Error: Timeout - Time to wait exceeded 300000ms
151
- ```
152
-
153
- This pattern allows errors to propagate up with context while keeping individual functions simple.
154
-
155
- ## Data Flow
156
-
157
- ```
158
- User runs: npx switch-profile
159
- │
160
- ▼
161
- ┌──────────────┐
162
- │ index.js │ Checks AWS CLI v2 exists
163
- │ switchCmd() │ Gets default profile + status
164
- │ │ Lists all profiles
165
- └──────┬───────┘
166
- │
167
- ▼
168
- User selects a profile
169
- │
170
- ▼
171
- ┌──────────────────────┐
172
- │ setProfileToDefault │
173
- │ │
174
- │ 1. Find profile │
175
- │ 2. Get credentials │──────► For SSO: refresh session,
176
- │ 3. Update default │ open browser if needed
177
- └──────────────────────┘
178
- │
179
- ▼
180
- ┌──────────────────────┐
181
- │ updateDefaultProfile│
182
- │ │
183
- │ Writes [default] │
184
- │ section to: │
185
- │ - ~/.aws/credentials│
186
- │ - ~/.aws/config │
187
- └──────────────────────┘
188
- │
189
- ▼
190
- ┌──────────────────────┐
191
- │ printExportHint │
192
- │ │
193
- │ Shows: │
194
- │ export AWS_PROFILE= │
195
- │ <name> │
196
- │ for per-terminal │
197
- │ isolation │
198
- └──────────────────────┘
199
- ```
@@ -1,325 +0,0 @@
1
- # AWS Profile Management
2
-
3
- This document explains how `switch-profile` manages AWS profiles, SSO sessions, and credentials internally.
4
-
5
- ## Overview
6
-
7
- All profile and credential logic lives in `src/aws/index.js`. This module directly reads and writes AWS configuration files using regex-based INI parsing - it does not shell out to `aws configure` for most operations.
8
-
9
- ## File Locations
10
-
11
- | Path | Purpose | Managed By |
12
- |------|---------|------------|
13
- | `~/.aws/config` | Profile settings (region, output, SSO metadata) | `switch-profile` + `aws configure sso` |
14
- | `~/.aws/credentials` | Access keys, secrets, session tokens | `switch-profile` |
15
- | `~/.aws/sso/cache/` | SSO session tokens (JSON files) | AWS CLI |
16
- | `~/.aws/cli/cache/` | Temporary SSO credentials (JSON files) | AWS CLI |
17
-
18
- ## Constants
19
-
20
- ```javascript
21
- SSO_GET_CREDS_TIMEOUT = 300000 // 5 minutes to complete SSO login
22
- ```
23
-
24
- ## Functions Reference
25
-
26
- ### Profile Listing
27
-
28
- #### `listProfiles()`
29
-
30
- Reads `~/.aws/config` and extracts all profile sections except `[default]`.
31
-
32
- **Returns:** `[errors, profiles[]]`
33
-
34
- Each profile object contains:
35
-
36
- ```javascript
37
- {
38
- name: 'sso-dev', // Profile name
39
- friendlyName: 'sso-dev (SSO [role:Admin - account:123456])', // Display name
40
- region: 'us-east-1', // AWS region
41
- output: 'json', // Output format
42
- sso_start_url: 'https://my.awsapps.com/start', // null if not SSO
43
- sso_region: 'us-east-1', // null if not SSO
44
- sso_account_id: '123456789012', // null if not SSO
45
- sso_role_name: 'Admin', // null if not SSO
46
- sso_session: 'my-session' // null if not using SSO sessions (legacy SSO format)
47
- }
48
- ```
49
-
50
- **Parsing logic:**
51
- 1. Matches all `[...]` sections in the config file.
52
- 2. For each section, extracts the content between it and the next `[` bracket.
53
- 3. Splits content by newline and extracts key-value pairs using `getParam()`.
54
- 4. Strips `[profile ...]` prefix to get the profile name.
55
- 5. For newer SSO profiles using `sso_session`, looks up the `[sso-session name]` section to retrieve `sso_start_url` and `sso_region`.
56
- 6. For SSO profiles, uses `sso_region` as the effective region.
57
- 7. Builds a `friendlyName` that includes role and account info for SSO profiles.
58
-
59
- #### `getDefaultProfile()`
60
-
61
- Reads the `[default]` section from `~/.aws/credentials`.
62
-
63
- **Returns:** `[errors, profile]`
64
-
65
- ```javascript
66
- {
67
- aws_access_key_id: 'AKIA...',
68
- aws_secret_access_key: '...',
69
- aws_session_token: '...', // null for standard profiles
70
- expiry_date: '2021-07-17...', // null for standard profiles
71
- profile: 'sso-dev' // Which profile these creds belong to
72
- }
73
- ```
74
-
75
- The `profile` and `expiry_date` fields are custom additions by `switch-profile` - they are not standard AWS CLI fields. They are written into the credentials file so the tool can track which profile is currently active and when it expires.
76
-
77
- ### Credential Retrieval
78
-
79
- #### `getCredentials(profile, ssoUrl)`
80
-
81
- The main credential retrieval function. Behaves differently for SSO vs. standard profiles.
82
-
83
- **For SSO profiles** (when `ssoUrl` is provided):
84
-
85
- 1. Calls `refreshSsoSession()` to ensure a valid SSO session exists.
86
- 2. Calls `getSsoCredentials()` to get temporary AWS credentials via the CLI cache.
87
- 3. If credentials fail with "session associated with this profile has expired", forces a session refresh and retries.
88
- 4. If `getSsoCredentials()` returns `null` (cache-based lookup failed), falls back to `aws configure export-credentials --profile <name> --format process` which directly outputs credentials as JSON.
89
- 5. If both approaches fail, throws an error.
90
-
91
- **For standard profiles** (when `ssoUrl` is null):
92
-
93
- 1. Reads `~/.aws/credentials`.
94
- 2. Finds the `[profile-name]` section.
95
- 3. Extracts `aws_access_key_id`, `aws_secret_access_key`, and `aws_session_token`.
96
-
97
- **Returns:** `[errors, { aws_access_key_id, aws_secret_access_key, aws_session_token, expiry_date }]`
98
-
99
- #### `getSsoCredentials(profile)`
100
-
101
- Gets SSO credentials by running `aws configure list --profile <profile>`, which triggers the AWS CLI to populate its credential cache.
102
-
103
- **Process:**
104
- 1. Executes `aws configure list --profile <profile>`.
105
- 2. Parses the output to extract the last 4 characters of the access key and secret key (the CLI masks them as `****XXXX`).
106
- 3. Calls `getSsoCredsFromCacheFile()` to find the full credentials in the CLI cache.
107
-
108
- **Returns:** `[errors, creds]` or `[errors, null]` if credentials not found.
109
-
110
- #### `getSsoCredsFromCacheFile(access_key_end, secret_key_end)`
111
-
112
- Searches `~/.aws/cli/cache/` for a JSON file containing credentials that match the given key suffixes.
113
-
114
- **Matching criteria:**
115
- - `ProviderType` must be `"sso"`.
116
- - `Credentials.Expiration` must be at least 2 minutes in the future.
117
- - Last 4 characters of `AccessKeyId` must match `access_key_end`.
118
- - Last 4 characters of `SecretAccessKey` must match `secret_key_end`.
119
-
120
- **Returns:** `[errors, creds]` or `[errors, null]` if no match found.
121
-
122
- ### SSO Session Management
123
-
124
- #### `getSsoSession(ssoUrl)`
125
-
126
- Finds a valid SSO session token in `~/.aws/sso/cache/`.
127
-
128
- **Process:**
129
- 1. Extracts the host from `ssoUrl` (e.g., `my.awsapps.com`).
130
- 2. Lists all `*.json` files in `~/.aws/sso/cache/`.
131
- 3. For each file, checks if:
132
- - `startUrl` host matches the SSO URL host.
133
- - `expiresAt` is at least 2 minutes in the future.
134
- - `accessToken` is present.
135
-
136
- **Returns:** `[errors, session]` or `[errors, null]` if no valid session found.
137
-
138
- #### `isSSOexpired(ssoSession)`
139
-
140
- Checks if an SSO session will expire within the next 2 minutes.
141
-
142
- **Returns:** `boolean`
143
-
144
- #### `refreshSsoSession(profile, ssoUrl, options)`
145
-
146
- Ensures a valid SSO session exists. If not, triggers a new SSO login.
147
-
148
- **Process:**
149
- 1. Check for existing valid session via `getSsoSession()`.
150
- 2. If expired or `options.force` is true:
151
- - Runs `aws sso login --profile <profile>` (opens browser).
152
- - Polls `getSsoSession()` every 2 seconds.
153
- - Waits up to 5 minutes (`SSO_GET_CREDS_TIMEOUT`) for the session to appear.
154
- 3. Returns the session object, or throws a timeout error.
155
-
156
- ### Profile Modification
157
-
158
- #### `updateDefaultProfile({ profile, region, expiry_date, aws_access_key_id, aws_secret_access_key, aws_session_token })`
159
-
160
- Updates the `[default]` section in both `~/.aws/credentials` and `~/.aws/config`.
161
-
162
- **In `~/.aws/credentials`:**
163
- ```ini
164
- [default]
165
- aws_access_key_id = AKIA...
166
- aws_secret_access_key = ...
167
- aws_session_token = ... # Only for SSO profiles
168
- expiry_date = 2021-07-17T... # Only for SSO profiles
169
- profile = sso-dev # Custom field: tracks active profile
170
- ```
171
-
172
- **In `~/.aws/config`:**
173
- ```ini
174
- [default]
175
- region = us-east-1
176
- output = json
177
- ```
178
-
179
- **Approach:** Uses regex to find the existing `[default]` section and replaces it. If no `[default]` section exists, prepends it. All other profile sections are preserved unchanged.
180
-
181
- #### `createProfile({ name, aws_access_key_id, aws_secret_access_key, region })`
182
-
183
- Creates a standard (non-SSO) profile.
184
-
185
- **Writes to `~/.aws/config`:**
186
- ```ini
187
- [profile my-profile]
188
- region = us-east-1
189
- output = json
190
- ```
191
-
192
- **Writes to `~/.aws/credentials`:**
193
- ```ini
194
- [my-profile]
195
- aws_access_key_id = AKIA...
196
- aws_secret_access_key = ...
197
- ```
198
-
199
- If the config or credentials files don't exist, they are created with a default `[default]` section first.
200
-
201
- #### `createSsoProfile(name)`
202
-
203
- Creates an SSO profile by spawning the interactive `aws configure sso --profile <name>` command. This hands control to the AWS CLI, which walks the user through the SSO setup (URL, region, account, role).
204
-
205
- Uses `spawn` with `stdio: 'inherit'` so the user interacts directly with the AWS CLI process. On Windows, `shell: true` is also passed because `aws` is typically a `.cmd` batch file that requires shell resolution. Checks the exit code of the spawned process and throws an error if it is non-zero, indicating the SSO setup failed.
206
-
207
- #### `deleteProfiles(profiles)`
208
-
209
- Deletes one or more profiles from both `~/.aws/config` and `~/.aws/credentials`.
210
-
211
- **Rules:**
212
- - The `default` profile cannot be deleted.
213
- - Uses regex matching to find and remove the profile section and everything up to the next `[` bracket.
214
-
215
- #### `deleteProfileFromConfig(profile, fileContent)` / `deleteProfileFromCreds(profile, fileContent)`
216
-
217
- Internal helpers that remove a single profile section from file content using regex. The config version handles both `[profile name]` and `[name]` syntax. The creds version handles `[name]` syntax only.
218
-
219
- ### AWS CLI Detection
220
-
221
- #### `awsCliV2Exists(noFailIfMissing)`
222
-
223
- Checks that AWS CLI v2+ is installed.
224
-
225
- **Process:**
226
- 1. Runs `which aws` (or `where aws` on Windows) to check if the command exists.
227
- 2. Runs `aws --version` and parses the major version number.
228
- 3. Rejects if version is 1 or lower.
229
-
230
- **Parameters:**
231
- - `noFailIfMissing`: If `true`, returns `false` instead of throwing when AWS CLI is not found.
232
-
233
- ### Utility
234
-
235
- #### `getParam(params, paramName)`
236
-
237
- Extracts a parameter value from an array of lines in `key = value` format.
238
-
239
- ```javascript
240
- getParam([' region = us-east-1', ' output = json'], 'region')
241
- // Returns: 'us-east-1'
242
- ```
243
-
244
- #### `getCredsFile()` / `getConfigFile()`
245
-
246
- Reads the credentials or config file and returns its contents as a string. Returns an empty string if the file doesn't exist.
247
-
248
- ## SSO Credential Flow Diagram
249
-
250
- ```
251
- User selects an SSO profile
252
- │
253
- ▼
254
- getCredentials(profile, ssoUrl)
255
- │
256
- ▼
257
- refreshSsoSession(profile, ssoUrl)
258
- │
259
- ├── getSsoSession(ssoUrl)
260
- │ │
261
- │ ▼
262
- │ Search ~/.aws/sso/cache/*.json
263
- │ for matching, non-expired session
264
- │ │
265
- │ ├── Found valid session → continue
266
- │ │
267
- │ └── Not found or expired
268
- │ │
269
- │ ▼
270
- │ aws sso login --profile <name>
271
- │ (opens browser for SSO auth)
272
- │ │
273
- │ ▼
274
- │ Poll ~/.aws/sso/cache/ every 2s
275
- │ (up to 5 min timeout)
276
- │
277
- ▼
278
- getSsoCredentials(profile)
279
- │
280
- ▼
281
- aws configure list --profile <name>
282
- (populates ~/.aws/cli/cache/)
283
- │
284
- ▼
285
- getSsoCredsFromCacheFile(keyEnd, secretEnd)
286
- │
287
- ▼
288
- Search ~/.aws/cli/cache/*.json
289
- for matching credentials
290
- │
291
- ├── Found → return credentials
292
- │
293
- └── Not found (null)
294
- │
295
- ▼
296
- Fallback: aws configure export-credentials
297
- --profile <name> --format process
298
- (directly outputs credentials as JSON)
299
- │
300
- ├── Success → return credentials
301
- │
302
- └── Failure → throw error
303
- │
304
- ▼
305
- Return { aws_access_key_id,
306
- aws_secret_access_key,
307
- aws_session_token,
308
- expiry_date }
309
- ```
310
-
311
- ## Expiry Handling
312
-
313
- The tool uses a 2-minute buffer for expiry checks throughout. A session or credential is considered expired if it expires within the next 2 minutes:
314
-
315
- ```javascript
316
- const credStillValid = (Date.now() - 2*60*1000) < new Date(expiresAt).getTime()
317
- ```
318
-
319
- This prevents credentials from expiring between the time they're fetched and the time they're used.
320
-
321
- ## Profile Name Conventions
322
-
323
- - In `~/.aws/config`: Non-default profiles use `[profile name]` syntax. The default uses `[default]`.
324
- - In `~/.aws/credentials`: All profiles use `[name]` syntax (no `profile` prefix).
325
- - Profile names must be: lowercase alphanumeric characters, dashes, and underscores. Minimum 2 characters.