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 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**: [AWS CLI configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) or environment variables
66
- - **Azure**: [Azure CLI logged in](https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli) or environment variables
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
- # Set Azure subscription ID (or use --subscription-id flag)
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
- Summary: 47 resources analyzed | 12 idle | 8 oversized | 5 unused
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
- 💡 Note: Cost estimates based on us-east-1 pricing and may vary by region.
132
- For more accurate estimates, actual costs depend on your usage and region.
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.1.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
+ }
@@ -0,0 +1,6 @@
1
+ interface CostsCommandOptions {
2
+ days?: string;
3
+ clear?: boolean;
4
+ }
5
+ export declare function costsCommand(options: CostsCommandOptions): Promise<void>;
6
+ export {};