cloud-cost-cli 0.1.0 → 0.1.1
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
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# cloud-cost-cli
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/cloud-cost-cli)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
3
6
|
**Optimize your cloud spend in seconds.**
|
|
4
7
|
|
|
5
8
|
A command-line tool that analyzes your AWS, GCP, and Azure bills to identify cost-saving opportunities — idle resources, oversized instances, unattached volumes, and more.
|
|
@@ -17,11 +17,11 @@ async function scanCommand(options) {
|
|
|
17
17
|
(0, logger_1.error)(`Provider "${options.provider}" not yet supported. Use --provider aws`);
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
20
|
-
(0, logger_1.info)(`Scanning AWS account (profile: ${options.profile || 'default'}, region: ${options.region || 'us-east-1'})...`);
|
|
21
20
|
const client = new client_1.AWSClient({
|
|
22
21
|
region: options.region,
|
|
23
22
|
profile: options.profile,
|
|
24
23
|
});
|
|
24
|
+
(0, logger_1.info)(`Scanning AWS account (profile: ${options.profile || 'default'}, region: ${client.region})...`);
|
|
25
25
|
// Run analyzers in parallel
|
|
26
26
|
(0, logger_1.info)('Analyzing EC2 instances...');
|
|
27
27
|
const ec2Promise = (0, ec2_1.analyzeEC2Instances)(client);
|
|
@@ -18,6 +18,7 @@ export declare class AWSClient {
|
|
|
18
18
|
region: string;
|
|
19
19
|
profile: string;
|
|
20
20
|
constructor(options: AWSClientOptions);
|
|
21
|
+
private getProfileRegion;
|
|
21
22
|
getEC2Client(): EC2Client;
|
|
22
23
|
getCloudWatchClient(): CloudWatchClient;
|
|
23
24
|
getCostExplorerClient(): CostExplorerClient;
|
|
@@ -1,4 +1,37 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.AWSClient = void 0;
|
|
4
37
|
const client_ec2_1 = require("@aws-sdk/client-ec2");
|
|
@@ -8,6 +41,9 @@ const client_rds_1 = require("@aws-sdk/client-rds");
|
|
|
8
41
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
9
42
|
const client_elastic_load_balancing_v2_1 = require("@aws-sdk/client-elastic-load-balancing-v2");
|
|
10
43
|
const credential_providers_1 = require("@aws-sdk/credential-providers");
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const ini = __importStar(require("ini"));
|
|
11
47
|
class AWSClient {
|
|
12
48
|
ec2;
|
|
13
49
|
cloudwatch;
|
|
@@ -18,8 +54,9 @@ class AWSClient {
|
|
|
18
54
|
region;
|
|
19
55
|
profile;
|
|
20
56
|
constructor(options) {
|
|
21
|
-
this.region = options.region || 'us-east-1';
|
|
22
57
|
this.profile = options.profile || 'default';
|
|
58
|
+
// Determine region: CLI option > profile config > default
|
|
59
|
+
this.region = options.region || this.getProfileRegion(this.profile) || 'us-east-1';
|
|
23
60
|
// Use environment credentials if available, otherwise fall back to profile
|
|
24
61
|
const credentials = process.env.AWS_ACCESS_KEY_ID
|
|
25
62
|
? (0, credential_providers_1.fromEnv)()
|
|
@@ -31,6 +68,28 @@ class AWSClient {
|
|
|
31
68
|
this.s3 = new client_s3_1.S3Client({ region: this.region, credentials });
|
|
32
69
|
this.elb = new client_elastic_load_balancing_v2_1.ElasticLoadBalancingV2Client({ region: this.region, credentials });
|
|
33
70
|
}
|
|
71
|
+
getProfileRegion(profileName) {
|
|
72
|
+
try {
|
|
73
|
+
const configPath = path.join(process.env.HOME || process.env.USERPROFILE || '', '.aws', 'config');
|
|
74
|
+
if (!fs.existsSync(configPath)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
78
|
+
const config = ini.parse(configContent);
|
|
79
|
+
// Profile names in config are prefixed with "profile " except for "default"
|
|
80
|
+
const profileKey = profileName === 'default' ? 'default' : `profile ${profileName}`;
|
|
81
|
+
const profileConfig = config[profileKey];
|
|
82
|
+
// Handle both regular objects and null prototype objects
|
|
83
|
+
if (profileConfig && typeof profileConfig === 'object') {
|
|
84
|
+
return profileConfig.region || null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// If we can't read the config, just return null
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
34
93
|
getEC2Client() {
|
|
35
94
|
return this.ec2;
|
|
36
95
|
}
|
package/docs/RELEASE.md
CHANGED
|
@@ -16,16 +16,19 @@ npm now uses "Granular Access Tokens" instead of automation tokens.
|
|
|
16
16
|
- **Expiration**: 1 year (or No expiration)
|
|
17
17
|
- **Packages and scopes**: Select packages → Choose `cloud-cost-cli`
|
|
18
18
|
- **Permissions**: Read and write
|
|
19
|
+
- **⚠️ IMPORTANT**: Under "Additional options" → **Disable "Require two-factor authentication for this token"**
|
|
20
|
+
- This allows GitHub Actions to publish without OTP
|
|
21
|
+
- The token itself is still secure (stored in GitHub Secrets)
|
|
19
22
|
4. Click **"Generate Token"**
|
|
20
23
|
5. Copy the token (starts with `npm_...`)
|
|
21
24
|
|
|
22
25
|
### 2. Add Token to GitHub Secrets
|
|
23
26
|
|
|
24
27
|
1. Go to https://github.com/vuhp/cloud-cost-cli/settings/secrets/actions
|
|
25
|
-
2. Click **"New repository secret"**
|
|
28
|
+
2. Click **"New repository secret"** (or edit existing `NPM_TOKEN`)
|
|
26
29
|
3. Name: `NPM_TOKEN`
|
|
27
|
-
4. Value: (paste the `npm_...` token)
|
|
28
|
-
5. Click **"Add secret"**
|
|
30
|
+
4. Value: (paste the `npm_...` token with 2FA disabled)
|
|
31
|
+
5. Click **"Add secret"** or **"Update secret"**
|
|
29
32
|
|
|
30
33
|
## Steps to Release
|
|
31
34
|
|