switch-profile 0.1.2 → 0.2.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.
@@ -0,0 +1,240 @@
1
+ # Configuration Files
2
+
3
+ This document describes the exact format of all AWS configuration files that `switch-profile` reads and writes.
4
+
5
+ ## `~/.aws/config`
6
+
7
+ The AWS CLI configuration file. Stores profile settings including region, output format, and SSO metadata.
8
+
9
+ ### Format
10
+
11
+ Standard INI-style format. The default profile uses `[default]`, all other profiles use `[profile name]`.
12
+
13
+ ### Example
14
+
15
+ ```ini
16
+ [default]
17
+ region = us-east-1
18
+ output = json
19
+
20
+ [profile my-standard]
21
+ region = ap-southeast-2
22
+ output = json
23
+
24
+ [profile sso-dev]
25
+ sso_start_url = https://my-company.awsapps.com/start
26
+ sso_region = us-east-1
27
+ sso_account_id = 123456789012
28
+ sso_role_name = CloudlessAdmin
29
+ region = us-east-1
30
+ output = json
31
+ ```
32
+
33
+ ### Fields
34
+
35
+ **Standard profile fields:**
36
+
37
+ | Field | Description | Example |
38
+ |-------|-------------|---------|
39
+ | `region` | AWS region for API calls | `us-east-1` |
40
+ | `output` | CLI output format | `json` |
41
+
42
+ **SSO profile additional fields:**
43
+
44
+ | Field | Description | Example |
45
+ |-------|-------------|---------|
46
+ | `sso_start_url` | SSO portal URL | `https://my-company.awsapps.com/start` |
47
+ | `sso_region` | Region where SSO is configured | `us-east-1` |
48
+ | `sso_account_id` | AWS account ID (12 digits) | `123456789012` |
49
+ | `sso_role_name` | IAM role name in the account | `CloudlessAdmin` |
50
+
51
+ ### How `switch-profile` Modifies This File
52
+
53
+ When setting a profile as default, only the `[default]` section is updated (region and output). Other profile sections are preserved unchanged.
54
+
55
+ When creating a standard profile, a new `[profile name]` section is appended. When creating an SSO profile, the `aws configure sso` command writes the section.
56
+
57
+ When deleting a profile, the `[profile name]` section and its contents (up to the next `[` bracket) are removed via regex.
58
+
59
+ ---
60
+
61
+ ## `~/.aws/credentials`
62
+
63
+ Stores AWS access credentials. All profiles use `[name]` syntax (no `profile` prefix).
64
+
65
+ ### Example
66
+
67
+ ```ini
68
+ [default]
69
+ aws_access_key_id = ASIAZOCWXABCD123456
70
+ aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
71
+ aws_session_token = AQoDYXdzEJr...
72
+ expiry_date = 2021-07-17T11:33:12.000Z
73
+ profile = sso-dev
74
+
75
+ [my-standard]
76
+ aws_access_key_id = AKIAIOSFODNN7EXAMPLE
77
+ aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
78
+ ```
79
+
80
+ ### Standard AWS Fields
81
+
82
+ | Field | Description | Present For |
83
+ |-------|-------------|-------------|
84
+ | `aws_access_key_id` | AWS access key | All profiles |
85
+ | `aws_secret_access_key` | AWS secret key | All profiles |
86
+ | `aws_session_token` | Temporary session token | SSO profiles only |
87
+
88
+ ### Custom Fields (added by `switch-profile`)
89
+
90
+ These fields are written into the `[default]` section only:
91
+
92
+ | Field | Description | Purpose |
93
+ |-------|-------------|---------|
94
+ | `profile` | Name of the profile these credentials belong to | Track which profile is currently active |
95
+ | `expiry_date` | ISO 8601 timestamp of when credentials expire | Display expiry status, detect expired profiles |
96
+
97
+ These are non-standard fields. The AWS CLI ignores them, but `switch-profile` reads them back to show profile status.
98
+
99
+ ### How `switch-profile` Modifies This File
100
+
101
+ When setting a profile as default, the `[default]` section is replaced with the selected profile's credentials plus the custom `profile` and `expiry_date` fields.
102
+
103
+ When creating a standard profile, a new `[name]` section with access key and secret key is appended.
104
+
105
+ When deleting a profile, the `[name]` section and its contents are removed via regex.
106
+
107
+ ---
108
+
109
+ ## `~/.aws/sso/cache/` (Read-only)
110
+
111
+ `switch-profile` reads but does not write to this directory. It is managed entirely by the AWS CLI.
112
+
113
+ ### Purpose
114
+
115
+ Stores SSO session tokens obtained through the `aws sso login` flow.
116
+
117
+ ### File Format
118
+
119
+ Files are named with hash-based identifiers (e.g., `bdc1be3a4f0c3b5e8c0e4d6a1b2c3d4e.json`).
120
+
121
+ ```json
122
+ {
123
+ "startUrl": "https://my-company.awsapps.com/start",
124
+ "region": "us-east-1",
125
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
126
+ "expiresAt": "2021-07-17T11:33:12Z"
127
+ }
128
+ ```
129
+
130
+ ### Fields
131
+
132
+ | Field | Description |
133
+ |-------|-------------|
134
+ | `startUrl` | The SSO portal URL this session belongs to |
135
+ | `region` | The SSO region |
136
+ | `accessToken` | JWT token for the SSO session |
137
+ | `expiresAt` | ISO 8601 expiry timestamp |
138
+
139
+ ### How `switch-profile` Uses It
140
+
141
+ The tool searches all `*.json` files in this directory when it needs to verify or find an SSO session. It matches by comparing the `startUrl` host with the profile's `sso_start_url` host. A session is considered valid if it expires more than 2 minutes from now.
142
+
143
+ ---
144
+
145
+ ## `~/.aws/cli/cache/` (Read-only)
146
+
147
+ `switch-profile` reads but does not write to this directory. It is managed entirely by the AWS CLI.
148
+
149
+ ### Purpose
150
+
151
+ Stores temporary AWS credentials (access key, secret key, session token) obtained through SSO authentication.
152
+
153
+ ### File Format
154
+
155
+ Files are named with hash-based identifiers.
156
+
157
+ ```json
158
+ {
159
+ "ProviderType": "sso",
160
+ "Credentials": {
161
+ "AccessKeyId": "ASIAZOCWXABCD123456",
162
+ "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
163
+ "SessionToken": "AQoDYXdzEJr...",
164
+ "Expiration": "2021-07-17T11:33:12Z"
165
+ }
166
+ }
167
+ ```
168
+
169
+ ### Fields
170
+
171
+ | Field | Description |
172
+ |-------|-------------|
173
+ | `ProviderType` | Credential source type (`"sso"` for SSO) |
174
+ | `Credentials.AccessKeyId` | AWS access key ID |
175
+ | `Credentials.SecretAccessKey` | AWS secret access key |
176
+ | `Credentials.SessionToken` | Temporary session token |
177
+ | `Credentials.Expiration` | ISO 8601 expiry timestamp |
178
+
179
+ ### How `switch-profile` Uses It
180
+
181
+ After running `aws configure list --profile <name>` (which populates this cache), the tool searches for a file where:
182
+
183
+ 1. `ProviderType` is `"sso"`.
184
+ 2. The last 4 characters of `AccessKeyId` match what `aws configure list` reported.
185
+ 3. The last 4 characters of `SecretAccessKey` match what `aws configure list` reported.
186
+ 4. `Expiration` is at least 2 minutes in the future.
187
+
188
+ This matching-by-suffix approach is necessary because `aws configure list` only shows masked keys (e.g., `****3456`), not the full values.
189
+
190
+ ---
191
+
192
+ ## Default File Contents
193
+
194
+ When `switch-profile` creates a standard profile and the AWS config files don't exist yet, it initializes them with these defaults:
195
+
196
+ **`~/.aws/config`:**
197
+ ```ini
198
+ [default]
199
+ region = ap-southeast-1
200
+ output = json
201
+
202
+ ```
203
+
204
+ **`~/.aws/credentials`:**
205
+ ```ini
206
+ [default]
207
+ aws_access_key_id = 1234_DUMMY
208
+ aws_secret_access_key = 4567_DUMMY
209
+
210
+ ```
211
+
212
+ These are dummy values that serve as placeholders until a real profile is set as default.
213
+
214
+ ---
215
+
216
+ ## INI Parsing Approach
217
+
218
+ `switch-profile` does not use a dedicated INI parser. Instead, it uses regex-based string operations:
219
+
220
+ **Reading a section:**
221
+ ```javascript
222
+ // Extract [default] section content (everything until the next [ or end of file)
223
+ const section = fileContent.match(/\[default\]((.|\n|\r)*?)(\[|$)/)[0]
224
+ ```
225
+
226
+ **Extracting a parameter:**
227
+ ```javascript
228
+ // Match " paramName = value" with flexible whitespace
229
+ const regexp = new RegExp(`\\s*${paramName}\\s*=\\s*`)
230
+ const value = params.filter(p => regexp.test(p)).map(p => p.replace(regexp,'').trim())[0]
231
+ ```
232
+
233
+ **Replacing a section:**
234
+ ```javascript
235
+ // Replace the [default] section while preserving the opening bracket of the next section
236
+ const lastChar = defaultSection.slice(-1) // Either '[' or end-of-string
237
+ updatedContent = fileContent.replace(defaultSection, newSection + lastChar)
238
+ ```
239
+
240
+ This approach works for the tool's use case but depends on well-formed config files. It preserves all content outside the targeted section unchanged.
@@ -0,0 +1,168 @@
1
+ # Development Guide
2
+
3
+ This document covers how to set up, develop, test, lint, and release `switch-profile`.
4
+
5
+ ## Setup
6
+
7
+ ```shell
8
+ git clone https://github.com/nicolasdao/switch-profile.git
9
+ cd switch-profile
10
+ npm install
11
+ ```
12
+
13
+ ## Running Locally
14
+
15
+ **Development mode:**
16
+ ```shell
17
+ npm run dev
18
+ # Runs: TZ=UTC NODE_ENV=dev node index.js
19
+ ```
20
+
21
+ **Production mode:**
22
+ ```shell
23
+ npm start
24
+ # Runs: TZ=UTC NODE_ENV=production node index.js
25
+ ```
26
+
27
+ **Direct execution:**
28
+ ```shell
29
+ node index.js
30
+ ```
31
+
32
+ Both modes set `TZ=UTC` to ensure consistent timestamp handling.
33
+
34
+ ## Linting
35
+
36
+ ```shell
37
+ npm run lint
38
+ # Runs: eslint index.js src/ test/ --fix
39
+ ```
40
+
41
+ The ESLint configuration (`.eslintrc.json`) enforces:
42
+
43
+ | Rule | Setting |
44
+ |------|---------|
45
+ | Indentation | Tabs |
46
+ | Quotes | Single |
47
+ | Semicolons | None (never) |
48
+ | Line endings | Unix (`\n`) |
49
+ | Environment | ES6, Node.js (CommonJS) |
50
+ | Console | Allowed |
51
+
52
+ The `--fix` flag auto-fixes style issues when possible.
53
+
54
+ ## Testing
55
+
56
+ ```shell
57
+ npm test
58
+ # Runs: mocha --exit
59
+ ```
60
+
61
+ Tests are located in `test/index.js` and use [Mocha](https://mochajs.org/) as the test runner and [Chai](https://www.chaijs.com/) for assertions.
62
+
63
+ **Current state:** The test suite contains a single placeholder test. There are no functional tests for the AWS operations, profile management, or file I/O.
64
+
65
+ **Test utilities (from comments in test file):**
66
+ - Skip a test: Use `xit` instead of `it`, or `describe.skip` instead of `describe`.
67
+ - Run only one test: Use `it.only` instead of `it`.
68
+
69
+ ## Dependencies
70
+
71
+ ### Production Dependencies
72
+
73
+ | Package | Purpose |
74
+ |---------|---------|
75
+ | `commander` | CLI argument parsing and command registration |
76
+ | `inquirer` | Interactive prompts (list, input, confirm, checkbox) |
77
+ | `inquirer-autocomplete-prompt` | Autocomplete support for region selection |
78
+ | `colors` | Colored terminal output |
79
+ | `puffy` | Error handling (`catchErrors`, `wrapErrors`, `delay`) |
80
+ | `core-async` | Generator-based async flow (co-routines) |
81
+ | `fast-glob` | File pattern matching for cache directory scanning |
82
+ | `rimraf` | Cross-platform recursive directory deletion |
83
+ | `mime-types` | MIME type detection (used by fileHelper, not core functionality) |
84
+ | `archiver` | ZIP creation (imported by fileHelper, unused in current flows) |
85
+ | `tar-stream` | TAR streaming (imported by fileHelper, unused in current flows) |
86
+ | `convert-stream` | Stream conversion (imported by fileHelper, unused in current flows) |
87
+
88
+ ### Dev Dependencies
89
+
90
+ | Package | Purpose |
91
+ |---------|---------|
92
+ | `mocha` | Test runner |
93
+ | `chai` | Assertion library |
94
+ | `eslint` | Code linting |
95
+ | `standard-version` | Automated versioning and changelog generation |
96
+
97
+ ### Unused Dependencies
98
+
99
+ The `fileHelper.js` module is a shared utility library that imports several packages not used by `switch-profile`'s core functionality: `archiver`, `tar-stream`, `convert-stream`, and partially `mime-types`. These are present because `fileHelper.js` provides general-purpose file operations that may be used in other projects sharing this codebase.
100
+
101
+ ## Release Process
102
+
103
+ ### Step 1: Version Bump and Changelog
104
+
105
+ ```shell
106
+ npm run rls -- minor # For new features (0.1.x → 0.2.0)
107
+ npm run rls -- patch # For bug fixes (0.1.2 → 0.1.3)
108
+ npm run rls -- major # For breaking changes (0.x.x → 1.0.0)
109
+ ```
110
+
111
+ This runs `standard-version --release-as <type>`, which:
112
+ 1. Bumps the version in `package.json`.
113
+ 2. Updates `CHANGELOG.md` based on conventional commit messages.
114
+ 3. Creates a git commit with the version bump.
115
+ 4. Creates an annotated git tag (e.g., `v0.1.3`).
116
+
117
+ ### Step 2: Publish
118
+
119
+ ```shell
120
+ npm run push
121
+ # Runs: git push --follow-tags origin master && npm publish --access=public
122
+ ```
123
+
124
+ This:
125
+ 1. Pushes the commit and tag to the `master` branch on GitHub.
126
+ 2. Publishes the package to the npm registry with public access.
127
+
128
+ ### Version Check
129
+
130
+ ```shell
131
+ npm run v
132
+ # Prints the current version from package.json
133
+ ```
134
+
135
+ ## npm Package Configuration
136
+
137
+ | Field | Value |
138
+ |-------|-------|
139
+ | Package name | `switch-profile` |
140
+ | Entry point | `index.js` |
141
+ | Binary | `index.js` (registered via `"bin"` field) |
142
+ | Access | Public |
143
+ | License | BSD-3-Clause |
144
+ | Files excluded from npm | `test/` (via `.npmignore`) |
145
+
146
+ ## Conventional Commits
147
+
148
+ The project uses [standard-version](https://github.com/conventional-commits/standard-version) for automated changelog generation. Commit messages should follow the [Conventional Commits](https://www.conventionalcommits.org/) format:
149
+
150
+ ```
151
+ feat: Add support for profile refresh
152
+ fix: Multiple SSO profiles are not supported
153
+ chore(release): 0.1.2
154
+ ```
155
+
156
+ Prefixes:
157
+ - `feat:` - New features (bumps minor version)
158
+ - `fix:` - Bug fixes (bumps patch version)
159
+ - `chore:` - Maintenance tasks (no version bump unless specified)
160
+
161
+ ## Project Layout Conventions
162
+
163
+ - **No semicolons** in JavaScript files.
164
+ - **Tab indentation** throughout.
165
+ - **Single quotes** for strings.
166
+ - **CommonJS** module system (`require`/`module.exports`), not ES modules.
167
+ - **Error tuple pattern**: Async functions return `[errors, result]` via the `catchErrors` wrapper from `puffy`.
168
+ - **Shebang**: `index.js` starts with `#!/usr/bin/env node` for direct CLI execution.
package/index.js CHANGED
@@ -109,8 +109,38 @@ const createNewProfile = async (profiles, makeItDefault) => {
109
109
  const [profileErrors] = await createProfile({ name, aws_access_key_id, aws_secret_access_key, region })
110
110
  if (profileErrors)
111
111
  return printAWSerrors([new Error('Fail to create profile'), ...profileErrors])
112
- } else
113
- await createSsoProfile(name)
112
+ } else {
113
+ console.log('')
114
+ console.log('┌─────────────────────────────────────────────────────────────────────┐'.cyan)
115
+ console.log('│ SSO Profile Setup Guide │'.cyan)
116
+ console.log('├─────────────────────────────────────────────────────────────────────┤'.cyan)
117
+ console.log('│ │'.cyan)
118
+ console.log('│ You\'re about to run \'aws configure sso\'. Here\'s what to expect: │'.cyan)
119
+ console.log('│ │'.cyan)
120
+ console.log('│ 1. SSO session name'.bold + ' │'.cyan)
121
+ console.log('│ Provide a name (e.g., "my-company-sso"). │'.cyan)
122
+ console.log('│ This enables token reuse across profiles and automatic refresh. │'.cyan)
123
+ console.log('│ Skipping this uses the legacy format (no auto-refresh). │'.cyan)
124
+ console.log('│ │'.cyan)
125
+ console.log('│ 2. SSO start URL'.bold + ' │'.cyan)
126
+ console.log('│ Your AWS SSO portal URL (e.g., https://my-co.awsapps.com/start) │'.cyan)
127
+ console.log('│ Ask your AWS administrator if you don\'t have it. │'.cyan)
128
+ console.log('│ │'.cyan)
129
+ console.log('│ 3. SSO region'.bold + ' │'.cyan)
130
+ console.log('│ The region where your SSO instance is configured. │'.cyan)
131
+ console.log('│ WARNING: This is NOT your deployment region. │'.yellow)
132
+ console.log('│ Using the wrong region causes "invalid_grant" errors. │'.yellow)
133
+ console.log('│ │'.cyan)
134
+ console.log('│ Docs: https://docs.aws.amazon.com/cli/latest/userguide/ │'.cyan)
135
+ console.log('│ cli-configure-sso.html │'.cyan)
136
+ console.log('│ │'.cyan)
137
+ console.log('└─────────────────────────────────────────────────────────────────────┘'.cyan)
138
+ console.log('')
139
+
140
+ const [ssoErrors] = await createSsoProfile(name)
141
+ if (ssoErrors)
142
+ return printAWSerrors([new Error(`Fail to create SSO profile ${name}`), ...ssoErrors])
143
+ }
114
144
 
115
145
  console.log(`New profile ${name.bold} successfully created 🚀`.green)
116
146
 
@@ -302,6 +332,9 @@ const switchCmd = async () => {
302
332
  const setProfileToDefault = async (profileName, profileList, successMsg) => {
303
333
  const profile = profileList.find(p => p.friendlyName == profileName || p.name == profileName)
304
334
 
335
+ if (!profile)
336
+ return printAWSerrors([new Error(`Profile ${profileName.bold} was not found in ~/.aws/config. It may not have been created correctly. Try creating it again.`)], { noStack:true })
337
+
305
338
  // Gets the AWS credentials for a specific profile. If that profile is an SSO profile, this function has a series of
306
339
  // side-effects:
307
340
  // - If the local SSO session stored under ~/.aws/sso/cache has expired, then it will redirect the user to the SSO portal and eventually refresh that ~/.aws/sso/cache.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switch-profile",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "main": "index.js",
5
5
  "bin": "index.js",
6
6
  "description": "NPX script that exposes terminal shortcut to easily switch between cloud configuration (aws, gcp).",
@@ -32,7 +32,7 @@
32
32
  "license": "BSD-3-Clause",
33
33
  "devDependencies": {
34
34
  "chai": "^4.1.2",
35
- "eslint": "^6.8.0",
35
+ "eslint": "^10.0.0",
36
36
  "mocha": "^8.2.1",
37
37
  "standard-version": "^9.3.0"
38
38
  },
package/src/aws/index.js CHANGED
@@ -510,10 +510,12 @@ const createProfile = ({ name, aws_access_key_id, aws_secret_access_key, region
510
510
 
511
511
  const createSsoProfile = name => catchErrors((async () => {
512
512
  await awsCliV2Exists()
513
- await new Promise(next => {
513
+ const exitCode = await new Promise(next => {
514
514
  const child = spawn('aws', ['configure', 'sso', '--profile', name],{ stdio: 'inherit' })
515
- child.on('exit', next)
515
+ child.on('exit', code => next(code))
516
516
  })
517
+ if (exitCode !== 0)
518
+ throw new Error(`'aws configure sso' exited with code ${exitCode}. The SSO profile may not have been created correctly.`)
517
519
  })())
518
520
 
519
521
  module.exports = {