cloud-cost-cli 0.2.0 → 0.3.0-beta.2
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 +223 -13
- package/dist/bin/cloud-cost-cli.js +23 -1
- package/dist/src/commands/ask.d.ts +11 -0
- package/dist/src/commands/ask.js +164 -0
- package/dist/src/commands/config.d.ts +1 -0
- package/dist/src/commands/config.js +120 -0
- package/dist/src/commands/costs.d.ts +6 -0
- package/dist/src/commands/costs.js +54 -0
- package/dist/src/commands/scan.d.ts +3 -0
- package/dist/src/commands/scan.js +93 -2
- package/dist/src/commands/script.d.ts +8 -0
- package/dist/src/commands/script.js +27 -0
- package/dist/src/providers/azure/client.d.ts +1 -0
- package/dist/src/providers/azure/client.js +23 -0
- package/dist/src/reporters/table.d.ts +2 -1
- package/dist/src/reporters/table.js +66 -2
- package/dist/src/services/ai.d.ts +44 -0
- package/dist/src/services/ai.js +345 -0
- package/dist/src/services/script-generator.d.ts +21 -0
- package/dist/src/services/script-generator.js +245 -0
- package/dist/src/utils/cache.d.ts +25 -0
- package/dist/src/utils/cache.js +197 -0
- package/dist/src/utils/config.d.ts +37 -0
- package/dist/src/utils/config.js +175 -0
- package/dist/src/utils/cost-tracker.d.ts +33 -0
- package/dist/src/utils/cost-tracker.js +135 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
A command-line tool that analyzes your AWS and Azure resources to identify cost-saving opportunities — idle resources, oversized instances, unattached volumes, and more.
|
|
9
9
|
|
|
10
|
+
**✨ NEW in v0.3.0-beta:** AI-powered explanations and natural language queries!
|
|
11
|
+
|
|
10
12
|
---
|
|
11
13
|
|
|
12
14
|
## The Problem
|
|
@@ -34,6 +36,11 @@ Cloud bills are growing faster than revenue. Engineering teams overprovision, fo
|
|
|
34
36
|
- ✅ **Multi-cloud support** - AWS and Azure
|
|
35
37
|
- ✅ **AWS analyzers** - EC2, EBS, RDS, S3, ELB, Elastic IP
|
|
36
38
|
- ✅ **Azure analyzers** - VMs, Managed Disks, Storage, SQL, Public IPs
|
|
39
|
+
- ✅ **🤖 AI-powered explanations** - Get human-readable explanations for why resources are costing money (beta)
|
|
40
|
+
- ✅ **💬 Natural language queries** - Ask questions like "What's my biggest cost?" or "Show me idle VMs" (beta)
|
|
41
|
+
- ✅ **🔒 Privacy-first AI** - Use local Ollama or cloud OpenAI
|
|
42
|
+
- ✅ **💰 Cost tracking** - Track AI API costs (OpenAI only)
|
|
43
|
+
- ✅ **⚙️ Configuration file** - Save your preferences
|
|
37
44
|
- ✅ Connect via cloud credentials (read-only recommended)
|
|
38
45
|
- ✅ Analyze last 7-30 days of usage
|
|
39
46
|
- ✅ Output top savings opportunities with estimated monthly savings
|
|
@@ -44,7 +51,6 @@ Cloud bills are growing faster than revenue. Engineering teams overprovision, fo
|
|
|
44
51
|
**Potential future additions:**
|
|
45
52
|
- GCP support (Compute Engine, Cloud Storage, Cloud SQL)
|
|
46
53
|
- Real-time pricing API integration
|
|
47
|
-
- Configuration file support
|
|
48
54
|
- Additional AWS services (Lambda, DynamoDB, CloudFront, etc.)
|
|
49
55
|
- Additional Azure services (App Services, CosmosDB, etc.)
|
|
50
56
|
- Multi-region analysis
|
|
@@ -61,19 +67,34 @@ No commitment on timeline - contributions welcome!
|
|
|
61
67
|
|
|
62
68
|
**Requirements:**
|
|
63
69
|
- Node.js >= 18
|
|
64
|
-
- Cloud credentials:
|
|
65
|
-
- **AWS**:
|
|
66
|
-
|
|
70
|
+
- Cloud credentials (choose one per provider):
|
|
71
|
+
- **AWS**:
|
|
72
|
+
- [AWS CLI configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) OR
|
|
73
|
+
- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
|
|
74
|
+
- **Azure**:
|
|
75
|
+
- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) (`az login`) OR
|
|
76
|
+
- Service Principal (env vars: `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`) OR
|
|
77
|
+
- Managed Identity (for Azure VMs)
|
|
78
|
+
- **Optional for AI features**:
|
|
79
|
+
- OpenAI API key OR
|
|
80
|
+
- [Ollama](https://ollama.ai) installed locally (free, private, runs on your machine)
|
|
67
81
|
|
|
68
82
|
**Install via npm:**
|
|
69
83
|
```bash
|
|
70
84
|
npm install -g cloud-cost-cli
|
|
71
85
|
```
|
|
72
86
|
|
|
87
|
+
**Try the beta with AI features:**
|
|
88
|
+
```bash
|
|
89
|
+
npm install -g cloud-cost-cli@beta
|
|
90
|
+
```
|
|
91
|
+
|
|
73
92
|
---
|
|
74
93
|
|
|
75
94
|
## Usage
|
|
76
95
|
|
|
96
|
+
### Basic Scan
|
|
97
|
+
|
|
77
98
|
**AWS scan:**
|
|
78
99
|
```bash
|
|
79
100
|
cloud-cost-cli scan --provider aws --profile default --region us-east-1
|
|
@@ -81,12 +102,92 @@ cloud-cost-cli scan --provider aws --profile default --region us-east-1
|
|
|
81
102
|
|
|
82
103
|
**Azure scan:**
|
|
83
104
|
```bash
|
|
84
|
-
#
|
|
105
|
+
# Option 1: Azure CLI (easiest for local use)
|
|
106
|
+
az login
|
|
85
107
|
export AZURE_SUBSCRIPTION_ID="your-subscription-id"
|
|
108
|
+
cloud-cost-cli scan --provider azure --location eastus
|
|
86
109
|
|
|
110
|
+
# Option 2: Service Principal (recommended for CI/CD and automation)
|
|
111
|
+
export AZURE_CLIENT_ID="your-app-id"
|
|
112
|
+
export AZURE_CLIENT_SECRET="your-secret"
|
|
113
|
+
export AZURE_TENANT_ID="your-tenant-id"
|
|
114
|
+
export AZURE_SUBSCRIPTION_ID="your-subscription-id"
|
|
87
115
|
cloud-cost-cli scan --provider azure --location eastus
|
|
88
116
|
```
|
|
89
117
|
|
|
118
|
+
**How to create Azure Service Principal:**
|
|
119
|
+
```bash
|
|
120
|
+
# Create service principal with Reader role
|
|
121
|
+
az ad sp create-for-rbac --name "cloud-cost-cli" --role Reader --scopes /subscriptions/YOUR_SUBSCRIPTION_ID
|
|
122
|
+
|
|
123
|
+
# Output will show:
|
|
124
|
+
# {
|
|
125
|
+
# "appId": "xxx", # Use as AZURE_CLIENT_ID
|
|
126
|
+
# "password": "xxx", # Use as AZURE_CLIENT_SECRET
|
|
127
|
+
# "tenant": "xxx" # Use as AZURE_TENANT_ID
|
|
128
|
+
# }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 🤖 AI-Powered Features (Beta)
|
|
132
|
+
|
|
133
|
+
**Get AI explanations for opportunities:**
|
|
134
|
+
```bash
|
|
135
|
+
# Using OpenAI (requires API key)
|
|
136
|
+
export OPENAI_API_KEY="sk-..."
|
|
137
|
+
cloud-cost-cli scan --provider aws --region us-east-1 --explain
|
|
138
|
+
|
|
139
|
+
# Using local Ollama (free, private, no API key needed)
|
|
140
|
+
cloud-cost-cli scan --provider aws --region us-east-1 --explain --ai-provider ollama
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Ask natural language questions:**
|
|
144
|
+
```bash
|
|
145
|
+
# First, run a scan to collect data
|
|
146
|
+
cloud-cost-cli scan --provider aws --region us-east-1
|
|
147
|
+
|
|
148
|
+
# Then ask questions about your costs
|
|
149
|
+
cloud-cost-cli ask "What's my biggest cost opportunity?"
|
|
150
|
+
cloud-cost-cli ask "Show me all idle EC2 instances"
|
|
151
|
+
cloud-cost-cli ask "How much can I save on storage?"
|
|
152
|
+
cloud-cost-cli ask "Which resources should I optimize first?"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Configure AI settings (saves preferences):**
|
|
156
|
+
```bash
|
|
157
|
+
# Initialize config file
|
|
158
|
+
cloud-cost-cli config init
|
|
159
|
+
|
|
160
|
+
# Set AI provider (openai or ollama)
|
|
161
|
+
cloud-cost-cli config set ai.provider ollama
|
|
162
|
+
|
|
163
|
+
# Set OpenAI API key (if using OpenAI)
|
|
164
|
+
cloud-cost-cli config set ai.apiKey "sk-..."
|
|
165
|
+
|
|
166
|
+
# Set AI model
|
|
167
|
+
cloud-cost-cli config set ai.model "llama3.1:8b" # For Ollama
|
|
168
|
+
cloud-cost-cli config set ai.model "gpt-4o-mini" # For OpenAI
|
|
169
|
+
|
|
170
|
+
# Set max explanations (how many to explain)
|
|
171
|
+
cloud-cost-cli config set ai.maxExplanations 5
|
|
172
|
+
|
|
173
|
+
# View your config
|
|
174
|
+
cloud-cost-cli config show
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Track AI costs (OpenAI only):**
|
|
178
|
+
```bash
|
|
179
|
+
# View AI API costs
|
|
180
|
+
cloud-cost-cli costs
|
|
181
|
+
|
|
182
|
+
# View last 7 days
|
|
183
|
+
cloud-cost-cli costs --days 7
|
|
184
|
+
|
|
185
|
+
# Clear cost tracking
|
|
186
|
+
cloud-cost-cli costs --clear
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Advanced Options
|
|
190
|
+
|
|
90
191
|
**Show more opportunities:**
|
|
91
192
|
```bash
|
|
92
193
|
cloud-cost-cli scan --provider aws --top 20 # Show top 20 instead of default 5
|
|
@@ -102,7 +203,7 @@ cloud-cost-cli scan --provider azure --min-savings 50 # Only show opportunities
|
|
|
102
203
|
cloud-cost-cli scan --provider aws --output json > report.json
|
|
103
204
|
```
|
|
104
205
|
|
|
105
|
-
**Example output:**
|
|
206
|
+
**Example output (with AI explanations):**
|
|
106
207
|
```
|
|
107
208
|
Cloud Cost Optimization Report
|
|
108
209
|
Provider: AWS | Region: us-east-1 | Account: N/A
|
|
@@ -118,18 +219,114 @@ Top 5 Savings Opportunities (est. $1,245/month):
|
|
|
118
219
|
│ 2 │ EBS │ vol-0xyz789abc │ Delete unattached volume (500 GB) │ $40.00 │
|
|
119
220
|
├───┼──────────┼────────────────────────────────────────┼──────────────────────────────────────────────────────────┼─────────────┤
|
|
120
221
|
│ 3 │ RDS │ mydb-production │ Downsize from db.r5.xlarge to db.t3.large (CPU: 15%) │ $180.00 │
|
|
121
|
-
├───┼──────────┼────────────────────────────────────────┼──────────────────────────────────────────────────────────┼─────────────┤
|
|
122
|
-
│ 4 │ ELB │ my-old-alb │ Delete unused load balancer │ $22.00 │
|
|
123
|
-
├───┼──────────┼────────────────────────────────────────┼──────────────────────────────────────────────────────────┼─────────────┤
|
|
124
|
-
│ 5 │ S3 │ logs-bucket-2023 │ Add lifecycle policy to transition to Glacier │ $938.00 │
|
|
125
222
|
└───┴──────────┴────────────────────────────────────────┴──────────────────────────────────────────────────────────┴─────────────┘
|
|
126
223
|
|
|
224
|
+
🤖 AI Explanations:
|
|
225
|
+
|
|
226
|
+
💡 Opportunity #1: Stop idle instance (CPU: 2%)
|
|
227
|
+
This EC2 instance is consuming only 2% CPU, indicating it's severely underutilized.
|
|
228
|
+
Consider stopping it during off-hours or right-sizing to a smaller instance type.
|
|
229
|
+
Quick win: Stop it immediately if it's a dev/test server not actively used.
|
|
230
|
+
Risk: Low - monitor for 24h first to confirm usage patterns.
|
|
231
|
+
|
|
232
|
+
💡 Opportunity #2: Delete unattached volume (500 GB)
|
|
233
|
+
This EBS volume isn't attached to any instance but you're still paying for storage.
|
|
234
|
+
Either delete it if data isn't needed, or create a snapshot first for backup.
|
|
235
|
+
Quick win: Take a snapshot ($0.05/GB/mo vs $0.08/GB/mo), then delete the volume.
|
|
236
|
+
Risk: Medium - verify no one needs this data before deleting.
|
|
237
|
+
|
|
238
|
+
💡 Opportunity #3: Downsize RDS instance
|
|
239
|
+
Your database is only using 15% CPU on a db.r5.xlarge. You're paying for 4 vCPUs
|
|
240
|
+
but only need 1-2. Downsize to db.t3.large (2 vCPUs) and save $180/month.
|
|
241
|
+
Quick win: Schedule a downsize during your next maintenance window.
|
|
242
|
+
Risk: Low-Medium - test query performance after resize.
|
|
243
|
+
|
|
127
244
|
Total potential savings: $1,245/month ($14,940/year)
|
|
128
245
|
|
|
129
|
-
|
|
246
|
+
ℹ AI explanations powered by OpenAI GPT-4o-mini (cost: $0.02)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## AI Features Setup
|
|
252
|
+
|
|
253
|
+
### Option 1: OpenAI (Cloud, Paid)
|
|
254
|
+
|
|
255
|
+
**Pros:** Fast, accurate, works anywhere
|
|
256
|
+
**Cons:** Costs ~$0.01-0.05 per scan, data sent to OpenAI
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# Get API key from https://platform.openai.com/api-keys
|
|
260
|
+
export OPENAI_API_KEY="sk-..."
|
|
261
|
+
|
|
262
|
+
# Or save to config
|
|
263
|
+
cloud-cost-cli config set ai.apiKey "sk-..."
|
|
264
|
+
cloud-cost-cli config set ai.provider openai
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Option 2: Ollama (Local, Free)
|
|
130
268
|
|
|
131
|
-
|
|
132
|
-
|
|
269
|
+
**Pros:** Free, private (runs on your machine), no API costs
|
|
270
|
+
**Cons:** Requires ~4GB RAM, slower than OpenAI
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Install Ollama
|
|
274
|
+
curl -fsSL https://ollama.ai/install.sh | sh
|
|
275
|
+
|
|
276
|
+
# Pull a model (one-time, ~4GB download)
|
|
277
|
+
ollama pull llama3.1:8b
|
|
278
|
+
|
|
279
|
+
# Configure cloud-cost-cli
|
|
280
|
+
cloud-cost-cli config set ai.provider ollama
|
|
281
|
+
cloud-cost-cli config set ai.model "llama3.1:8b"
|
|
282
|
+
|
|
283
|
+
# Use it
|
|
284
|
+
cloud-cost-cli scan --provider aws --region us-east-1 --explain
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Recommended Ollama models:**
|
|
288
|
+
- `llama3.1:8b` - Best balance (4GB RAM, good quality)
|
|
289
|
+
- `llama3.2:3b` - Faster, less RAM (2GB, slightly lower quality)
|
|
290
|
+
- `mistral:7b` - Alternative, similar to llama3.1
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Configuration File
|
|
295
|
+
|
|
296
|
+
**Location:** `~/.cloud-cost-cli.json`
|
|
297
|
+
|
|
298
|
+
**Example config:**
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"ai": {
|
|
302
|
+
"provider": "ollama",
|
|
303
|
+
"model": "llama3.1:8b",
|
|
304
|
+
"maxExplanations": 5,
|
|
305
|
+
"cache": {
|
|
306
|
+
"enabled": true,
|
|
307
|
+
"ttlDays": 7
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
"scan": {
|
|
311
|
+
"defaultProvider": "aws",
|
|
312
|
+
"defaultRegion": "us-east-1",
|
|
313
|
+
"defaultTop": 5,
|
|
314
|
+
"minSavings": 10
|
|
315
|
+
},
|
|
316
|
+
"aws": {
|
|
317
|
+
"profile": "default",
|
|
318
|
+
"region": "us-east-1"
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Manage config:**
|
|
324
|
+
```bash
|
|
325
|
+
cloud-cost-cli config init # Create config file
|
|
326
|
+
cloud-cost-cli config show # View current config
|
|
327
|
+
cloud-cost-cli config get ai.provider # Get specific value
|
|
328
|
+
cloud-cost-cli config set ai.provider ollama # Set value
|
|
329
|
+
cloud-cost-cli config path # Show config file location
|
|
133
330
|
```
|
|
134
331
|
|
|
135
332
|
---
|
|
@@ -208,6 +405,8 @@ MIT License - see [LICENSE](LICENSE)
|
|
|
208
405
|
- [Commander.js](https://github.com/tj/commander.js) - CLI framework
|
|
209
406
|
- [cli-table3](https://github.com/cli-table/cli-table3) - Terminal tables
|
|
210
407
|
- [Chalk](https://github.com/chalk/chalk) - Terminal styling
|
|
408
|
+
- [OpenAI API](https://platform.openai.com/) - AI explanations
|
|
409
|
+
- [Ollama](https://ollama.ai) - Local AI models
|
|
211
410
|
|
|
212
411
|
---
|
|
213
412
|
|
|
@@ -224,9 +423,20 @@ A: Read-only permissions for each cloud provider:
|
|
|
224
423
|
**Q: How accurate are the savings estimates?**
|
|
225
424
|
A: Estimates are based on current pricing and usage patterns. Actual savings may vary by region and your specific pricing agreements (Reserved Instances, Savings Plans, etc.).
|
|
226
425
|
|
|
426
|
+
**Q: Is my data sent to OpenAI?**
|
|
427
|
+
A: Only if you use `--ai-provider openai` (the default). Resource metadata and recommendations are sent to OpenAI's API to generate explanations. If you want complete privacy, use `--ai-provider ollama` which runs 100% locally on your machine.
|
|
428
|
+
|
|
429
|
+
**Q: How much do AI features cost?**
|
|
430
|
+
A:
|
|
431
|
+
- **Ollama**: Free! Runs locally, no API costs
|
|
432
|
+
- **OpenAI**: ~$0.01-0.05 per scan (GPT-4o-mini). Use `cloud-cost-cli costs` to track spending.
|
|
433
|
+
|
|
227
434
|
**Q: Can I run this in CI/CD?**
|
|
228
435
|
A: Yes. Use `--output json` and parse the results to fail builds if savings exceed a threshold.
|
|
229
436
|
|
|
437
|
+
**Q: Do I need AI features to use the tool?**
|
|
438
|
+
A: No! AI features are completely optional. The core cost scanning works without any AI setup.
|
|
439
|
+
|
|
230
440
|
---
|
|
231
441
|
|
|
232
442
|
**Star this repo if it saves you money!** ⭐
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
5
|
const scan_js_1 = require("../src/commands/scan.js");
|
|
6
|
+
const ask_js_1 = require("../src/commands/ask.js");
|
|
7
|
+
const config_js_1 = require("../src/commands/config.js");
|
|
8
|
+
const costs_js_1 = require("../src/commands/costs.js");
|
|
6
9
|
const program = new commander_1.Command();
|
|
7
10
|
program
|
|
8
11
|
.name('cloud-cost-cli')
|
|
9
12
|
.description('Optimize your cloud spend in seconds')
|
|
10
|
-
.version('0.
|
|
13
|
+
.version('0.3.0-beta.1');
|
|
11
14
|
program
|
|
12
15
|
.command('scan')
|
|
13
16
|
.description('Scan cloud account for cost savings')
|
|
@@ -21,6 +24,25 @@ program
|
|
|
21
24
|
.option('--days <N>', 'Analysis period in days', '30')
|
|
22
25
|
.option('--min-savings <amount>', 'Filter by minimum savings ($/month)')
|
|
23
26
|
.option('--accurate', 'Use real-time pricing from AWS (slower but more accurate)')
|
|
27
|
+
.option('--explain', 'AI-powered explanations for top opportunities')
|
|
28
|
+
.option('--ai-provider <openai|ollama>', 'AI provider (reads from config if not specified)')
|
|
29
|
+
.option('--ai-model <model>', 'AI model (gpt-4o-mini for OpenAI, llama3.2:3b for Ollama)')
|
|
24
30
|
.option('--verbose', 'Verbose logging')
|
|
25
31
|
.action(scan_js_1.scanCommand);
|
|
32
|
+
program
|
|
33
|
+
.command('ask <query>')
|
|
34
|
+
.description('Ask natural language questions about your cloud costs')
|
|
35
|
+
.option('--ai-provider <openai|ollama>', 'AI provider (reads from config if not specified)')
|
|
36
|
+
.option('--ai-model <model>', 'AI model to use')
|
|
37
|
+
.action(ask_js_1.askCommand);
|
|
38
|
+
program
|
|
39
|
+
.command('config <action> [key] [value]')
|
|
40
|
+
.description('Manage configuration (actions: init, show, get, set, path)')
|
|
41
|
+
.action(config_js_1.configCommand);
|
|
42
|
+
program
|
|
43
|
+
.command('costs')
|
|
44
|
+
.description('Show AI usage costs')
|
|
45
|
+
.option('--days <N>', 'Number of days to include', '30')
|
|
46
|
+
.option('--clear', 'Clear cost tracking data')
|
|
47
|
+
.action(costs_js_1.costsCommand);
|
|
26
48
|
program.parse();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface AskCommandOptions {
|
|
2
|
+
provider?: string;
|
|
3
|
+
aiProvider?: string;
|
|
4
|
+
aiModel?: string;
|
|
5
|
+
region?: string;
|
|
6
|
+
location?: string;
|
|
7
|
+
subscriptionId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function askCommand(query: string, options: AskCommandOptions): Promise<void>;
|
|
10
|
+
export declare function saveScanCache(provider: string, region: string | undefined, report: any): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.askCommand = askCommand;
|
|
40
|
+
exports.saveScanCache = saveScanCache;
|
|
41
|
+
const ai_1 = require("../services/ai");
|
|
42
|
+
const logger_1 = require("../utils/logger");
|
|
43
|
+
const config_1 = require("../utils/config");
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
async function askCommand(query, options) {
|
|
48
|
+
(0, logger_1.info)(`Analyzing: "${query}"`);
|
|
49
|
+
// Load most recent scan results
|
|
50
|
+
const scanData = loadRecentScan();
|
|
51
|
+
if (!scanData) {
|
|
52
|
+
(0, logger_1.error)('No recent scan found. Please run a scan first:');
|
|
53
|
+
(0, logger_1.info)(' cloud-cost-cli scan --provider aws --region us-east-1');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const ageMinutes = Math.floor((Date.now() - scanData.timestamp) / 60000);
|
|
57
|
+
(0, logger_1.info)(`Using scan from ${ageMinutes} minutes ago (${scanData.provider}${scanData.region ? ', ' + scanData.region : ''})`);
|
|
58
|
+
// Initialize AI service
|
|
59
|
+
// Load config file to get defaults
|
|
60
|
+
const fileConfig = config_1.ConfigLoader.load();
|
|
61
|
+
// CLI flags override config file
|
|
62
|
+
const provider = options.aiProvider || fileConfig.ai?.provider || 'openai';
|
|
63
|
+
const model = options.aiModel || fileConfig.ai?.model;
|
|
64
|
+
if (provider === 'openai' && !process.env.OPENAI_API_KEY && !fileConfig.ai?.apiKey) {
|
|
65
|
+
(0, logger_1.error)('Natural language queries require OPENAI_API_KEY or --ai-provider ollama');
|
|
66
|
+
(0, logger_1.info)('Set it with: export OPENAI_API_KEY="sk-..."');
|
|
67
|
+
(0, logger_1.info)('Or run: cloud-cost-cli config set ai.provider ollama');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const aiService = new ai_1.AIService({
|
|
72
|
+
provider,
|
|
73
|
+
apiKey: provider === 'openai' ? (process.env.OPENAI_API_KEY || fileConfig.ai?.apiKey) : undefined,
|
|
74
|
+
model,
|
|
75
|
+
});
|
|
76
|
+
console.log(chalk_1.default.cyan('\n🤔 Thinking...\n'));
|
|
77
|
+
const answer = await aiService.answerQuery(query, scanData.report);
|
|
78
|
+
console.log(chalk_1.default.bold('💡 Answer:\n'));
|
|
79
|
+
console.log(chalk_1.default.white(answer.response));
|
|
80
|
+
if (answer.suggestions && answer.suggestions.length > 0) {
|
|
81
|
+
console.log(chalk_1.default.bold('\n📋 Suggestions:\n'));
|
|
82
|
+
answer.suggestions.forEach((suggestion, i) => {
|
|
83
|
+
console.log(chalk_1.default.green(` ${i + 1}. ${suggestion}`));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (answer.relatedOpportunities && answer.relatedOpportunities.length > 0) {
|
|
87
|
+
console.log(chalk_1.default.bold('\n🎯 Related Opportunities:\n'));
|
|
88
|
+
answer.relatedOpportunities.forEach((opp, i) => {
|
|
89
|
+
console.log(chalk_1.default.yellow(` ${i + 1}. ${opp.resourceId}: ${opp.recommendation} (Save $${opp.estimatedSavings.toFixed(2)}/mo)`));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
console.log();
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
(0, logger_1.error)(`Query failed: ${err.message}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function loadRecentScan() {
|
|
100
|
+
const cacheDir = path.join(process.env.HOME || '/tmp', '.cloud-cost-cli', 'scans');
|
|
101
|
+
if (!fs.existsSync(cacheDir)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const files = fs.readdirSync(cacheDir)
|
|
106
|
+
.filter(f => f.endsWith('.json'))
|
|
107
|
+
.map(f => ({
|
|
108
|
+
path: path.join(cacheDir, f),
|
|
109
|
+
stat: fs.statSync(path.join(cacheDir, f)),
|
|
110
|
+
}))
|
|
111
|
+
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
|
112
|
+
if (files.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// Get most recent scan (max 24 hours old)
|
|
116
|
+
const mostRecent = files[0];
|
|
117
|
+
const ageMs = Date.now() - mostRecent.stat.mtimeMs;
|
|
118
|
+
if (ageMs > 24 * 60 * 60 * 1000) {
|
|
119
|
+
return null; // Too old
|
|
120
|
+
}
|
|
121
|
+
const data = fs.readFileSync(mostRecent.path, 'utf-8');
|
|
122
|
+
return JSON.parse(data);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function saveScanCache(provider, region, report) {
|
|
129
|
+
const cacheDir = path.join(process.env.HOME || '/tmp', '.cloud-cost-cli', 'scans');
|
|
130
|
+
if (!fs.existsSync(cacheDir)) {
|
|
131
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
const cache = {
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
provider,
|
|
136
|
+
region,
|
|
137
|
+
report,
|
|
138
|
+
};
|
|
139
|
+
const filename = `scan-${provider}-${Date.now()}.json`;
|
|
140
|
+
const filepath = path.join(cacheDir, filename);
|
|
141
|
+
try {
|
|
142
|
+
fs.writeFileSync(filepath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
143
|
+
// Clean up old scans (keep only last 10)
|
|
144
|
+
const files = fs.readdirSync(cacheDir)
|
|
145
|
+
.filter(f => f.endsWith('.json'))
|
|
146
|
+
.map(f => ({
|
|
147
|
+
path: path.join(cacheDir, f),
|
|
148
|
+
stat: fs.statSync(path.join(cacheDir, f)),
|
|
149
|
+
}))
|
|
150
|
+
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
|
151
|
+
// Delete all but the 10 most recent
|
|
152
|
+
files.slice(10).forEach(f => {
|
|
153
|
+
try {
|
|
154
|
+
fs.unlinkSync(f.path);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
// Ignore deletion errors
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
// Failed to save cache, not critical
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function configCommand(action: string, key?: string, value?: string): Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.configCommand = configCommand;
|
|
7
|
+
const config_1 = require("../utils/config");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
async function configCommand(action, key, value) {
|
|
11
|
+
switch (action) {
|
|
12
|
+
case 'init':
|
|
13
|
+
initConfig();
|
|
14
|
+
break;
|
|
15
|
+
case 'show':
|
|
16
|
+
showConfig();
|
|
17
|
+
break;
|
|
18
|
+
case 'get':
|
|
19
|
+
if (!key) {
|
|
20
|
+
(0, logger_1.error)('Key required for get action');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
getConfigValue(key);
|
|
24
|
+
break;
|
|
25
|
+
case 'set':
|
|
26
|
+
if (!key || !value) {
|
|
27
|
+
(0, logger_1.error)('Key and value required for set action');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
setConfigValue(key, value);
|
|
31
|
+
break;
|
|
32
|
+
case 'path':
|
|
33
|
+
showConfigPath();
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
(0, logger_1.error)(`Unknown config action: ${action}`);
|
|
37
|
+
(0, logger_1.info)('Available actions: init, show, get, set, path');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function initConfig() {
|
|
42
|
+
const configPath = config_1.ConfigLoader.getConfigPath();
|
|
43
|
+
const example = config_1.ConfigLoader.generateExample();
|
|
44
|
+
(0, logger_1.info)('Creating example configuration...');
|
|
45
|
+
console.log(chalk_1.default.dim('─'.repeat(80)));
|
|
46
|
+
console.log(example);
|
|
47
|
+
console.log(chalk_1.default.dim('─'.repeat(80)));
|
|
48
|
+
config_1.ConfigLoader.save(JSON.parse(example));
|
|
49
|
+
(0, logger_1.success)(`Configuration saved to: ${configPath}`);
|
|
50
|
+
(0, logger_1.info)('Edit this file to customize your settings');
|
|
51
|
+
}
|
|
52
|
+
function showConfig() {
|
|
53
|
+
const config = config_1.ConfigLoader.load();
|
|
54
|
+
const configPath = config_1.ConfigLoader.getConfigPath();
|
|
55
|
+
console.log(chalk_1.default.bold(`\nConfiguration (from ${configPath}):\n`));
|
|
56
|
+
console.log(JSON.stringify(config, null, 2));
|
|
57
|
+
console.log();
|
|
58
|
+
const errors = config_1.ConfigLoader.validate(config);
|
|
59
|
+
if (errors.length > 0) {
|
|
60
|
+
console.log(chalk_1.default.yellow('⚠️ Validation warnings:'));
|
|
61
|
+
errors.forEach(err => console.log(chalk_1.default.yellow(` - ${err}`)));
|
|
62
|
+
console.log();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function getConfigValue(key) {
|
|
66
|
+
const config = config_1.ConfigLoader.load();
|
|
67
|
+
const value = getNestedValue(config, key);
|
|
68
|
+
if (value === undefined) {
|
|
69
|
+
(0, logger_1.error)(`Key not found: ${key}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
console.log(JSON.stringify(value, null, 2));
|
|
73
|
+
}
|
|
74
|
+
function setConfigValue(key, value) {
|
|
75
|
+
const config = config_1.ConfigLoader.load();
|
|
76
|
+
// Parse value (try JSON first, fall back to string)
|
|
77
|
+
let parsedValue = value;
|
|
78
|
+
try {
|
|
79
|
+
parsedValue = JSON.parse(value);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
// Not JSON, use as string
|
|
83
|
+
}
|
|
84
|
+
setNestedValue(config, key, parsedValue);
|
|
85
|
+
const errors = config_1.ConfigLoader.validate(config);
|
|
86
|
+
if (errors.length > 0) {
|
|
87
|
+
(0, logger_1.error)('Configuration validation failed:');
|
|
88
|
+
errors.forEach(err => console.log(chalk_1.default.red(` - ${err}`)));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
config_1.ConfigLoader.save(config);
|
|
92
|
+
(0, logger_1.success)(`Set ${key} = ${JSON.stringify(parsedValue)}`);
|
|
93
|
+
}
|
|
94
|
+
function showConfigPath() {
|
|
95
|
+
const configPath = config_1.ConfigLoader.getConfigPath();
|
|
96
|
+
console.log(configPath);
|
|
97
|
+
}
|
|
98
|
+
function getNestedValue(obj, path) {
|
|
99
|
+
const keys = path.split('.');
|
|
100
|
+
let current = obj;
|
|
101
|
+
for (const key of keys) {
|
|
102
|
+
if (current === undefined || current === null) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
current = current[key];
|
|
106
|
+
}
|
|
107
|
+
return current;
|
|
108
|
+
}
|
|
109
|
+
function setNestedValue(obj, path, value) {
|
|
110
|
+
const keys = path.split('.');
|
|
111
|
+
let current = obj;
|
|
112
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
113
|
+
const key = keys[i];
|
|
114
|
+
if (current[key] === undefined || typeof current[key] !== 'object') {
|
|
115
|
+
current[key] = {};
|
|
116
|
+
}
|
|
117
|
+
current = current[key];
|
|
118
|
+
}
|
|
119
|
+
current[keys[keys.length - 1]] = value;
|
|
120
|
+
}
|