switch-profile 0.2.1 → 0.2.3
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/CHANGELOG.md +14 -0
- package/docs/aws-profile-management.md +21 -4
- package/index.js +29 -28
- package/package.json +1 -1
- package/src/aws/index.js +45 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.2.3](https://github.com/nicolasdao/switch-profile/compare/v0.2.2...v0.2.3) (2026-02-11)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* Add support for AWS CLI v2 SSO session format and improve profile deletion UX ([4eb7366](https://github.com/nicolasdao/switch-profile/commit/4eb7366d6ed1f50f024128d94da213515a4ff9da))
|
|
11
|
+
|
|
12
|
+
### [0.2.2](https://github.com/nicolasdao/switch-profile/compare/v0.2.1...v0.2.2) (2026-02-10)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* SSO profile credentials written as undefined by falling back to aws configure export-credentials ([7067eff](https://github.com/nicolasdao/switch-profile/commit/7067eff15d376a3dd902ee4a03f5db7cfa539918))
|
|
18
|
+
|
|
5
19
|
### [0.2.1](https://github.com/nicolasdao/switch-profile/compare/v0.2.0...v0.2.1) (2026-02-08)
|
|
6
20
|
|
|
7
21
|
## [0.2.0](https://github.com/nicolasdao/switch-profile/compare/v0.1.2...v0.2.0) (2026-02-08)
|
|
@@ -42,7 +42,8 @@ Each profile object contains:
|
|
|
42
42
|
sso_start_url: 'https://my.awsapps.com/start', // null if not SSO
|
|
43
43
|
sso_region: 'us-east-1', // null if not SSO
|
|
44
44
|
sso_account_id: '123456789012', // null if not SSO
|
|
45
|
-
sso_role_name: 'Admin'
|
|
45
|
+
sso_role_name: 'Admin', // null if not SSO
|
|
46
|
+
sso_session: 'my-session' // null if not using SSO sessions (legacy SSO format)
|
|
46
47
|
}
|
|
47
48
|
```
|
|
48
49
|
|
|
@@ -51,8 +52,9 @@ Each profile object contains:
|
|
|
51
52
|
2. For each section, extracts the content between it and the next `[` bracket.
|
|
52
53
|
3. Splits content by newline and extracts key-value pairs using `getParam()`.
|
|
53
54
|
4. Strips `[profile ...]` prefix to get the profile name.
|
|
54
|
-
5. For SSO profiles
|
|
55
|
-
6.
|
|
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.
|
|
56
58
|
|
|
57
59
|
#### `getDefaultProfile()`
|
|
58
60
|
|
|
@@ -81,8 +83,10 @@ The main credential retrieval function. Behaves differently for SSO vs. standard
|
|
|
81
83
|
**For SSO profiles** (when `ssoUrl` is provided):
|
|
82
84
|
|
|
83
85
|
1. Calls `refreshSsoSession()` to ensure a valid SSO session exists.
|
|
84
|
-
2. Calls `getSsoCredentials()` to get temporary AWS credentials.
|
|
86
|
+
2. Calls `getSsoCredentials()` to get temporary AWS credentials via the CLI cache.
|
|
85
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.
|
|
86
90
|
|
|
87
91
|
**For standard profiles** (when `ssoUrl` is null):
|
|
88
92
|
|
|
@@ -284,6 +288,19 @@ User selects an SSO profile
|
|
|
284
288
|
Search ~/.aws/cli/cache/*.json
|
|
285
289
|
for matching credentials
|
|
286
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
|
+
│
|
|
287
304
|
▼
|
|
288
305
|
Return { aws_access_key_id,
|
|
289
306
|
aws_secret_access_key,
|
package/index.js
CHANGED
|
@@ -280,44 +280,45 @@ const switchCmd = async () => {
|
|
|
280
280
|
await setProfileToDefault(defaultProfileName, profiles, `AWS profile ${defaultProfileName.bold} successfully refreshed.`)
|
|
281
281
|
else if (option == 'delete') {
|
|
282
282
|
const { delProfiles } = await inquirer.prompt([
|
|
283
|
-
{
|
|
284
|
-
type: 'checkbox',
|
|
285
|
-
name: 'delProfiles',
|
|
286
|
-
message: 'Select the profiles you
|
|
283
|
+
{
|
|
284
|
+
type: 'checkbox',
|
|
285
|
+
name: 'delProfiles',
|
|
286
|
+
message: 'Select the profiles you wish to delete (SPACE to select, ENTER to confirm):',
|
|
287
287
|
pageSize: 20,
|
|
288
288
|
choices: profiles.map((p,i) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
289
|
+
return {
|
|
290
|
+
name: `${i+1}. ${p.friendlyName}`,
|
|
291
|
+
value: p.name
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
294
|
}
|
|
295
295
|
])
|
|
296
296
|
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
if (!delProfiles.length)
|
|
298
|
+
return
|
|
299
299
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
300
|
+
const label = delProfiles.length > 1 ? 'profiles' : 'profile'
|
|
301
|
+
const labelText = delProfiles.length > 1 ? `those ${delProfiles.length} profiles` : 'this profile'
|
|
302
|
+
const { delConfirm } = await inquirer.prompt([
|
|
303
|
+
{
|
|
304
|
+
type: 'confirm',
|
|
305
|
+
name: 'delConfirm',
|
|
306
|
+
message: `Are you sure you want to delete ${labelText}? `,
|
|
307
|
+
}
|
|
308
|
+
])
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
if (!delConfirm)
|
|
311
|
+
return
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
|
|
313
|
+
if (defaultProfileName && delProfiles.some(p => p == defaultProfileName)) {
|
|
314
|
+
return printAWSerrors([new Error(`Fail to delete ${label}. Profile ${defaultProfileName.bold} is the current default. Set another profile as the default, then try deleting again.`)], { noStack:true })
|
|
315
|
+
}
|
|
315
316
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
const [delErrors] = await deleteProfiles(delProfiles)
|
|
318
|
+
if (delErrors)
|
|
319
|
+
return printAWSerrors([new Error(`Fail to delete ${label}`), ...delErrors])
|
|
319
320
|
|
|
320
|
-
|
|
321
|
+
console.log(`AWS profile${delProfiles.length > 1 ? 's' : ''} successfully deleted.`.green)
|
|
321
322
|
} else if (option == 'new')
|
|
322
323
|
await createNewProfile(profiles)
|
|
323
324
|
else if (option == ABORT_KEY)
|
package/package.json
CHANGED
package/src/aws/index.js
CHANGED
|
@@ -107,17 +107,33 @@ const listProfiles = () => catchErrors((async () => {
|
|
|
107
107
|
sso_region: getParam(params, 'sso_region'),
|
|
108
108
|
sso_account_id: getParam(params, 'sso_account_id'),
|
|
109
109
|
sso_role_name: getParam(params, 'sso_role_name'),
|
|
110
|
+
sso_session: getParam(params, 'sso_session'),
|
|
110
111
|
region: getParam(params, 'region'),
|
|
111
112
|
output: getParam(params, 'output')
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// Handle newer AWS CLI v2 SSO session format where sso_start_url is in a separate [sso-session] section
|
|
116
|
+
if (p.sso_session && !p.sso_start_url) {
|
|
117
|
+
const ssoSessionSection = `[sso-session ${p.sso_session}]`
|
|
118
|
+
const ssoSessionMatch = configStr.split(ssoSessionSection)
|
|
119
|
+
if (ssoSessionMatch.length > 1) {
|
|
120
|
+
const [ssoSessionRest=''] = ssoSessionMatch[1].split('[')
|
|
121
|
+
const ssoSessionParams = ssoSessionRest.split(EOL)
|
|
122
|
+
p.sso_start_url = getParam(ssoSessionParams, 'sso_start_url')
|
|
123
|
+
// If sso_region is not in profile, check the session section
|
|
124
|
+
if (!p.sso_region) {
|
|
125
|
+
p.sso_region = getParam(ssoSessionParams, 'sso_region')
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
if (p.sso_region)
|
|
115
131
|
p.region = p.sso_region
|
|
116
132
|
|
|
117
133
|
p.friendlyName = `${p.name}${p.sso_start_url ? ` (SSO [role:${p.sso_role_name||'unknown'} - account:${p.sso_account_id||'unknown'}])` : ''}`
|
|
118
134
|
|
|
119
|
-
return p
|
|
120
|
-
}).filter(p => p.name != 'default')
|
|
135
|
+
return p
|
|
136
|
+
}).filter(p => p.name != 'default' && !p.name.startsWith('sso-session '))
|
|
121
137
|
})())
|
|
122
138
|
|
|
123
139
|
/**
|
|
@@ -337,6 +353,26 @@ const getCredentials = (profile, ssoUrl) => catchErrors((async () => {
|
|
|
337
353
|
throw wrapErrors(errMsg, ssoCredsErrors)
|
|
338
354
|
}
|
|
339
355
|
|
|
356
|
+
if (!ssoCreds) {
|
|
357
|
+
try {
|
|
358
|
+
const data = await exec(`aws configure export-credentials --profile ${profile} --format process`)
|
|
359
|
+
const parsed = JSON.parse(data)
|
|
360
|
+
if (parsed.AccessKeyId && parsed.SecretAccessKey) {
|
|
361
|
+
ssoCreds = {
|
|
362
|
+
aws_access_key_id: parsed.AccessKeyId,
|
|
363
|
+
aws_secret_access_key: parsed.SecretAccessKey,
|
|
364
|
+
aws_session_token: parsed.SessionToken,
|
|
365
|
+
expiry_date: parsed.Expiration ? new Date(parsed.Expiration) : null
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch(e) {
|
|
369
|
+
// Fallback also failed
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!ssoCreds)
|
|
374
|
+
throw new Error(errMsg)
|
|
375
|
+
|
|
340
376
|
return ssoCreds
|
|
341
377
|
} else {
|
|
342
378
|
const [errors, credsStr] = await getCredsFile()
|
|
@@ -344,6 +380,9 @@ const getCredentials = (profile, ssoUrl) => catchErrors((async () => {
|
|
|
344
380
|
throw wrapErrors(errMsg, errors)
|
|
345
381
|
|
|
346
382
|
const [, rest] = credsStr.split(`[${profile}]`)
|
|
383
|
+
if (!rest)
|
|
384
|
+
throw new Error(`Profile ${profile} not found in ${AWS_CREDS_FILE}. Standard profiles must have credentials defined in this file.`)
|
|
385
|
+
|
|
347
386
|
const [config=''] = rest.split('[')
|
|
348
387
|
const params = config.split(EOL)
|
|
349
388
|
const creds = {
|
|
@@ -352,7 +391,7 @@ const getCredentials = (profile, ssoUrl) => catchErrors((async () => {
|
|
|
352
391
|
aws_session_token: getParam(params, 'aws_session_token'),
|
|
353
392
|
expiry_date: null
|
|
354
393
|
}
|
|
355
|
-
|
|
394
|
+
|
|
356
395
|
return creds
|
|
357
396
|
}
|
|
358
397
|
})())
|
|
@@ -419,7 +458,7 @@ const deleteProfileFromConfig = (profile, fileContent) => {
|
|
|
419
458
|
fileContent = fileContent || ''
|
|
420
459
|
const regExp = new RegExp(`\\[(profile\\s){0,1}${profile}\\]((.|\\n|\\r)*?)(\\[|$)`)
|
|
421
460
|
const profileMatch = (fileContent.match(regExp)||[])[0] || ''
|
|
422
|
-
|
|
461
|
+
|
|
423
462
|
if (!profileMatch)
|
|
424
463
|
return fileContent
|
|
425
464
|
|
|
@@ -431,7 +470,7 @@ const deleteProfileFromCreds = (profile, fileContent) => {
|
|
|
431
470
|
fileContent = fileContent || ''
|
|
432
471
|
const regExp = new RegExp(`\\[${profile}\\]((.|\\n|\\r)*?)(\\[|$)`)
|
|
433
472
|
const profileMatch = (fileContent.match(regExp)||[])[0] || ''
|
|
434
|
-
|
|
473
|
+
|
|
435
474
|
if (!profileMatch)
|
|
436
475
|
return fileContent
|
|
437
476
|
|
|
@@ -441,7 +480,7 @@ const deleteProfileFromCreds = (profile, fileContent) => {
|
|
|
441
480
|
|
|
442
481
|
const deleteProfiles = profiles => catchErrors((async () => {
|
|
443
482
|
if (!profiles || !profiles.length)
|
|
444
|
-
return
|
|
483
|
+
return
|
|
445
484
|
|
|
446
485
|
const errMsg = 'Fail to delete AWS profiles'
|
|
447
486
|
|