devvami 1.1.2 → 1.3.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 +36 -1
- package/oclif.manifest.json +233 -7
- package/package.json +2 -1
- package/src/commands/costs/get.js +112 -18
- package/src/commands/costs/trend.js +165 -0
- package/src/commands/init.js +8 -3
- package/src/commands/logs/index.js +190 -0
- package/src/commands/prompts/run.js +19 -1
- package/src/commands/security/setup.js +249 -0
- package/src/commands/welcome.js +17 -0
- package/src/formatters/charts.js +205 -0
- package/src/formatters/cost.js +18 -5
- package/src/formatters/security.js +119 -0
- package/src/help.js +44 -24
- package/src/services/aws-costs.js +130 -6
- package/src/services/clickup.js +9 -3
- package/src/services/cloudwatch-logs.js +92 -0
- package/src/services/config.js +17 -1
- package/src/services/docs.js +5 -1
- package/src/services/prompts.js +2 -2
- package/src/services/security.js +634 -0
- package/src/types.js +132 -4
- package/src/utils/aws-vault.js +144 -0
- package/src/utils/welcome.js +173 -0
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
|
-
- **
|
|
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
|
package/oclif.manifest.json
CHANGED
|
@@ -273,6 +273,29 @@
|
|
|
273
273
|
"upgrade.js"
|
|
274
274
|
]
|
|
275
275
|
},
|
|
276
|
+
"welcome": {
|
|
277
|
+
"aliases": [],
|
|
278
|
+
"args": {},
|
|
279
|
+
"description": "Show the dvmi mission dashboard with animated intro",
|
|
280
|
+
"examples": [
|
|
281
|
+
"<%= config.bin %> welcome"
|
|
282
|
+
],
|
|
283
|
+
"flags": {},
|
|
284
|
+
"hasDynamicHelp": false,
|
|
285
|
+
"hiddenAliases": [],
|
|
286
|
+
"id": "welcome",
|
|
287
|
+
"pluginAlias": "devvami",
|
|
288
|
+
"pluginName": "devvami",
|
|
289
|
+
"pluginType": "core",
|
|
290
|
+
"strict": true,
|
|
291
|
+
"enableJsonFlag": false,
|
|
292
|
+
"isESM": true,
|
|
293
|
+
"relativePath": [
|
|
294
|
+
"src",
|
|
295
|
+
"commands",
|
|
296
|
+
"welcome.js"
|
|
297
|
+
]
|
|
298
|
+
},
|
|
276
299
|
"whoami": {
|
|
277
300
|
"aliases": [],
|
|
278
301
|
"args": {},
|
|
@@ -361,16 +384,20 @@
|
|
|
361
384
|
"aliases": [],
|
|
362
385
|
"args": {
|
|
363
386
|
"service": {
|
|
364
|
-
"description": "
|
|
387
|
+
"description": "Service name (used to derive tag filter from config)",
|
|
365
388
|
"name": "service",
|
|
366
|
-
"required":
|
|
389
|
+
"required": false
|
|
367
390
|
}
|
|
368
391
|
},
|
|
369
|
-
"description": "
|
|
392
|
+
"description": "Get AWS costs for a service, grouped by service, tag, or both",
|
|
370
393
|
"examples": [
|
|
394
|
+
"<%= config.bin %> costs get",
|
|
371
395
|
"<%= config.bin %> costs get my-service",
|
|
372
|
-
"<%= config.bin %> costs get
|
|
373
|
-
"<%= config.bin %> costs get
|
|
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"
|
|
374
401
|
],
|
|
375
402
|
"flags": {
|
|
376
403
|
"json": {
|
|
@@ -381,7 +408,7 @@
|
|
|
381
408
|
"type": "boolean"
|
|
382
409
|
},
|
|
383
410
|
"period": {
|
|
384
|
-
"description": "
|
|
411
|
+
"description": "Time period: last-month, last-week, mtd",
|
|
385
412
|
"name": "period",
|
|
386
413
|
"default": "last-month",
|
|
387
414
|
"hasDynamicHelp": false,
|
|
@@ -392,6 +419,26 @@
|
|
|
392
419
|
"mtd"
|
|
393
420
|
],
|
|
394
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"
|
|
395
442
|
}
|
|
396
443
|
},
|
|
397
444
|
"hasDynamicHelp": false,
|
|
@@ -410,6 +457,69 @@
|
|
|
410
457
|
"get.js"
|
|
411
458
|
]
|
|
412
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
|
+
},
|
|
413
523
|
"create:repo": {
|
|
414
524
|
"aliases": [],
|
|
415
525
|
"args": {
|
|
@@ -703,6 +813,82 @@
|
|
|
703
813
|
"search.js"
|
|
704
814
|
]
|
|
705
815
|
},
|
|
816
|
+
"logs": {
|
|
817
|
+
"aliases": [],
|
|
818
|
+
"args": {},
|
|
819
|
+
"description": "Browse and query CloudWatch log groups interactively",
|
|
820
|
+
"examples": [
|
|
821
|
+
"<%= config.bin %> logs",
|
|
822
|
+
"<%= config.bin %> logs --group /aws/lambda/my-fn",
|
|
823
|
+
"<%= config.bin %> logs --group /aws/lambda/my-fn --filter \"ERROR\" --since 24h",
|
|
824
|
+
"<%= config.bin %> logs --group /aws/lambda/my-fn --limit 50 --json"
|
|
825
|
+
],
|
|
826
|
+
"flags": {
|
|
827
|
+
"json": {
|
|
828
|
+
"description": "Format output as json.",
|
|
829
|
+
"helpGroup": "GLOBAL",
|
|
830
|
+
"name": "json",
|
|
831
|
+
"allowNo": false,
|
|
832
|
+
"type": "boolean"
|
|
833
|
+
},
|
|
834
|
+
"group": {
|
|
835
|
+
"char": "g",
|
|
836
|
+
"description": "Log group name — bypasses interactive picker",
|
|
837
|
+
"name": "group",
|
|
838
|
+
"hasDynamicHelp": false,
|
|
839
|
+
"multiple": false,
|
|
840
|
+
"type": "option"
|
|
841
|
+
},
|
|
842
|
+
"filter": {
|
|
843
|
+
"char": "f",
|
|
844
|
+
"description": "CloudWatch filter pattern (empty = all events)",
|
|
845
|
+
"name": "filter",
|
|
846
|
+
"default": "",
|
|
847
|
+
"hasDynamicHelp": false,
|
|
848
|
+
"multiple": false,
|
|
849
|
+
"type": "option"
|
|
850
|
+
},
|
|
851
|
+
"since": {
|
|
852
|
+
"description": "Time window: 1h, 24h, 7d",
|
|
853
|
+
"name": "since",
|
|
854
|
+
"default": "1h",
|
|
855
|
+
"hasDynamicHelp": false,
|
|
856
|
+
"multiple": false,
|
|
857
|
+
"type": "option"
|
|
858
|
+
},
|
|
859
|
+
"limit": {
|
|
860
|
+
"description": "Max log events to return (1–10000)",
|
|
861
|
+
"name": "limit",
|
|
862
|
+
"default": 100,
|
|
863
|
+
"hasDynamicHelp": false,
|
|
864
|
+
"multiple": false,
|
|
865
|
+
"type": "option"
|
|
866
|
+
},
|
|
867
|
+
"region": {
|
|
868
|
+
"char": "r",
|
|
869
|
+
"description": "AWS region (defaults to project config awsRegion)",
|
|
870
|
+
"name": "region",
|
|
871
|
+
"hasDynamicHelp": false,
|
|
872
|
+
"multiple": false,
|
|
873
|
+
"type": "option"
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
"hasDynamicHelp": false,
|
|
877
|
+
"hiddenAliases": [],
|
|
878
|
+
"id": "logs",
|
|
879
|
+
"pluginAlias": "devvami",
|
|
880
|
+
"pluginName": "devvami",
|
|
881
|
+
"pluginType": "core",
|
|
882
|
+
"strict": true,
|
|
883
|
+
"enableJsonFlag": true,
|
|
884
|
+
"isESM": true,
|
|
885
|
+
"relativePath": [
|
|
886
|
+
"src",
|
|
887
|
+
"commands",
|
|
888
|
+
"logs",
|
|
889
|
+
"index.js"
|
|
890
|
+
]
|
|
891
|
+
},
|
|
706
892
|
"pipeline:logs": {
|
|
707
893
|
"aliases": [],
|
|
708
894
|
"args": {
|
|
@@ -1346,6 +1532,46 @@
|
|
|
1346
1532
|
"list.js"
|
|
1347
1533
|
]
|
|
1348
1534
|
},
|
|
1535
|
+
"security:setup": {
|
|
1536
|
+
"aliases": [],
|
|
1537
|
+
"args": {},
|
|
1538
|
+
"description": "Interactive wizard to install and configure credential protection tools (aws-vault, pass, GPG, Git Credential Manager, macOS Keychain)",
|
|
1539
|
+
"examples": [
|
|
1540
|
+
"<%= config.bin %> security setup",
|
|
1541
|
+
"<%= config.bin %> security setup --json"
|
|
1542
|
+
],
|
|
1543
|
+
"flags": {
|
|
1544
|
+
"json": {
|
|
1545
|
+
"description": "Format output as json.",
|
|
1546
|
+
"helpGroup": "GLOBAL",
|
|
1547
|
+
"name": "json",
|
|
1548
|
+
"allowNo": false,
|
|
1549
|
+
"type": "boolean"
|
|
1550
|
+
},
|
|
1551
|
+
"help": {
|
|
1552
|
+
"char": "h",
|
|
1553
|
+
"description": "Show CLI help.",
|
|
1554
|
+
"name": "help",
|
|
1555
|
+
"allowNo": false,
|
|
1556
|
+
"type": "boolean"
|
|
1557
|
+
}
|
|
1558
|
+
},
|
|
1559
|
+
"hasDynamicHelp": false,
|
|
1560
|
+
"hiddenAliases": [],
|
|
1561
|
+
"id": "security:setup",
|
|
1562
|
+
"pluginAlias": "devvami",
|
|
1563
|
+
"pluginName": "devvami",
|
|
1564
|
+
"pluginType": "core",
|
|
1565
|
+
"strict": true,
|
|
1566
|
+
"enableJsonFlag": true,
|
|
1567
|
+
"isESM": true,
|
|
1568
|
+
"relativePath": [
|
|
1569
|
+
"src",
|
|
1570
|
+
"commands",
|
|
1571
|
+
"security",
|
|
1572
|
+
"setup.js"
|
|
1573
|
+
]
|
|
1574
|
+
},
|
|
1349
1575
|
"tasks:assigned": {
|
|
1350
1576
|
"aliases": [],
|
|
1351
1577
|
"args": {},
|
|
@@ -1498,5 +1724,5 @@
|
|
|
1498
1724
|
]
|
|
1499
1725
|
}
|
|
1500
1726
|
},
|
|
1501
|
-
"version": "1.
|
|
1727
|
+
"version": "1.3.0"
|
|
1502
1728
|
}
|
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.
|
|
4
|
+
"version": "1.3.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 = '
|
|
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
|
|
13
|
-
'<%= config.bin %> costs get
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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(
|
|
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
|
|
56
|
-
this.log('Check service name and tagging convention.')
|
|
128
|
+
this.log(`No costs found.`)
|
|
57
129
|
return result
|
|
58
130
|
}
|
|
59
131
|
|
|
60
|
-
|
|
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
|
|
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
|
}
|