devvami 1.2.0 → 1.4.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 CHANGED
@@ -114,8 +114,19 @@ dvmi tasks assigned # Show tasks assigned to you
114
114
 
115
115
  ```bash
116
116
  dvmi costs get # Analyze AWS costs
117
+ dvmi costs trend # Show 2-month daily AWS cost trend
117
118
  ```
118
119
 
120
+ If `awsProfile` is configured (`dvmi init`), AWS cost commands automatically re-run via
121
+ `aws-vault exec <profile> -- ...` when credentials are missing, so developers can run:
122
+
123
+ ```bash
124
+ dvmi costs get
125
+ dvmi costs trend
126
+ ```
127
+
128
+ without manually prefixing `aws-vault exec`.
129
+
119
130
  ### Documentation
120
131
 
121
132
  ```bash
@@ -164,12 +175,36 @@ Devvami uses your system's **secure credential storage**:
164
175
 
165
176
  - **macOS**: Keychain
166
177
  - **Linux**: Secret Service / pass
167
- - **Windows**: Credential Manager
178
+ - **WSL2**: Windows bridge for browser/GCM + Linux tooling for security setup
179
+ - **Windows (native / non-WSL)**: limited support (see Platform Support)
168
180
 
169
181
  Tokens are **never stored in plain text**. They're stored securely via `@keytar/keytar`.
170
182
 
171
183
  ---
172
184
 
185
+ ## 🖥️ Platform Support
186
+
187
+ ### Fully supported
188
+
189
+ - **macOS**
190
+ - **Linux (Debian/Ubuntu family)**
191
+ - **Windows via WSL2**
192
+
193
+ ### Linux/WSL notes
194
+
195
+ - `dvmi security setup` currently uses `apt-get` for package install (Debian/Ubuntu oriented).
196
+ - `dvmi security setup` requires authenticated `sudo` (`sudo -n true` must pass).
197
+ - On WSL2, browser opening tries `wslview` first, then falls back to `xdg-open`.
198
+
199
+ ### Windows native (non-WSL)
200
+
201
+ - Not fully supported today.
202
+ - Platform detection does not handle `win32` explicitly yet.
203
+ - Some shell assumptions are Unix-centric (for example `which` usage and security setup steps).
204
+ - Recommended path on Windows is to use **WSL2**.
205
+
206
+ ---
207
+
173
208
  ## 📚 Documentation
174
209
 
175
210
  - **Setup**: See [Quick Start](#-quick-start) above
@@ -384,16 +384,20 @@
384
384
  "aliases": [],
385
385
  "args": {
386
386
  "service": {
387
- "description": "Nome del servizio",
387
+ "description": "Service name (used to derive tag filter from config)",
388
388
  "name": "service",
389
- "required": true
389
+ "required": false
390
390
  }
391
391
  },
392
- "description": "Stima costi AWS per un servizio (via Cost Explorer API)",
392
+ "description": "Get AWS costs for a service, grouped by service, tag, or both",
393
393
  "examples": [
394
+ "<%= config.bin %> costs get",
394
395
  "<%= config.bin %> costs get my-service",
395
- "<%= config.bin %> costs get my-api --period mtd",
396
- "<%= config.bin %> costs get my-service --json"
396
+ "<%= config.bin %> costs get --period mtd",
397
+ "<%= config.bin %> costs get --period last-week",
398
+ "<%= config.bin %> costs get --group-by tag --tag-key env",
399
+ "<%= config.bin %> costs get my-service --group-by both --tag-key env",
400
+ "<%= config.bin %> costs get --group-by tag --tag-key env --json"
397
401
  ],
398
402
  "flags": {
399
403
  "json": {
@@ -404,7 +408,7 @@
404
408
  "type": "boolean"
405
409
  },
406
410
  "period": {
407
- "description": "Periodo: last-month, last-week, mtd",
411
+ "description": "Time period: last-month, last-week, mtd",
408
412
  "name": "period",
409
413
  "default": "last-month",
410
414
  "hasDynamicHelp": false,
@@ -415,6 +419,26 @@
415
419
  "mtd"
416
420
  ],
417
421
  "type": "option"
422
+ },
423
+ "group-by": {
424
+ "description": "Grouping dimension: service, tag, or both",
425
+ "name": "group-by",
426
+ "default": "service",
427
+ "hasDynamicHelp": false,
428
+ "multiple": false,
429
+ "options": [
430
+ "service",
431
+ "tag",
432
+ "both"
433
+ ],
434
+ "type": "option"
435
+ },
436
+ "tag-key": {
437
+ "description": "Tag key for grouping when --group-by tag or both",
438
+ "name": "tag-key",
439
+ "hasDynamicHelp": false,
440
+ "multiple": false,
441
+ "type": "option"
418
442
  }
419
443
  },
420
444
  "hasDynamicHelp": false,
@@ -433,6 +457,69 @@
433
457
  "get.js"
434
458
  ]
435
459
  },
460
+ "costs:trend": {
461
+ "aliases": [],
462
+ "args": {},
463
+ "description": "Show a rolling 2-month daily cost trend chart",
464
+ "examples": [
465
+ "<%= config.bin %> costs trend",
466
+ "<%= config.bin %> costs trend --line",
467
+ "<%= config.bin %> costs trend --group-by tag --tag-key env",
468
+ "<%= config.bin %> costs trend --group-by both --tag-key env",
469
+ "<%= config.bin %> costs trend --group-by tag --tag-key env --line",
470
+ "<%= config.bin %> costs trend --json"
471
+ ],
472
+ "flags": {
473
+ "json": {
474
+ "description": "Format output as json.",
475
+ "helpGroup": "GLOBAL",
476
+ "name": "json",
477
+ "allowNo": false,
478
+ "type": "boolean"
479
+ },
480
+ "group-by": {
481
+ "description": "Grouping dimension: service, tag, or both",
482
+ "name": "group-by",
483
+ "default": "service",
484
+ "hasDynamicHelp": false,
485
+ "multiple": false,
486
+ "options": [
487
+ "service",
488
+ "tag",
489
+ "both"
490
+ ],
491
+ "type": "option"
492
+ },
493
+ "tag-key": {
494
+ "description": "Tag key for grouping when --group-by tag or both",
495
+ "name": "tag-key",
496
+ "hasDynamicHelp": false,
497
+ "multiple": false,
498
+ "type": "option"
499
+ },
500
+ "line": {
501
+ "description": "Render as line chart instead of default bar chart",
502
+ "name": "line",
503
+ "allowNo": false,
504
+ "type": "boolean"
505
+ }
506
+ },
507
+ "hasDynamicHelp": false,
508
+ "hiddenAliases": [],
509
+ "id": "costs:trend",
510
+ "pluginAlias": "devvami",
511
+ "pluginName": "devvami",
512
+ "pluginType": "core",
513
+ "strict": true,
514
+ "enableJsonFlag": true,
515
+ "isESM": true,
516
+ "relativePath": [
517
+ "src",
518
+ "commands",
519
+ "costs",
520
+ "trend.js"
521
+ ]
522
+ },
436
523
  "create:repo": {
437
524
  "aliases": [],
438
525
  "args": {
@@ -726,6 +813,292 @@
726
813
  "search.js"
727
814
  ]
728
815
  },
816
+ "dotfiles:add": {
817
+ "aliases": [],
818
+ "args": {
819
+ "files": {
820
+ "description": "File paths to add",
821
+ "name": "files",
822
+ "required": false
823
+ }
824
+ },
825
+ "description": "Add dotfiles to chezmoi management with automatic encryption for sensitive files",
826
+ "examples": [
827
+ "<%= config.bin %> dotfiles add",
828
+ "<%= config.bin %> dotfiles add ~/.zshrc",
829
+ "<%= config.bin %> dotfiles add ~/.zshrc ~/.gitconfig",
830
+ "<%= config.bin %> dotfiles add ~/.ssh/id_ed25519 --encrypt",
831
+ "<%= config.bin %> dotfiles add --json ~/.zshrc"
832
+ ],
833
+ "flags": {
834
+ "json": {
835
+ "description": "Format output as json.",
836
+ "helpGroup": "GLOBAL",
837
+ "name": "json",
838
+ "allowNo": false,
839
+ "type": "boolean"
840
+ },
841
+ "help": {
842
+ "char": "h",
843
+ "description": "Show CLI help.",
844
+ "name": "help",
845
+ "allowNo": false,
846
+ "type": "boolean"
847
+ },
848
+ "encrypt": {
849
+ "char": "e",
850
+ "description": "Force encryption for all files being added",
851
+ "name": "encrypt",
852
+ "allowNo": false,
853
+ "type": "boolean"
854
+ },
855
+ "no-encrypt": {
856
+ "description": "Disable auto-encryption (add all as plaintext)",
857
+ "name": "no-encrypt",
858
+ "allowNo": false,
859
+ "type": "boolean"
860
+ }
861
+ },
862
+ "hasDynamicHelp": false,
863
+ "hiddenAliases": [],
864
+ "id": "dotfiles:add",
865
+ "pluginAlias": "devvami",
866
+ "pluginName": "devvami",
867
+ "pluginType": "core",
868
+ "strict": false,
869
+ "enableJsonFlag": true,
870
+ "isESM": true,
871
+ "relativePath": [
872
+ "src",
873
+ "commands",
874
+ "dotfiles",
875
+ "add.js"
876
+ ]
877
+ },
878
+ "dotfiles:setup": {
879
+ "aliases": [],
880
+ "args": {},
881
+ "description": "Interactive wizard to configure chezmoi with age encryption for dotfile management",
882
+ "examples": [
883
+ "<%= config.bin %> dotfiles setup",
884
+ "<%= config.bin %> dotfiles setup --json"
885
+ ],
886
+ "flags": {
887
+ "json": {
888
+ "description": "Format output as json.",
889
+ "helpGroup": "GLOBAL",
890
+ "name": "json",
891
+ "allowNo": false,
892
+ "type": "boolean"
893
+ },
894
+ "help": {
895
+ "char": "h",
896
+ "description": "Show CLI help.",
897
+ "name": "help",
898
+ "allowNo": false,
899
+ "type": "boolean"
900
+ }
901
+ },
902
+ "hasDynamicHelp": false,
903
+ "hiddenAliases": [],
904
+ "id": "dotfiles:setup",
905
+ "pluginAlias": "devvami",
906
+ "pluginName": "devvami",
907
+ "pluginType": "core",
908
+ "strict": true,
909
+ "enableJsonFlag": true,
910
+ "isESM": true,
911
+ "relativePath": [
912
+ "src",
913
+ "commands",
914
+ "dotfiles",
915
+ "setup.js"
916
+ ]
917
+ },
918
+ "dotfiles:status": {
919
+ "aliases": [],
920
+ "args": {},
921
+ "description": "Show chezmoi dotfiles status: managed files, encryption state, and sync health",
922
+ "examples": [
923
+ "<%= config.bin %> dotfiles status",
924
+ "<%= config.bin %> dotfiles status --json"
925
+ ],
926
+ "flags": {
927
+ "json": {
928
+ "description": "Format output as json.",
929
+ "helpGroup": "GLOBAL",
930
+ "name": "json",
931
+ "allowNo": false,
932
+ "type": "boolean"
933
+ },
934
+ "help": {
935
+ "char": "h",
936
+ "description": "Show CLI help.",
937
+ "name": "help",
938
+ "allowNo": false,
939
+ "type": "boolean"
940
+ }
941
+ },
942
+ "hasDynamicHelp": false,
943
+ "hiddenAliases": [],
944
+ "id": "dotfiles:status",
945
+ "pluginAlias": "devvami",
946
+ "pluginName": "devvami",
947
+ "pluginType": "core",
948
+ "strict": true,
949
+ "enableJsonFlag": true,
950
+ "isESM": true,
951
+ "relativePath": [
952
+ "src",
953
+ "commands",
954
+ "dotfiles",
955
+ "status.js"
956
+ ]
957
+ },
958
+ "dotfiles:sync": {
959
+ "aliases": [],
960
+ "args": {
961
+ "repo": {
962
+ "description": "Remote repository URL (for initial remote setup)",
963
+ "name": "repo",
964
+ "required": false
965
+ }
966
+ },
967
+ "description": "Sync dotfiles with remote repository: push local changes or pull from remote",
968
+ "examples": [
969
+ "<%= config.bin %> dotfiles sync",
970
+ "<%= config.bin %> dotfiles sync --push",
971
+ "<%= config.bin %> dotfiles sync --pull",
972
+ "<%= config.bin %> dotfiles sync --pull git@github.com:user/dotfiles.git",
973
+ "<%= config.bin %> dotfiles sync --dry-run --push",
974
+ "<%= config.bin %> dotfiles sync --json"
975
+ ],
976
+ "flags": {
977
+ "json": {
978
+ "description": "Format output as json.",
979
+ "helpGroup": "GLOBAL",
980
+ "name": "json",
981
+ "allowNo": false,
982
+ "type": "boolean"
983
+ },
984
+ "help": {
985
+ "char": "h",
986
+ "description": "Show CLI help.",
987
+ "name": "help",
988
+ "allowNo": false,
989
+ "type": "boolean"
990
+ },
991
+ "push": {
992
+ "description": "Push local changes to remote",
993
+ "name": "push",
994
+ "allowNo": false,
995
+ "type": "boolean"
996
+ },
997
+ "pull": {
998
+ "description": "Pull remote changes and apply",
999
+ "name": "pull",
1000
+ "allowNo": false,
1001
+ "type": "boolean"
1002
+ },
1003
+ "dry-run": {
1004
+ "description": "Show what would change without applying",
1005
+ "name": "dry-run",
1006
+ "allowNo": false,
1007
+ "type": "boolean"
1008
+ }
1009
+ },
1010
+ "hasDynamicHelp": false,
1011
+ "hiddenAliases": [],
1012
+ "id": "dotfiles:sync",
1013
+ "pluginAlias": "devvami",
1014
+ "pluginName": "devvami",
1015
+ "pluginType": "core",
1016
+ "strict": true,
1017
+ "enableJsonFlag": true,
1018
+ "isESM": true,
1019
+ "relativePath": [
1020
+ "src",
1021
+ "commands",
1022
+ "dotfiles",
1023
+ "sync.js"
1024
+ ]
1025
+ },
1026
+ "logs": {
1027
+ "aliases": [],
1028
+ "args": {},
1029
+ "description": "Browse and query CloudWatch log groups interactively",
1030
+ "examples": [
1031
+ "<%= config.bin %> logs",
1032
+ "<%= config.bin %> logs --group /aws/lambda/my-fn",
1033
+ "<%= config.bin %> logs --group /aws/lambda/my-fn --filter \"ERROR\" --since 24h",
1034
+ "<%= config.bin %> logs --group /aws/lambda/my-fn --limit 50 --json"
1035
+ ],
1036
+ "flags": {
1037
+ "json": {
1038
+ "description": "Format output as json.",
1039
+ "helpGroup": "GLOBAL",
1040
+ "name": "json",
1041
+ "allowNo": false,
1042
+ "type": "boolean"
1043
+ },
1044
+ "group": {
1045
+ "char": "g",
1046
+ "description": "Log group name — bypasses interactive picker",
1047
+ "name": "group",
1048
+ "hasDynamicHelp": false,
1049
+ "multiple": false,
1050
+ "type": "option"
1051
+ },
1052
+ "filter": {
1053
+ "char": "f",
1054
+ "description": "CloudWatch filter pattern (empty = all events)",
1055
+ "name": "filter",
1056
+ "default": "",
1057
+ "hasDynamicHelp": false,
1058
+ "multiple": false,
1059
+ "type": "option"
1060
+ },
1061
+ "since": {
1062
+ "description": "Time window: 1h, 24h, 7d",
1063
+ "name": "since",
1064
+ "default": "1h",
1065
+ "hasDynamicHelp": false,
1066
+ "multiple": false,
1067
+ "type": "option"
1068
+ },
1069
+ "limit": {
1070
+ "description": "Max log events to return (1–10000)",
1071
+ "name": "limit",
1072
+ "default": 100,
1073
+ "hasDynamicHelp": false,
1074
+ "multiple": false,
1075
+ "type": "option"
1076
+ },
1077
+ "region": {
1078
+ "char": "r",
1079
+ "description": "AWS region (defaults to project config awsRegion)",
1080
+ "name": "region",
1081
+ "hasDynamicHelp": false,
1082
+ "multiple": false,
1083
+ "type": "option"
1084
+ }
1085
+ },
1086
+ "hasDynamicHelp": false,
1087
+ "hiddenAliases": [],
1088
+ "id": "logs",
1089
+ "pluginAlias": "devvami",
1090
+ "pluginName": "devvami",
1091
+ "pluginType": "core",
1092
+ "strict": true,
1093
+ "enableJsonFlag": true,
1094
+ "isESM": true,
1095
+ "relativePath": [
1096
+ "src",
1097
+ "commands",
1098
+ "logs",
1099
+ "index.js"
1100
+ ]
1101
+ },
729
1102
  "pipeline:logs": {
730
1103
  "aliases": [],
731
1104
  "args": {
@@ -1561,5 +1934,5 @@
1561
1934
  ]
1562
1935
  }
1563
1936
  },
1564
- "version": "1.2.0"
1937
+ "version": "1.4.0"
1565
1938
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devvami",
3
3
  "description": "DevEx CLI for developers and teams — manage repos, PRs, pipelines, tasks, and costs from the terminal",
4
- "version": "1.2.0",
4
+ "version": "1.4.0",
5
5
  "author": "",
6
6
  "type": "module",
7
7
  "bin": {
@@ -83,6 +83,7 @@
83
83
  }
84
84
  },
85
85
  "dependencies": {
86
+ "@aws-sdk/client-cloudwatch-logs": "^3.1018.0",
86
87
  "@aws-sdk/client-cost-explorer": "^3",
87
88
  "@inquirer/prompts": "^7",
88
89
  "@oclif/core": "^4",
@@ -1,49 +1,122 @@
1
1
  import { Command, Args, Flags } from '@oclif/core'
2
+ import { input } from '@inquirer/prompts'
2
3
  import ora from 'ora'
3
4
  import { getServiceCosts } from '../../services/aws-costs.js'
4
5
  import { loadConfig } from '../../services/config.js'
5
6
  import { formatCostTable, calculateTotal } from '../../formatters/cost.js'
7
+ import { DvmiError } from '../../utils/errors.js'
8
+ import {
9
+ awsVaultPrefix,
10
+ isAwsVaultSession,
11
+ reexecCurrentCommandWithAwsVault,
12
+ reexecCurrentCommandWithAwsVaultProfile,
13
+ } from '../../utils/aws-vault.js'
6
14
 
7
15
  export default class CostsGet extends Command {
8
- static description = 'Stima costi AWS per un servizio (via Cost Explorer API)'
16
+ static description = 'Get AWS costs for a service, grouped by service, tag, or both'
9
17
 
10
18
  static examples = [
19
+ '<%= config.bin %> costs get',
11
20
  '<%= config.bin %> costs get my-service',
12
- '<%= config.bin %> costs get my-api --period mtd',
13
- '<%= config.bin %> costs get my-service --json',
21
+ '<%= config.bin %> costs get --period mtd',
22
+ '<%= config.bin %> costs get --period last-week',
23
+ '<%= config.bin %> costs get --group-by tag --tag-key env',
24
+ '<%= config.bin %> costs get my-service --group-by both --tag-key env',
25
+ '<%= config.bin %> costs get --group-by tag --tag-key env --json',
14
26
  ]
15
27
 
16
28
  static enableJsonFlag = true
17
29
 
18
30
  static args = {
19
- service: Args.string({ description: 'Nome del servizio', required: true }),
31
+ service: Args.string({ description: 'Service name (used to derive tag filter from config)', required: false }),
20
32
  }
21
33
 
22
34
  static flags = {
23
35
  period: Flags.string({
24
- description: 'Periodo: last-month, last-week, mtd',
36
+ description: 'Time period: last-month, last-week, mtd',
25
37
  default: 'last-month',
26
38
  options: ['last-month', 'last-week', 'mtd'],
27
39
  }),
40
+ 'group-by': Flags.string({
41
+ description: 'Grouping dimension: service, tag, or both',
42
+ default: 'service',
43
+ options: ['service', 'tag', 'both'],
44
+ }),
45
+ 'tag-key': Flags.string({
46
+ description: 'Tag key for grouping when --group-by tag or both',
47
+ }),
28
48
  }
29
49
 
30
50
  async run() {
31
51
  const { args, flags } = await this.parse(CostsGet)
32
52
  const isJson = flags.json
53
+ const isInteractive = !isJson && process.stdout.isTTY && process.env.CI !== 'true'
54
+ const groupBy = /** @type {'service'|'tag'|'both'} */ (flags['group-by'])
33
55
 
34
- const spinner = isJson ? null : ora(`Fetching costs for ${args.service}...`).start()
35
-
36
- // Get project tags from config
37
56
  const config = await loadConfig()
38
- const tags = config.projectTags ?? { project: args.service }
57
+
58
+ if (
59
+ isInteractive &&
60
+ !isAwsVaultSession() &&
61
+ process.env.DVMI_AWS_VAULT_REEXEC !== '1'
62
+ ) {
63
+ const profile = await input({
64
+ message: 'AWS profile (aws-vault):',
65
+ default: config.awsProfile || process.env.AWS_VAULT || 'default',
66
+ })
67
+
68
+ const selected = profile.trim()
69
+ if (!selected) {
70
+ this.error('AWS profile is required to run this command.')
71
+ }
72
+
73
+ const promptedReexecExitCode = await reexecCurrentCommandWithAwsVaultProfile(selected)
74
+ if (promptedReexecExitCode !== null) {
75
+ this.exit(promptedReexecExitCode)
76
+ return
77
+ }
78
+ }
79
+
80
+ // Transparent aws-vault usage: if a profile is configured and no AWS creds are present,
81
+ // re-run this exact command via `aws-vault exec <profile> -- ...`.
82
+ const reexecExitCode = await reexecCurrentCommandWithAwsVault(config)
83
+ if (reexecExitCode !== null) {
84
+ this.exit(reexecExitCode)
85
+ return
86
+ }
87
+
88
+ // Resolve tag key: explicit flag → first key in config projectTags
89
+ const configTagKey = config.projectTags ? Object.keys(config.projectTags)[0] : undefined
90
+ const tagKey = flags['tag-key'] ?? configTagKey
91
+
92
+ // Validate: tag key required when grouping by tag or both
93
+ if ((groupBy === 'tag' || groupBy === 'both') && !tagKey) {
94
+ throw new DvmiError(
95
+ 'No tag key available.',
96
+ 'Pass --tag-key or configure projectTags in dvmi config.',
97
+ )
98
+ }
99
+
100
+ const serviceArg = args.service ?? 'all'
101
+ const tags = config.projectTags ?? (args.service ? { project: args.service } : {})
102
+
103
+ const spinner = isJson ? null : ora(`Fetching costs...`).start()
39
104
 
40
105
  try {
41
- const { entries, period } = await getServiceCosts(args.service, tags, /** @type {any} */ (flags.period))
106
+ const { entries, period } = await getServiceCosts(
107
+ serviceArg,
108
+ tags,
109
+ /** @type {any} */ (flags.period),
110
+ groupBy,
111
+ tagKey,
112
+ )
42
113
  spinner?.stop()
43
114
 
44
115
  const total = calculateTotal(entries)
45
116
  const result = {
46
- service: args.service,
117
+ service: args.service ?? null,
118
+ groupBy,
119
+ tagKey: tagKey ?? null,
47
120
  period,
48
121
  items: entries,
49
122
  total: { amount: total, unit: 'USD' },
@@ -52,21 +125,42 @@ export default class CostsGet extends Command {
52
125
  if (isJson) return result
53
126
 
54
127
  if (entries.length === 0) {
55
- this.log(`No costs found for service "${args.service}".`)
56
- this.log('Check service name and tagging convention.')
128
+ this.log(`No costs found.`)
57
129
  return result
58
130
  }
59
131
 
60
- this.log(formatCostTable(entries, args.service))
132
+ const label = tagKey && groupBy !== 'service' ? `${serviceArg} (by ${tagKey})` : serviceArg
133
+ this.log(formatCostTable(entries, label, groupBy))
61
134
  return result
62
135
  } catch (err) {
63
136
  spinner?.stop()
64
137
  if (String(err).includes('AccessDenied') || String(err).includes('UnauthorizedAccess')) {
65
- this.error('Missing IAM permission: ce:GetCostAndUsage\nContact your AWS admin to grant Cost Explorer access.')
138
+ this.error('Missing IAM permission: ce:GetCostAndUsage. Contact your AWS admin.')
139
+ }
140
+ if (String(err).includes('CredentialsProviderError') || String(err).includes('No credentials')) {
141
+ if (isInteractive) {
142
+ const suggestedProfile = config.awsProfile || process.env.AWS_VAULT || 'default'
143
+ const profile = await input({
144
+ message: 'No AWS credentials. Enter aws-vault profile to retry (empty to cancel):',
145
+ default: suggestedProfile,
146
+ })
147
+
148
+ const selected = profile.trim()
149
+ if (selected) {
150
+ const retryExitCode = await reexecCurrentCommandWithAwsVaultProfile(selected)
151
+ if (retryExitCode !== null) {
152
+ this.exit(retryExitCode)
153
+ return
154
+ }
155
+ }
156
+ }
157
+
158
+ const prefix = awsVaultPrefix(config)
159
+ this.error(
160
+ `No AWS credentials. Use: ${prefix}dvmi costs get` +
161
+ (args.service ? ` ${args.service}` : ''),
162
+ )
66
163
  }
67
- if (String(err).includes('CredentialsProviderError') || String(err).includes('No credentials')) {
68
- this.error('No AWS credentials. Use: aws-vault exec <profile> -- dvmi costs get ' + args.service)
69
- }
70
164
  throw err
71
165
  }
72
166
  }