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 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' // 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)
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, uses `sso_region` as the effective region.
55
- 6. Builds a `friendlyName` that includes role and account info for SSO profiles.
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 which to delete: ',
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
- return {
290
- name: `${i+1}. ${p.friendlyName}`,
291
- value: p.name
292
- }
293
- })
289
+ return {
290
+ name: `${i+1}. ${p.friendlyName}`,
291
+ value: p.name
292
+ }
293
+ })
294
294
  }
295
295
  ])
296
296
 
297
- if (!delProfiles.length)
298
- return
297
+ if (!delProfiles.length)
298
+ return
299
299
 
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
- ])
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
- if (!delConfirm)
311
- return
310
+ if (!delConfirm)
311
+ return
312
312
 
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 })
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
- const [delErrors] = await deleteProfiles(delProfiles)
317
- if (delErrors)
318
- return printAWSerrors([new Error(`Fail to delete ${label}`), ...delErrors])
317
+ const [delErrors] = await deleteProfiles(delProfiles)
318
+ if (delErrors)
319
+ return printAWSerrors([new Error(`Fail to delete ${label}`), ...delErrors])
319
320
 
320
- console.log(`AWS profile${delProfiles.length > 1 ? 's' : ''} successfully deleted.`.green)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switch-profile",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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).",
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