infra-cost 1.6.0 → 1.8.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.
Files changed (3) hide show
  1. package/dist/cli/index.js +1687 -356
  2. package/dist/index.js +1683 -351
  3. package/package.json +8 -1
package/dist/cli/index.js CHANGED
@@ -8,6 +8,9 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
8
8
  var __esm = (fn, res) => function __init() {
9
9
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
10
  };
11
+ var __commonJS = (cb, mod) => function __require() {
12
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
13
+ };
11
14
  var __export = (target, all) => {
12
15
  for (var name in all)
13
16
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -30,6 +33,171 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
33
  ));
31
34
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
35
 
36
+ // package.json
37
+ var require_package = __commonJS({
38
+ "package.json"(exports, module2) {
39
+ module2.exports = {
40
+ name: "infra-cost",
41
+ version: "1.8.0",
42
+ description: "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
43
+ keywords: [
44
+ "aws",
45
+ "gcp",
46
+ "azure",
47
+ "cloud-cost",
48
+ "finops",
49
+ "cost-optimization",
50
+ "multi-cloud",
51
+ "cost-analysis",
52
+ "infrastructure",
53
+ "cloud-billing",
54
+ "cost-management",
55
+ "devops",
56
+ "cli-tool",
57
+ "cost-monitoring",
58
+ "budget-tracking"
59
+ ],
60
+ author: {
61
+ name: "Code Collab",
62
+ email: "codecollab.co@gmail.com",
63
+ url: "https://github.com/codecollab-co/infra-cost"
64
+ },
65
+ files: [
66
+ "!tests/**/*",
67
+ "dist/**/*",
68
+ "!dist/**/*.js.map",
69
+ "bin/**/*"
70
+ ],
71
+ bin: {
72
+ "infra-cost": "./bin/index.js",
73
+ "aws-cost": "./bin/index.js"
74
+ },
75
+ main: "./dist/index.js",
76
+ scripts: {
77
+ prebuild: "run-s clean",
78
+ build: "tsup",
79
+ clean: "rm -rf dist",
80
+ typecheck: "tsc --noEmit",
81
+ lint: "eslint src --ext .ts",
82
+ "lint:fix": "eslint src --ext .ts --fix",
83
+ test: "jest",
84
+ "test:watch": "jest --watch",
85
+ "test:coverage": "jest --coverage",
86
+ dev: "tsup --watch",
87
+ "version:check": `echo "Current version: $(npm pkg get version | tr -d '"')"`,
88
+ "version:next": "npm version patch --no-git-tag-version",
89
+ "version:bump:patch": "npm version patch",
90
+ "version:bump:minor": "npm version minor",
91
+ "version:bump:major": "npm version major",
92
+ "publish:dry": "npm publish --dry-run",
93
+ "publish:latest": "npm publish",
94
+ "publish:beta": "npm publish --tag beta",
95
+ "prepare-release": "npm run build && npm run test && npm run version:bump:patch",
96
+ postpublish: 'echo "\u{1F389} Published $(npm pkg get name)@$(npm pkg get version) to npm!"',
97
+ prepublishOnly: "npm run build"
98
+ },
99
+ repository: {
100
+ type: "git",
101
+ url: "https://github.com/codecollab-co/infra-cost.git"
102
+ },
103
+ bugs: {
104
+ url: "https://github.com/codecollab-co/infra-cost/issues"
105
+ },
106
+ homepage: "https://github.com/codecollab-co/infra-cost#readme",
107
+ license: "MIT",
108
+ engines: {
109
+ node: ">=20.0.0",
110
+ npm: ">=10.0.0"
111
+ },
112
+ dependencies: {
113
+ "@alicloud/bssopenapi20171214": "^2.0.1",
114
+ "@alicloud/cs20151215": "^4.0.1",
115
+ "@alicloud/ecs20140526": "^4.0.3",
116
+ "@alicloud/oss20190517": "^1.0.6",
117
+ "@alicloud/rds20140815": "^3.0.2",
118
+ "@alicloud/tea-util": "^1.4.7",
119
+ "@aws-sdk/client-budgets": "^3.975.0",
120
+ "@aws-sdk/client-cost-explorer": "^3.975.0",
121
+ "@aws-sdk/client-ec2": "^3.975.0",
122
+ "@aws-sdk/client-elastic-load-balancing-v2": "^3.975.0",
123
+ "@aws-sdk/client-iam": "^3.975.0",
124
+ "@aws-sdk/client-lambda": "^3.975.0",
125
+ "@aws-sdk/client-rds": "^3.975.0",
126
+ "@aws-sdk/client-s3": "^3.975.0",
127
+ "@aws-sdk/client-sts": "^3.975.0",
128
+ "@aws-sdk/credential-providers": "^3.975.0",
129
+ "@azure/arm-compute": "^23.3.0",
130
+ "@azure/arm-consumption": "^9.2.1",
131
+ "@azure/arm-containerservice": "^24.1.0",
132
+ "@azure/arm-costmanagement": "^1.0.0-beta.2",
133
+ "@azure/arm-network": "^35.0.0",
134
+ "@azure/arm-sql": "^10.0.0",
135
+ "@azure/arm-storage": "^19.1.0",
136
+ "@azure/arm-subscriptions": "^6.0.0",
137
+ "@azure/identity": "^4.13.0",
138
+ "@google-cloud/bigquery": "^8.1.1",
139
+ "@google-cloud/billing": "^5.1.1",
140
+ "@google-cloud/compute": "^6.7.0",
141
+ "@google-cloud/container": "^6.6.0",
142
+ "@google-cloud/monitoring": "^5.3.1",
143
+ "@google-cloud/resource-manager": "^6.2.1",
144
+ "@google-cloud/sql": "^0.24.0",
145
+ "@google-cloud/storage": "^7.18.0",
146
+ "@slack/web-api": "^7.5.0",
147
+ callsites: "^3.1.0",
148
+ chalk: "^4.1.2",
149
+ "cli-progress": "^3.12.0",
150
+ "cli-table3": "^0.6.5",
151
+ commander: "^12.1.0",
152
+ cors: "^2.8.6",
153
+ dayjs: "^1.11.19",
154
+ exceljs: "^4.4.0",
155
+ express: "^5.2.1",
156
+ "express-rate-limit": "^8.2.1",
157
+ "fd-slicer": "^1.1.0",
158
+ "google-auth-library": "^10.5.0",
159
+ googleapis: "^171.0.0",
160
+ helmet: "^8.1.0",
161
+ ini: "^6.0.0",
162
+ ink: "^6.6.0",
163
+ moment: "^2.30.1",
164
+ "node-fetch": "^2.7.0",
165
+ "oci-budget": "^2.88.0",
166
+ "oci-common": "^2.88.0",
167
+ "oci-containerengine": "^2.88.0",
168
+ "oci-core": "^2.88.0",
169
+ "oci-database": "^2.88.0",
170
+ "oci-identity": "^2.88.0",
171
+ "oci-objectstorage": "^2.88.0",
172
+ "oci-usageapi": "^2.88.0",
173
+ ora: "^9.1.0",
174
+ pako: "^2.1.0",
175
+ pend: "^1.2.0",
176
+ react: "^19.2.4",
177
+ "swagger-jsdoc": "^6.2.8",
178
+ "swagger-ui-express": "^5.0.1",
179
+ yauzl: "^3.0.0",
180
+ zod: "^3.23.8"
181
+ },
182
+ devDependencies: {
183
+ "@types/cors": "^2.8.19",
184
+ "@types/express": "^5.0.6",
185
+ "@types/jest": "^29.5.12",
186
+ "@types/node": "^22.5.4",
187
+ "@types/yauzl": "^2.10.3",
188
+ "@typescript-eslint/eslint-plugin": "^8.5.0",
189
+ "@typescript-eslint/parser": "^8.5.0",
190
+ eslint: "^8.57.0",
191
+ jest: "^29.7.0",
192
+ "npm-run-all": "^4.1.5",
193
+ "ts-jest": "^29.2.5",
194
+ tsup: "^6.7.0",
195
+ typescript: "^5.6.2"
196
+ }
197
+ };
198
+ }
199
+ });
200
+
33
201
  // src/core/logging/structured-logger.ts
34
202
  function initializeLogger(config) {
35
203
  globalLogger = new StructuredLogger(config);
@@ -565,6 +733,209 @@ var init_schema = __esm({
565
733
  }
566
734
  });
567
735
 
736
+ // src/core/config/loader.ts
737
+ function discoverConfigFile() {
738
+ for (const configPath of CONFIG_PATHS) {
739
+ if ((0, import_fs.existsSync)(configPath)) {
740
+ return configPath;
741
+ }
742
+ }
743
+ return null;
744
+ }
745
+ function loadConfigFile(configPath) {
746
+ try {
747
+ const content = (0, import_fs.readFileSync)(configPath, "utf8");
748
+ const config = JSON.parse(content);
749
+ if (config.profiles && config.defaults?.profile) {
750
+ const activeProfile = config.profiles[config.defaults.profile];
751
+ if (activeProfile) {
752
+ return mergeConfigs(config.defaults, activeProfile);
753
+ }
754
+ }
755
+ return config.defaults || config;
756
+ } catch (error) {
757
+ console.warn(`Warning: Could not load config from ${configPath}: ${error.message}`);
758
+ return {};
759
+ }
760
+ }
761
+ function resolveEnvVars(config) {
762
+ const resolved = JSON.parse(JSON.stringify(config));
763
+ if (process.env.AWS_ACCESS_KEY_ID) {
764
+ resolved.accessKey = process.env.AWS_ACCESS_KEY_ID;
765
+ }
766
+ if (process.env.AWS_SECRET_ACCESS_KEY) {
767
+ resolved.secretKey = process.env.AWS_SECRET_ACCESS_KEY;
768
+ }
769
+ if (process.env.AWS_SESSION_TOKEN) {
770
+ resolved.sessionToken = process.env.AWS_SESSION_TOKEN;
771
+ }
772
+ if (process.env.AWS_REGION) {
773
+ resolved.region = process.env.AWS_REGION;
774
+ }
775
+ if (process.env.AWS_PROFILE) {
776
+ resolved.profile = process.env.AWS_PROFILE;
777
+ }
778
+ if (process.env.SLACK_TOKEN || process.env.SLACK_CHANNEL) {
779
+ resolved.slack = {
780
+ ...resolved.slack,
781
+ token: process.env.SLACK_TOKEN ?? resolved.slack?.token,
782
+ channel: process.env.SLACK_CHANNEL ?? resolved.slack?.channel,
783
+ enabled: true
784
+ };
785
+ }
786
+ return resolved;
787
+ }
788
+ function mergeConfigs(...configs) {
789
+ const result = {};
790
+ for (const config of configs) {
791
+ for (const [key, value] of Object.entries(config)) {
792
+ if (value === void 0 || value === null) {
793
+ continue;
794
+ }
795
+ if (typeof value === "object" && !Array.isArray(value)) {
796
+ result[key] = { ...result[key], ...value };
797
+ } else {
798
+ result[key] = value;
799
+ }
800
+ }
801
+ }
802
+ return result;
803
+ }
804
+ function autoLoadConfig(cliOptions = {}) {
805
+ let config = { ...DEFAULT_CONFIG2 };
806
+ const configPath = cliOptions.configFile || discoverConfigFile();
807
+ if (configPath) {
808
+ const fileConfig = loadConfigFile(configPath);
809
+ config = mergeConfigs(config, fileConfig);
810
+ }
811
+ config = mergeConfigs(config, resolveEnvVars(config));
812
+ const cliConfig = mapCliOptionsToConfig(cliOptions);
813
+ config = mergeConfigs(config, cliConfig);
814
+ return config;
815
+ }
816
+ function mapCliOptionsToConfig(options) {
817
+ const config = {};
818
+ if (options.provider)
819
+ config.provider = options.provider;
820
+ if (options.profile)
821
+ config.profile = options.profile;
822
+ if (options.region)
823
+ config.region = options.region;
824
+ if (options.accessKey)
825
+ config.accessKey = options.accessKey;
826
+ if (options.secretKey)
827
+ config.secretKey = options.secretKey;
828
+ if (options.sessionToken)
829
+ config.sessionToken = options.sessionToken;
830
+ if (options.json || options.text || options.summary) {
831
+ config.output = config.output || {};
832
+ if (options.json)
833
+ config.output.format = "json";
834
+ if (options.text)
835
+ config.output.format = "text";
836
+ if (options.summary)
837
+ config.output.summary = true;
838
+ }
839
+ if (options.delta !== void 0) {
840
+ config.output = config.output || {};
841
+ config.output.showDelta = options.delta;
842
+ }
843
+ if (options.deltaThreshold) {
844
+ config.output = config.output || {};
845
+ config.output.deltaThreshold = parseFloat(options.deltaThreshold);
846
+ }
847
+ if (options.quickWins !== void 0) {
848
+ config.output = config.output || {};
849
+ config.output.showQuickWins = options.quickWins;
850
+ }
851
+ if (options.quickWinsCount) {
852
+ config.output = config.output || {};
853
+ config.output.quickWinsCount = parseInt(options.quickWinsCount, 10);
854
+ }
855
+ if (options.cache !== void 0 || options.noCache !== void 0) {
856
+ config.cache = config.cache || {};
857
+ config.cache.enabled = options.cache === true || options.noCache !== true;
858
+ }
859
+ if (options.cacheTtl) {
860
+ config.cache = config.cache || {};
861
+ config.cache.ttl = options.cacheTtl;
862
+ }
863
+ if (options.cacheType) {
864
+ config.cache = config.cache || {};
865
+ config.cache.type = options.cacheType;
866
+ }
867
+ if (options.slackToken || options.slackChannel) {
868
+ config.slack = config.slack || {};
869
+ if (options.slackToken)
870
+ config.slack.token = options.slackToken;
871
+ if (options.slackChannel)
872
+ config.slack.channel = options.slackChannel;
873
+ config.slack.enabled = true;
874
+ }
875
+ if (options.logLevel || options.verbose || options.quiet) {
876
+ config.logging = config.logging || {};
877
+ if (options.logLevel)
878
+ config.logging.level = options.logLevel;
879
+ if (options.verbose)
880
+ config.logging.level = "debug";
881
+ if (options.quiet)
882
+ config.logging.level = "error";
883
+ }
884
+ return config;
885
+ }
886
+ var import_fs, import_path, import_os, CONFIG_PATHS, DEFAULT_CONFIG2;
887
+ var init_loader = __esm({
888
+ "src/core/config/loader.ts"() {
889
+ import_fs = require("fs");
890
+ import_path = require("path");
891
+ import_os = require("os");
892
+ CONFIG_PATHS = [
893
+ (0, import_path.join)(process.cwd(), "infra-cost.config.json"),
894
+ // Project-specific
895
+ (0, import_path.join)(process.cwd(), ".infra-cost.config.json"),
896
+ // Project-specific (hidden)
897
+ (0, import_path.join)(process.cwd(), ".infra-cost", "config.json"),
898
+ // Project directory
899
+ (0, import_path.join)((0, import_os.homedir)(), ".infra-cost", "config.json"),
900
+ // User global
901
+ (0, import_path.join)((0, import_os.homedir)(), ".config", "infra-cost", "config.json")
902
+ // XDG standard
903
+ ];
904
+ DEFAULT_CONFIG2 = {
905
+ provider: "aws",
906
+ profile: "default",
907
+ region: "us-east-1",
908
+ output: {
909
+ format: "fancy",
910
+ summary: false,
911
+ showDelta: true,
912
+ // NEW: Show delta by default
913
+ showQuickWins: true,
914
+ // NEW: Show quick wins by default
915
+ deltaThreshold: 10,
916
+ quickWinsCount: 3
917
+ },
918
+ cache: {
919
+ enabled: true,
920
+ // NEW: Cache enabled by default
921
+ ttl: "4h",
922
+ type: "file"
923
+ },
924
+ logging: {
925
+ level: "info",
926
+ format: "pretty",
927
+ auditEnabled: false
928
+ }
929
+ };
930
+ __name(discoverConfigFile, "discoverConfigFile");
931
+ __name(loadConfigFile, "loadConfigFile");
932
+ __name(resolveEnvVars, "resolveEnvVars");
933
+ __name(mergeConfigs, "mergeConfigs");
934
+ __name(autoLoadConfig, "autoLoadConfig");
935
+ __name(mapCliOptionsToConfig, "mapCliOptionsToConfig");
936
+ }
937
+ });
938
+
568
939
  // src/types/providers.ts
569
940
  var CloudProviderAdapter, ResourceType;
570
941
  var init_providers = __esm({
@@ -10208,372 +10579,675 @@ var init_multicloud = __esm({
10208
10579
  }
10209
10580
  });
10210
10581
 
10211
- // src/cli/index.ts
10212
- var cli_exports = {};
10213
- __export(cli_exports, {
10214
- createCLI: () => createCLI,
10215
- main: () => main
10582
+ // src/api/utils.ts
10583
+ async function getProviderFromConfig() {
10584
+ const config = autoLoadConfig();
10585
+ const factory = new CloudProviderFactory();
10586
+ return factory.createProvider(config);
10587
+ }
10588
+ function getConfig2() {
10589
+ return autoLoadConfig();
10590
+ }
10591
+ var init_utils = __esm({
10592
+ "src/api/utils.ts"() {
10593
+ init_factory();
10594
+ init_loader();
10595
+ __name(getProviderFromConfig, "getProviderFromConfig");
10596
+ __name(getConfig2, "getConfig");
10597
+ }
10216
10598
  });
10217
- module.exports = __toCommonJS(cli_exports);
10218
- var import_commander = require("commander");
10219
10599
 
10220
- // package.json
10221
- var package_default = {
10222
- name: "infra-cost",
10223
- version: "1.6.0",
10224
- description: "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
10225
- keywords: [
10226
- "aws",
10227
- "gcp",
10228
- "azure",
10229
- "cloud-cost",
10230
- "finops",
10231
- "cost-optimization",
10232
- "multi-cloud",
10233
- "cost-analysis",
10234
- "infrastructure",
10235
- "cloud-billing",
10236
- "cost-management",
10237
- "devops",
10238
- "cli-tool",
10239
- "cost-monitoring",
10240
- "budget-tracking"
10241
- ],
10242
- author: {
10243
- name: "Code Collab",
10244
- email: "codecollab.co@gmail.com",
10245
- url: "https://github.com/codecollab-co/infra-cost"
10246
- },
10247
- files: [
10248
- "!tests/**/*",
10249
- "dist/**/*",
10250
- "!dist/**/*.js.map",
10251
- "bin/**/*"
10252
- ],
10253
- bin: {
10254
- "infra-cost": "./bin/index.js",
10255
- "aws-cost": "./bin/index.js"
10256
- },
10257
- main: "./dist/index.js",
10258
- scripts: {
10259
- prebuild: "run-s clean",
10260
- build: "tsup",
10261
- clean: "rm -rf dist",
10262
- typecheck: "tsc --noEmit",
10263
- lint: "eslint src --ext .ts",
10264
- "lint:fix": "eslint src --ext .ts --fix",
10265
- test: "jest",
10266
- "test:watch": "jest --watch",
10267
- "test:coverage": "jest --coverage",
10268
- dev: "tsup --watch",
10269
- "version:check": `echo "Current version: $(npm pkg get version | tr -d '"')"`,
10270
- "version:next": "npm version patch --no-git-tag-version",
10271
- "version:bump:patch": "npm version patch",
10272
- "version:bump:minor": "npm version minor",
10273
- "version:bump:major": "npm version major",
10274
- "publish:dry": "npm publish --dry-run",
10275
- "publish:latest": "npm publish",
10276
- "publish:beta": "npm publish --tag beta",
10277
- "prepare-release": "npm run build && npm run test && npm run version:bump:patch",
10278
- postpublish: 'echo "\u{1F389} Published $(npm pkg get name)@$(npm pkg get version) to npm!"',
10279
- prepublishOnly: "npm run build"
10280
- },
10281
- repository: {
10282
- type: "git",
10283
- url: "https://github.com/codecollab-co/infra-cost.git"
10284
- },
10285
- bugs: {
10286
- url: "https://github.com/codecollab-co/infra-cost/issues"
10287
- },
10288
- homepage: "https://github.com/codecollab-co/infra-cost#readme",
10289
- license: "MIT",
10290
- engines: {
10291
- node: ">=20.0.0",
10292
- npm: ">=10.0.0"
10293
- },
10294
- dependencies: {
10295
- "@alicloud/bssopenapi20171214": "^2.0.1",
10296
- "@alicloud/cs20151215": "^4.0.1",
10297
- "@alicloud/ecs20140526": "^4.0.3",
10298
- "@alicloud/oss20190517": "^1.0.6",
10299
- "@alicloud/rds20140815": "^3.0.2",
10300
- "@alicloud/tea-util": "^1.4.7",
10301
- "@aws-sdk/client-budgets": "^3.975.0",
10302
- "@aws-sdk/client-cost-explorer": "^3.975.0",
10303
- "@aws-sdk/client-ec2": "^3.975.0",
10304
- "@aws-sdk/client-elastic-load-balancing-v2": "^3.975.0",
10305
- "@aws-sdk/client-iam": "^3.975.0",
10306
- "@aws-sdk/client-lambda": "^3.975.0",
10307
- "@aws-sdk/client-rds": "^3.975.0",
10308
- "@aws-sdk/client-s3": "^3.975.0",
10309
- "@aws-sdk/client-sts": "^3.975.0",
10310
- "@aws-sdk/credential-providers": "^3.975.0",
10311
- "@azure/arm-compute": "^23.3.0",
10312
- "@azure/arm-consumption": "^9.2.1",
10313
- "@azure/arm-containerservice": "^24.1.0",
10314
- "@azure/arm-costmanagement": "^1.0.0-beta.2",
10315
- "@azure/arm-network": "^35.0.0",
10316
- "@azure/arm-sql": "^10.0.0",
10317
- "@azure/arm-storage": "^19.1.0",
10318
- "@azure/arm-subscriptions": "^6.0.0",
10319
- "@azure/identity": "^4.13.0",
10320
- "@google-cloud/bigquery": "^8.1.1",
10321
- "@google-cloud/billing": "^5.1.1",
10322
- "@google-cloud/compute": "^6.7.0",
10323
- "@google-cloud/container": "^6.6.0",
10324
- "@google-cloud/monitoring": "^5.3.1",
10325
- "@google-cloud/resource-manager": "^6.2.1",
10326
- "@google-cloud/sql": "^0.24.0",
10327
- "@google-cloud/storage": "^7.18.0",
10328
- "@slack/web-api": "^7.5.0",
10329
- callsites: "^3.1.0",
10330
- chalk: "^4.1.2",
10331
- "cli-progress": "^3.12.0",
10332
- "cli-table3": "^0.6.5",
10333
- commander: "^12.1.0",
10334
- dayjs: "^1.11.19",
10335
- exceljs: "^4.4.0",
10336
- express: "^5.2.1",
10337
- "fd-slicer": "^1.1.0",
10338
- "google-auth-library": "^10.5.0",
10339
- googleapis: "^171.0.0",
10340
- ini: "^6.0.0",
10341
- ink: "^6.6.0",
10342
- moment: "^2.30.1",
10343
- "node-fetch": "^2.7.0",
10344
- "oci-budget": "^2.88.0",
10345
- "oci-common": "^2.88.0",
10346
- "oci-containerengine": "^2.88.0",
10347
- "oci-core": "^2.88.0",
10348
- "oci-database": "^2.88.0",
10349
- "oci-identity": "^2.88.0",
10350
- "oci-objectstorage": "^2.88.0",
10351
- "oci-usageapi": "^2.88.0",
10352
- ora: "^9.1.0",
10353
- pako: "^2.1.0",
10354
- pend: "^1.2.0",
10355
- react: "^19.2.4",
10356
- yauzl: "^3.0.0",
10357
- zod: "^3.23.8"
10358
- },
10359
- devDependencies: {
10360
- "@types/jest": "^29.5.12",
10361
- "@types/node": "^22.5.4",
10362
- "@types/yauzl": "^2.10.3",
10363
- "@typescript-eslint/eslint-plugin": "^8.5.0",
10364
- "@typescript-eslint/parser": "^8.5.0",
10365
- eslint: "^8.57.0",
10366
- jest: "^29.7.0",
10367
- "npm-run-all": "^4.1.5",
10368
- "ts-jest": "^29.2.5",
10369
- tsup: "^6.7.0",
10370
- typescript: "^5.6.2"
10600
+ // src/api/routes/costs.ts
10601
+ var costs_exports2 = {};
10602
+ __export(costs_exports2, {
10603
+ default: () => costs_default
10604
+ });
10605
+ var import_express, router, costs_default;
10606
+ var init_costs2 = __esm({
10607
+ "src/api/routes/costs.ts"() {
10608
+ import_express = require("express");
10609
+ init_utils();
10610
+ init_server();
10611
+ router = (0, import_express.Router)();
10612
+ router.get("/", async (req, res) => {
10613
+ try {
10614
+ const config = getConfig2();
10615
+ const provider = await getProviderFromConfig();
10616
+ const today = /* @__PURE__ */ new Date();
10617
+ const todayStart = new Date(today.setHours(0, 0, 0, 0));
10618
+ const todayEnd = new Date(today.setHours(23, 59, 59, 999));
10619
+ const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
10620
+ const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0);
10621
+ const [todayCosts, mtdCosts] = await Promise.all([
10622
+ provider.getCostBreakdown(todayStart, todayEnd, "SERVICE"),
10623
+ provider.getCostBreakdown(monthStart, monthEnd, "SERVICE")
10624
+ ]);
10625
+ const serviceBreakdown = {};
10626
+ todayCosts.breakdown.forEach((item) => {
10627
+ serviceBreakdown[item.service] = item.cost;
10628
+ });
10629
+ const accountInfo = await provider.getAccountInfo();
10630
+ const response = {
10631
+ account: {
10632
+ id: accountInfo.accountId,
10633
+ name: accountInfo.accountAlias || accountInfo.accountId,
10634
+ provider: config.provider
10635
+ },
10636
+ costs: {
10637
+ today: {
10638
+ total: todayCosts.totalCost,
10639
+ currency: "USD",
10640
+ byService: serviceBreakdown
10641
+ },
10642
+ mtd: {
10643
+ total: mtdCosts.totalCost,
10644
+ projected: mtdCosts.totalCost * (30 / today.getDate())
10645
+ },
10646
+ delta: {
10647
+ vsYesterday: 0,
10648
+ // Would need historical data
10649
+ vsLastWeek: 0
10650
+ // Would need historical data
10651
+ }
10652
+ },
10653
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10654
+ };
10655
+ res.json(createApiResponse(response));
10656
+ } catch (error) {
10657
+ res.status(500).json(createErrorResponse("FETCH_ERROR", error.message));
10658
+ }
10659
+ });
10660
+ router.get("/services", async (req, res) => {
10661
+ try {
10662
+ const provider = await getProviderFromConfig();
10663
+ const { startDate, endDate } = req.query;
10664
+ const start = startDate ? new Date(startDate) : /* @__PURE__ */ new Date();
10665
+ const end = endDate ? new Date(endDate) : /* @__PURE__ */ new Date();
10666
+ const breakdown = await provider.getCostBreakdown(start, end, "SERVICE");
10667
+ const services = breakdown.breakdown.map((item) => ({
10668
+ service: item.service,
10669
+ cost: item.cost,
10670
+ percentage: item.cost / breakdown.totalCost * 100
10671
+ }));
10672
+ res.json(
10673
+ createApiResponse({
10674
+ total: breakdown.totalCost,
10675
+ services,
10676
+ period: {
10677
+ start: start.toISOString(),
10678
+ end: end.toISOString()
10679
+ }
10680
+ })
10681
+ );
10682
+ } catch (error) {
10683
+ res.status(500).json(createErrorResponse("FETCH_ERROR", error.message));
10684
+ }
10685
+ });
10686
+ router.get("/daily", async (req, res) => {
10687
+ try {
10688
+ const provider = await getProviderFromConfig();
10689
+ const { days = 30 } = req.query;
10690
+ const daysNum = parseInt(days, 10);
10691
+ const end = /* @__PURE__ */ new Date();
10692
+ const start = /* @__PURE__ */ new Date();
10693
+ start.setDate(start.getDate() - daysNum);
10694
+ const breakdown = await provider.getCostBreakdown(start, end, "DAILY");
10695
+ const dailyCosts = breakdown.breakdown.map((item) => ({
10696
+ date: item.date,
10697
+ cost: item.cost
10698
+ }));
10699
+ res.json(
10700
+ createApiResponse({
10701
+ total: breakdown.totalCost,
10702
+ daily: dailyCosts,
10703
+ period: {
10704
+ start: start.toISOString(),
10705
+ end: end.toISOString(),
10706
+ days: daysNum
10707
+ }
10708
+ })
10709
+ );
10710
+ } catch (error) {
10711
+ res.status(500).json(createErrorResponse("FETCH_ERROR", error.message));
10712
+ }
10713
+ });
10714
+ router.get("/trends", async (req, res) => {
10715
+ try {
10716
+ const provider = await getProviderFromConfig();
10717
+ const end = /* @__PURE__ */ new Date();
10718
+ const start = /* @__PURE__ */ new Date();
10719
+ start.setMonth(start.getMonth() - 3);
10720
+ const breakdown = await provider.getCostBreakdown(start, end, "MONTHLY");
10721
+ const trends = breakdown.breakdown.map((item, index, arr) => {
10722
+ const previousMonth = index > 0 ? arr[index - 1].cost : null;
10723
+ const changePercent = previousMonth ? (item.cost - previousMonth) / previousMonth * 100 : 0;
10724
+ return {
10725
+ month: item.date,
10726
+ cost: item.cost,
10727
+ change: item.cost - (previousMonth || 0),
10728
+ changePercent
10729
+ };
10730
+ });
10731
+ res.json(
10732
+ createApiResponse({
10733
+ trends,
10734
+ average: breakdown.totalCost / breakdown.breakdown.length,
10735
+ period: {
10736
+ start: start.toISOString(),
10737
+ end: end.toISOString()
10738
+ }
10739
+ })
10740
+ );
10741
+ } catch (error) {
10742
+ res.status(500).json(createErrorResponse("FETCH_ERROR", error.message));
10743
+ }
10744
+ });
10745
+ costs_default = router;
10371
10746
  }
10372
- };
10373
-
10374
- // src/cli/index.ts
10375
- init_logging();
10376
-
10377
- // src/core/config/index.ts
10378
- init_schema();
10747
+ });
10379
10748
 
10380
- // src/core/config/loader.ts
10381
- var import_fs = require("fs");
10382
- var import_path = require("path");
10383
- var import_os = require("os");
10384
- var CONFIG_PATHS = [
10385
- (0, import_path.join)(process.cwd(), "infra-cost.config.json"),
10386
- // Project-specific
10387
- (0, import_path.join)(process.cwd(), ".infra-cost.config.json"),
10388
- // Project-specific (hidden)
10389
- (0, import_path.join)(process.cwd(), ".infra-cost", "config.json"),
10390
- // Project directory
10391
- (0, import_path.join)((0, import_os.homedir)(), ".infra-cost", "config.json"),
10392
- // User global
10393
- (0, import_path.join)((0, import_os.homedir)(), ".config", "infra-cost", "config.json")
10394
- // XDG standard
10395
- ];
10396
- var DEFAULT_CONFIG2 = {
10397
- provider: "aws",
10398
- profile: "default",
10399
- region: "us-east-1",
10400
- output: {
10401
- format: "fancy",
10402
- summary: false,
10403
- showDelta: true,
10404
- // NEW: Show delta by default
10405
- showQuickWins: true,
10406
- // NEW: Show quick wins by default
10407
- deltaThreshold: 10,
10408
- quickWinsCount: 3
10409
- },
10410
- cache: {
10411
- enabled: true,
10412
- // NEW: Cache enabled by default
10413
- ttl: "4h",
10414
- type: "file"
10415
- },
10416
- logging: {
10417
- level: "info",
10418
- format: "pretty",
10419
- auditEnabled: false
10749
+ // src/api/routes/inventory.ts
10750
+ var inventory_exports2 = {};
10751
+ __export(inventory_exports2, {
10752
+ default: () => inventory_default
10753
+ });
10754
+ var import_express2, router2, inventory_default;
10755
+ var init_inventory6 = __esm({
10756
+ "src/api/routes/inventory.ts"() {
10757
+ import_express2 = require("express");
10758
+ init_utils();
10759
+ init_server();
10760
+ router2 = (0, import_express2.Router)();
10761
+ router2.get("/", async (req, res) => {
10762
+ try {
10763
+ const provider = await getProviderFromConfig();
10764
+ const inventory = await provider.getResourceInventory();
10765
+ const summary = {
10766
+ totalResources: inventory.resources.length,
10767
+ byType: inventory.resources.reduce((acc, resource) => {
10768
+ acc[resource.type] = (acc[resource.type] || 0) + 1;
10769
+ return acc;
10770
+ }, {}),
10771
+ byRegion: inventory.resources.reduce((acc, resource) => {
10772
+ const region = resource.region || "global";
10773
+ acc[region] = (acc[region] || 0) + 1;
10774
+ return acc;
10775
+ }, {})
10776
+ };
10777
+ res.json(
10778
+ createApiResponse({
10779
+ summary,
10780
+ resources: inventory.resources
10781
+ })
10782
+ );
10783
+ } catch (error) {
10784
+ res.status(500).json(createErrorResponse("FETCH_ERROR", error.message));
10785
+ }
10786
+ });
10787
+ inventory_default = router2;
10420
10788
  }
10421
- };
10422
- function discoverConfigFile() {
10423
- for (const configPath of CONFIG_PATHS) {
10424
- if ((0, import_fs.existsSync)(configPath)) {
10425
- return configPath;
10426
- }
10427
- }
10428
- return null;
10429
- }
10430
- __name(discoverConfigFile, "discoverConfigFile");
10431
- function loadConfigFile(configPath) {
10432
- try {
10433
- const content = (0, import_fs.readFileSync)(configPath, "utf8");
10434
- const config = JSON.parse(content);
10435
- if (config.profiles && config.defaults?.profile) {
10436
- const activeProfile = config.profiles[config.defaults.profile];
10437
- if (activeProfile) {
10438
- return mergeConfigs(config.defaults, activeProfile);
10789
+ });
10790
+
10791
+ // src/api/routes/optimization.ts
10792
+ var optimization_exports = {};
10793
+ __export(optimization_exports, {
10794
+ default: () => optimization_default
10795
+ });
10796
+ var import_express3, router3, optimization_default;
10797
+ var init_optimization = __esm({
10798
+ "src/api/routes/optimization.ts"() {
10799
+ import_express3 = require("express");
10800
+ init_utils();
10801
+ init_server();
10802
+ router3 = (0, import_express3.Router)();
10803
+ router3.get("/", async (req, res) => {
10804
+ try {
10805
+ const provider = await getProviderFromConfig();
10806
+ const recommendations = await provider.getOptimizationRecommendations();
10807
+ const summary = {
10808
+ totalSavings: recommendations.reduce((sum, rec) => sum + (rec.estimatedMonthlySavings || 0), 0),
10809
+ recommendationCount: recommendations.length,
10810
+ byCategory: recommendations.reduce((acc, rec) => {
10811
+ const category = rec.category || "other";
10812
+ acc[category] = (acc[category] || 0) + 1;
10813
+ return acc;
10814
+ }, {})
10815
+ };
10816
+ res.json(
10817
+ createApiResponse({
10818
+ summary,
10819
+ recommendations
10820
+ })
10821
+ );
10822
+ } catch (error) {
10823
+ res.status(500).json(createErrorResponse("FETCH_ERROR", error.message));
10439
10824
  }
10440
- }
10441
- return config.defaults || config;
10442
- } catch (error) {
10443
- console.warn(`Warning: Could not load config from ${configPath}: ${error.message}`);
10444
- return {};
10445
- }
10446
- }
10447
- __name(loadConfigFile, "loadConfigFile");
10448
- function resolveEnvVars(config) {
10449
- const resolved = JSON.parse(JSON.stringify(config));
10450
- if (process.env.AWS_ACCESS_KEY_ID) {
10451
- resolved.accessKey = process.env.AWS_ACCESS_KEY_ID;
10452
- }
10453
- if (process.env.AWS_SECRET_ACCESS_KEY) {
10454
- resolved.secretKey = process.env.AWS_SECRET_ACCESS_KEY;
10455
- }
10456
- if (process.env.AWS_SESSION_TOKEN) {
10457
- resolved.sessionToken = process.env.AWS_SESSION_TOKEN;
10458
- }
10459
- if (process.env.AWS_REGION) {
10460
- resolved.region = process.env.AWS_REGION;
10825
+ });
10826
+ optimization_default = router3;
10461
10827
  }
10462
- if (process.env.AWS_PROFILE) {
10463
- resolved.profile = process.env.AWS_PROFILE;
10828
+ });
10829
+
10830
+ // src/api/routes/chargeback.ts
10831
+ var chargeback_exports = {};
10832
+ __export(chargeback_exports, {
10833
+ default: () => chargeback_default
10834
+ });
10835
+ var import_express4, router4, chargeback_default;
10836
+ var init_chargeback = __esm({
10837
+ "src/api/routes/chargeback.ts"() {
10838
+ import_express4 = require("express");
10839
+ init_utils();
10840
+ init_server();
10841
+ router4 = (0, import_express4.Router)();
10842
+ router4.get("/", async (req, res) => {
10843
+ try {
10844
+ const provider = await getProviderFromConfig();
10845
+ const { groupBy = "tag" } = req.query;
10846
+ const now = /* @__PURE__ */ new Date();
10847
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
10848
+ const breakdown = await provider.getCostBreakdown(
10849
+ monthStart,
10850
+ now,
10851
+ groupBy === "tag" ? "TAG" : "SERVICE"
10852
+ );
10853
+ res.json(
10854
+ createApiResponse({
10855
+ total: breakdown.totalCost,
10856
+ breakdown: breakdown.breakdown,
10857
+ period: {
10858
+ start: monthStart.toISOString(),
10859
+ end: now.toISOString()
10860
+ },
10861
+ groupBy
10862
+ })
10863
+ );
10864
+ } catch (error) {
10865
+ res.status(500).json(createErrorResponse("FETCH_ERROR", error.message));
10866
+ }
10867
+ });
10868
+ chargeback_default = router4;
10464
10869
  }
10465
- if (process.env.SLACK_TOKEN || process.env.SLACK_CHANNEL) {
10466
- resolved.slack = {
10467
- ...resolved.slack,
10468
- token: process.env.SLACK_TOKEN ?? resolved.slack?.token,
10469
- channel: process.env.SLACK_CHANNEL ?? resolved.slack?.channel,
10470
- enabled: true
10471
- };
10870
+ });
10871
+
10872
+ // src/api/routes/forecast.ts
10873
+ var forecast_exports = {};
10874
+ __export(forecast_exports, {
10875
+ default: () => forecast_default
10876
+ });
10877
+ var import_express5, router5, forecast_default;
10878
+ var init_forecast = __esm({
10879
+ "src/api/routes/forecast.ts"() {
10880
+ import_express5 = require("express");
10881
+ init_server();
10882
+ router5 = (0, import_express5.Router)();
10883
+ router5.get("/", async (req, res) => {
10884
+ try {
10885
+ const { days = 30 } = req.query;
10886
+ const daysNum = parseInt(days, 10);
10887
+ const currentDailyAverage = 150;
10888
+ const projectedDaily = currentDailyAverage * 1.05;
10889
+ const forecast = [];
10890
+ for (let i = 1; i <= daysNum; i++) {
10891
+ const date = /* @__PURE__ */ new Date();
10892
+ date.setDate(date.getDate() + i);
10893
+ forecast.push({
10894
+ date: date.toISOString().split("T")[0],
10895
+ projected: projectedDaily,
10896
+ confidence: Math.max(0.9 - i * 0.01, 0.5)
10897
+ // Decreasing confidence
10898
+ });
10899
+ }
10900
+ const totalProjected = projectedDaily * daysNum;
10901
+ res.json(
10902
+ createApiResponse({
10903
+ forecast,
10904
+ summary: {
10905
+ totalProjected,
10906
+ averageDaily: projectedDaily,
10907
+ period: {
10908
+ days: daysNum,
10909
+ start: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
10910
+ end: forecast[forecast.length - 1].date
10911
+ }
10912
+ },
10913
+ model: "linear-growth",
10914
+ confidence: 0.75
10915
+ })
10916
+ );
10917
+ } catch (error) {
10918
+ res.status(500).json(createErrorResponse("FORECAST_ERROR", error.message));
10919
+ }
10920
+ });
10921
+ forecast_default = router5;
10472
10922
  }
10473
- return resolved;
10474
- }
10475
- __name(resolveEnvVars, "resolveEnvVars");
10476
- function mergeConfigs(...configs) {
10477
- const result = {};
10478
- for (const config of configs) {
10479
- for (const [key, value] of Object.entries(config)) {
10480
- if (value === void 0 || value === null) {
10481
- continue;
10923
+ });
10924
+
10925
+ // src/api/routes/accounts.ts
10926
+ var accounts_exports = {};
10927
+ __export(accounts_exports, {
10928
+ default: () => accounts_default
10929
+ });
10930
+ var import_express6, router6, accounts_default;
10931
+ var init_accounts = __esm({
10932
+ "src/api/routes/accounts.ts"() {
10933
+ import_express6 = require("express");
10934
+ init_utils();
10935
+ init_server();
10936
+ router6 = (0, import_express6.Router)();
10937
+ router6.get("/", async (req, res) => {
10938
+ try {
10939
+ const config = getConfig2();
10940
+ const provider = await getProviderFromConfig();
10941
+ const accountInfo = await provider.getAccountInfo();
10942
+ const accounts = [
10943
+ {
10944
+ id: accountInfo.accountId,
10945
+ name: accountInfo.accountAlias || accountInfo.accountId,
10946
+ provider: config.provider,
10947
+ region: accountInfo.region
10948
+ }
10949
+ ];
10950
+ res.json(
10951
+ createApiResponse({
10952
+ accounts,
10953
+ count: accounts.length
10954
+ })
10955
+ );
10956
+ } catch (error) {
10957
+ res.status(500).json(createErrorResponse("FETCH_ERROR", error.message));
10482
10958
  }
10483
- if (typeof value === "object" && !Array.isArray(value)) {
10484
- result[key] = { ...result[key], ...value };
10485
- } else {
10486
- result[key] = value;
10959
+ });
10960
+ router6.get("/:id/costs", async (req, res) => {
10961
+ try {
10962
+ const config = getConfig2();
10963
+ const provider = await getProviderFromConfig();
10964
+ const { id } = req.params;
10965
+ const accountInfo = await provider.getAccountInfo();
10966
+ if (accountInfo.accountId !== id) {
10967
+ return res.status(404).json(createErrorResponse("NOT_FOUND", `Account ${id} not found`));
10968
+ }
10969
+ const now = /* @__PURE__ */ new Date();
10970
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
10971
+ const costs = await provider.getCostBreakdown(monthStart, now, "SERVICE");
10972
+ res.json(
10973
+ createApiResponse({
10974
+ accountId: id,
10975
+ costs: {
10976
+ total: costs.totalCost,
10977
+ breakdown: costs.breakdown
10978
+ },
10979
+ period: {
10980
+ start: monthStart.toISOString(),
10981
+ end: now.toISOString()
10982
+ }
10983
+ })
10984
+ );
10985
+ } catch (error) {
10986
+ res.status(500).json(createErrorResponse("FETCH_ERROR", error.message));
10487
10987
  }
10488
- }
10988
+ });
10989
+ accounts_default = router6;
10489
10990
  }
10490
- return result;
10491
- }
10492
- __name(mergeConfigs, "mergeConfigs");
10493
- function autoLoadConfig(cliOptions = {}) {
10494
- let config = { ...DEFAULT_CONFIG2 };
10495
- const configPath = cliOptions.configFile || discoverConfigFile();
10496
- if (configPath) {
10497
- const fileConfig = loadConfigFile(configPath);
10498
- config = mergeConfigs(config, fileConfig);
10991
+ });
10992
+
10993
+ // src/api/routes/reports.ts
10994
+ var reports_exports2 = {};
10995
+ __export(reports_exports2, {
10996
+ default: () => reports_default
10997
+ });
10998
+ var import_express7, router7, reports_default;
10999
+ var init_reports2 = __esm({
11000
+ "src/api/routes/reports.ts"() {
11001
+ import_express7 = require("express");
11002
+ init_utils();
11003
+ init_server();
11004
+ router7 = (0, import_express7.Router)();
11005
+ router7.post("/generate", async (req, res) => {
11006
+ try {
11007
+ const {
11008
+ reportType = "summary",
11009
+ startDate,
11010
+ endDate,
11011
+ format = "json",
11012
+ groupBy = "service"
11013
+ } = req.body;
11014
+ const config = getConfig2();
11015
+ const provider = await getProviderFromConfig();
11016
+ const start = startDate ? new Date(startDate) : /* @__PURE__ */ new Date();
11017
+ const end = endDate ? new Date(endDate) : /* @__PURE__ */ new Date();
11018
+ let groupByType = "SERVICE";
11019
+ if (groupBy === "tag")
11020
+ groupByType = "TAG";
11021
+ else if (groupBy === "daily")
11022
+ groupByType = "DAILY";
11023
+ else if (groupBy === "monthly")
11024
+ groupByType = "MONTHLY";
11025
+ const breakdown = await provider.getCostBreakdown(start, end, groupByType);
11026
+ const report = {
11027
+ reportType,
11028
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
11029
+ period: {
11030
+ start: start.toISOString(),
11031
+ end: end.toISOString()
11032
+ },
11033
+ data: {
11034
+ total: breakdown.totalCost,
11035
+ breakdown: breakdown.breakdown,
11036
+ groupBy
11037
+ },
11038
+ format
11039
+ };
11040
+ res.json(createApiResponse(report));
11041
+ } catch (error) {
11042
+ res.status(500).json(createErrorResponse("REPORT_ERROR", error.message));
11043
+ }
11044
+ });
11045
+ reports_default = router7;
10499
11046
  }
10500
- config = mergeConfigs(config, resolveEnvVars(config));
10501
- const cliConfig = mapCliOptionsToConfig(cliOptions);
10502
- config = mergeConfigs(config, cliConfig);
10503
- return config;
11047
+ });
11048
+
11049
+ // src/api/server.ts
11050
+ function createApiResponse(data) {
11051
+ return {
11052
+ status: "success",
11053
+ data,
11054
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11055
+ };
10504
11056
  }
10505
- __name(autoLoadConfig, "autoLoadConfig");
10506
- function mapCliOptionsToConfig(options) {
10507
- const config = {};
10508
- if (options.provider)
10509
- config.provider = options.provider;
10510
- if (options.profile)
10511
- config.profile = options.profile;
10512
- if (options.region)
10513
- config.region = options.region;
10514
- if (options.accessKey)
10515
- config.accessKey = options.accessKey;
10516
- if (options.secretKey)
10517
- config.secretKey = options.secretKey;
10518
- if (options.sessionToken)
10519
- config.sessionToken = options.sessionToken;
10520
- if (options.json || options.text || options.summary) {
10521
- config.output = config.output || {};
10522
- if (options.json)
10523
- config.output.format = "json";
10524
- if (options.text)
10525
- config.output.format = "text";
10526
- if (options.summary)
10527
- config.output.summary = true;
10528
- }
10529
- if (options.delta !== void 0) {
10530
- config.output = config.output || {};
10531
- config.output.showDelta = options.delta;
10532
- }
10533
- if (options.deltaThreshold) {
10534
- config.output = config.output || {};
10535
- config.output.deltaThreshold = parseFloat(options.deltaThreshold);
10536
- }
10537
- if (options.quickWins !== void 0) {
10538
- config.output = config.output || {};
10539
- config.output.showQuickWins = options.quickWins;
10540
- }
10541
- if (options.quickWinsCount) {
10542
- config.output = config.output || {};
10543
- config.output.quickWinsCount = parseInt(options.quickWinsCount, 10);
10544
- }
10545
- if (options.cache !== void 0 || options.noCache !== void 0) {
10546
- config.cache = config.cache || {};
10547
- config.cache.enabled = options.cache === true || options.noCache !== true;
10548
- }
10549
- if (options.cacheTtl) {
10550
- config.cache = config.cache || {};
10551
- config.cache.ttl = options.cacheTtl;
10552
- }
10553
- if (options.cacheType) {
10554
- config.cache = config.cache || {};
10555
- config.cache.type = options.cacheType;
10556
- }
10557
- if (options.slackToken || options.slackChannel) {
10558
- config.slack = config.slack || {};
10559
- if (options.slackToken)
10560
- config.slack.token = options.slackToken;
10561
- if (options.slackChannel)
10562
- config.slack.channel = options.slackChannel;
10563
- config.slack.enabled = true;
10564
- }
10565
- if (options.logLevel || options.verbose || options.quiet) {
10566
- config.logging = config.logging || {};
10567
- if (options.logLevel)
10568
- config.logging.level = options.logLevel;
10569
- if (options.verbose)
10570
- config.logging.level = "debug";
10571
- if (options.quiet)
10572
- config.logging.level = "error";
10573
- }
10574
- return config;
11057
+ function createErrorResponse(code, message) {
11058
+ return {
11059
+ status: "error",
11060
+ error: { code, message },
11061
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11062
+ };
10575
11063
  }
10576
- __name(mapCliOptionsToConfig, "mapCliOptionsToConfig");
11064
+ var import_express8, import_cors, import_express_rate_limit, import_helmet, cache, ApiServer;
11065
+ var init_server = __esm({
11066
+ "src/api/server.ts"() {
11067
+ import_express8 = __toESM(require("express"));
11068
+ import_cors = __toESM(require("cors"));
11069
+ import_express_rate_limit = __toESM(require("express-rate-limit"));
11070
+ import_helmet = __toESM(require("helmet"));
11071
+ cache = /* @__PURE__ */ new Map();
11072
+ ApiServer = class {
11073
+ constructor(config) {
11074
+ this.config = config;
11075
+ this.app = (0, import_express8.default)();
11076
+ this.setupMiddleware();
11077
+ this.setupRoutes();
11078
+ }
11079
+ setupMiddleware() {
11080
+ this.app.use((0, import_helmet.default)());
11081
+ this.app.use(import_express8.default.json());
11082
+ if (this.config.cors.enabled) {
11083
+ this.app.use(
11084
+ (0, import_cors.default)({
11085
+ origin: this.config.cors.origins || "*",
11086
+ methods: ["GET", "POST", "PUT", "DELETE"],
11087
+ credentials: true
11088
+ })
11089
+ );
11090
+ }
11091
+ if (this.config.rateLimit.enabled) {
11092
+ const limiter = (0, import_express_rate_limit.default)({
11093
+ windowMs: this.config.rateLimit.windowMs,
11094
+ max: this.config.rateLimit.max,
11095
+ message: {
11096
+ status: "error",
11097
+ error: {
11098
+ code: "RATE_LIMIT_EXCEEDED",
11099
+ message: "Too many requests, please try again later."
11100
+ },
11101
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11102
+ }
11103
+ });
11104
+ this.app.use("/api/", limiter);
11105
+ }
11106
+ this.app.use("/api/", this.authMiddleware.bind(this));
11107
+ if (this.config.cache.enabled) {
11108
+ this.app.use("/api/", this.cacheMiddleware.bind(this));
11109
+ }
11110
+ }
11111
+ authMiddleware(req, res, next) {
11112
+ if (req.path === "/api/v1/health") {
11113
+ return next();
11114
+ }
11115
+ if (this.config.auth.type === "none") {
11116
+ return next();
11117
+ }
11118
+ if (this.config.auth.type === "api-key") {
11119
+ const apiKey = req.headers["x-api-key"] || req.query.apiKey;
11120
+ if (!apiKey || !this.config.auth.apiKeys?.includes(apiKey)) {
11121
+ return res.status(401).json({
11122
+ status: "error",
11123
+ error: {
11124
+ code: "UNAUTHORIZED",
11125
+ message: "Invalid or missing API key"
11126
+ },
11127
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11128
+ });
11129
+ }
11130
+ }
11131
+ next();
11132
+ }
11133
+ cacheMiddleware(req, res, next) {
11134
+ if (req.method !== "GET") {
11135
+ return next();
11136
+ }
11137
+ const key = `${req.method}:${req.path}:${JSON.stringify(req.query)}`;
11138
+ const cached = cache.get(key);
11139
+ if (cached && cached.expiry > Date.now()) {
11140
+ return res.json(cached.data);
11141
+ }
11142
+ const originalJson = res.json.bind(res);
11143
+ res.json = (body) => {
11144
+ if (res.statusCode === 200) {
11145
+ cache.set(key, {
11146
+ data: body,
11147
+ expiry: Date.now() + this.config.cache.ttl * 1e3
11148
+ });
11149
+ }
11150
+ return originalJson(body);
11151
+ };
11152
+ next();
11153
+ }
11154
+ setupRoutes() {
11155
+ this.app.get("/api/v1/health", (_req, res) => {
11156
+ res.json({
11157
+ status: "success",
11158
+ data: {
11159
+ healthy: true,
11160
+ version: require_package().version,
11161
+ uptime: process.uptime()
11162
+ },
11163
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11164
+ });
11165
+ });
11166
+ this.app.use("/api/v1/costs", (init_costs2(), __toCommonJS(costs_exports2)).default);
11167
+ this.app.use("/api/v1/inventory", (init_inventory6(), __toCommonJS(inventory_exports2)).default);
11168
+ this.app.use("/api/v1/optimization", (init_optimization(), __toCommonJS(optimization_exports)).default);
11169
+ this.app.use("/api/v1/chargeback", (init_chargeback(), __toCommonJS(chargeback_exports)).default);
11170
+ this.app.use("/api/v1/forecast", (init_forecast(), __toCommonJS(forecast_exports)).default);
11171
+ this.app.use("/api/v1/accounts", (init_accounts(), __toCommonJS(accounts_exports)).default);
11172
+ this.app.use("/api/v1/reports", (init_reports2(), __toCommonJS(reports_exports2)).default);
11173
+ this.app.use((_req, res) => {
11174
+ res.status(404).json({
11175
+ status: "error",
11176
+ error: {
11177
+ code: "NOT_FOUND",
11178
+ message: "API endpoint not found"
11179
+ },
11180
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11181
+ });
11182
+ });
11183
+ this.app.use((err, _req, res, _next) => {
11184
+ console.error("API Error:", err);
11185
+ res.status(500).json({
11186
+ status: "error",
11187
+ error: {
11188
+ code: "INTERNAL_ERROR",
11189
+ message: err.message || "Internal server error"
11190
+ },
11191
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11192
+ });
11193
+ });
11194
+ }
11195
+ async start() {
11196
+ return new Promise((resolve2, reject) => {
11197
+ try {
11198
+ this.server = this.app.listen(this.config.port, this.config.host, () => {
11199
+ console.log(
11200
+ `\u2705 API Server running at http://${this.config.host}:${this.config.port}`
11201
+ );
11202
+ console.log(`\u{1F4D6} API Documentation: http://${this.config.host}:${this.config.port}/api/docs`);
11203
+ resolve2();
11204
+ });
11205
+ this.server.on("error", reject);
11206
+ } catch (error) {
11207
+ reject(error);
11208
+ }
11209
+ });
11210
+ }
11211
+ async stop() {
11212
+ return new Promise((resolve2, reject) => {
11213
+ if (!this.server) {
11214
+ resolve2();
11215
+ return;
11216
+ }
11217
+ this.server.close((err) => {
11218
+ if (err) {
11219
+ reject(err);
11220
+ } else {
11221
+ console.log("API Server stopped");
11222
+ resolve2();
11223
+ }
11224
+ });
11225
+ });
11226
+ }
11227
+ getApp() {
11228
+ return this.app;
11229
+ }
11230
+ };
11231
+ __name(ApiServer, "ApiServer");
11232
+ __name(createApiResponse, "createApiResponse");
11233
+ __name(createErrorResponse, "createErrorResponse");
11234
+ }
11235
+ });
11236
+
11237
+ // src/cli/index.ts
11238
+ var cli_exports = {};
11239
+ __export(cli_exports, {
11240
+ createCLI: () => createCLI,
11241
+ main: () => main
11242
+ });
11243
+ module.exports = __toCommonJS(cli_exports);
11244
+ var import_commander = require("commander");
11245
+ var import_package = __toESM(require_package());
11246
+ init_logging();
11247
+
11248
+ // src/core/config/index.ts
11249
+ init_schema();
11250
+ init_loader();
10577
11251
 
10578
11252
  // src/core/config/discovery.ts
10579
11253
  var import_chalk2 = __toESM(require("chalk"));
@@ -13887,6 +14561,661 @@ function registerPluginCommands(program) {
13887
14561
  }
13888
14562
  __name(registerPluginCommands, "registerPluginCommands");
13889
14563
 
14564
+ // src/cli/commands/server/index.ts
14565
+ var import_chalk22 = __toESM(require("chalk"));
14566
+ var import_fs8 = require("fs");
14567
+ var import_path7 = require("path");
14568
+ var import_os7 = require("os");
14569
+ init_server();
14570
+ var CONFIG_DIR4 = (0, import_path7.join)((0, import_os7.homedir)(), ".infra-cost");
14571
+ var SERVER_CONFIG_PATH = (0, import_path7.join)(CONFIG_DIR4, "server-config.json");
14572
+ var PID_FILE2 = (0, import_path7.join)(CONFIG_DIR4, "server.pid");
14573
+ var DEFAULT_CONFIG3 = {
14574
+ port: 3e3,
14575
+ host: "127.0.0.1",
14576
+ cors: {
14577
+ enabled: true,
14578
+ origins: ["*"]
14579
+ },
14580
+ auth: {
14581
+ type: "none"
14582
+ },
14583
+ rateLimit: {
14584
+ enabled: true,
14585
+ windowMs: 6e4,
14586
+ max: 100
14587
+ },
14588
+ cache: {
14589
+ enabled: true,
14590
+ ttl: 300
14591
+ // 5 minutes
14592
+ }
14593
+ };
14594
+ function loadServerConfig() {
14595
+ if ((0, import_fs8.existsSync)(SERVER_CONFIG_PATH)) {
14596
+ const config = JSON.parse((0, import_fs8.readFileSync)(SERVER_CONFIG_PATH, "utf-8"));
14597
+ return { ...DEFAULT_CONFIG3, ...config };
14598
+ }
14599
+ return DEFAULT_CONFIG3;
14600
+ }
14601
+ __name(loadServerConfig, "loadServerConfig");
14602
+ function saveServerConfig(config) {
14603
+ (0, import_fs8.writeFileSync)(SERVER_CONFIG_PATH, JSON.stringify(config, null, 2));
14604
+ }
14605
+ __name(saveServerConfig, "saveServerConfig");
14606
+ async function handleStart2(options) {
14607
+ try {
14608
+ if ((0, import_fs8.existsSync)(PID_FILE2)) {
14609
+ const pid = parseInt((0, import_fs8.readFileSync)(PID_FILE2, "utf-8").trim(), 10);
14610
+ try {
14611
+ process.kill(pid, 0);
14612
+ console.log(import_chalk22.default.yellow("\u26A0\uFE0F Server is already running"));
14613
+ console.log(import_chalk22.default.gray(` PID: ${pid}`));
14614
+ return;
14615
+ } catch {
14616
+ require("fs").unlinkSync(PID_FILE2);
14617
+ }
14618
+ }
14619
+ const config = loadServerConfig();
14620
+ if (options.port)
14621
+ config.port = parseInt(options.port, 10);
14622
+ if (options.host)
14623
+ config.host = options.host;
14624
+ if (options.apiKey) {
14625
+ config.auth = {
14626
+ type: "api-key",
14627
+ apiKeys: [options.apiKey]
14628
+ };
14629
+ }
14630
+ if (options.apiKeyRequired && !options.apiKey) {
14631
+ console.log(import_chalk22.default.red("\u274C --api-key is required when --api-key-required is set"));
14632
+ process.exit(1);
14633
+ }
14634
+ const server = new ApiServer(config);
14635
+ if (options.daemon) {
14636
+ console.log(import_chalk22.default.blue("Starting server in daemon mode..."));
14637
+ const { spawn } = require("child_process");
14638
+ const child = spawn(
14639
+ process.argv[0],
14640
+ [process.argv[1], "server", "start", "--port", config.port.toString(), "--host", config.host],
14641
+ {
14642
+ detached: true,
14643
+ stdio: "ignore"
14644
+ }
14645
+ );
14646
+ child.unref();
14647
+ (0, import_fs8.writeFileSync)(PID_FILE2, child.pid.toString());
14648
+ console.log(import_chalk22.default.green("\u2705 Server started in background"));
14649
+ console.log(import_chalk22.default.gray(` PID: ${child.pid}`));
14650
+ console.log(import_chalk22.default.gray(` URL: http://${config.host}:${config.port}`));
14651
+ return;
14652
+ }
14653
+ await server.start();
14654
+ (0, import_fs8.writeFileSync)(PID_FILE2, process.pid.toString());
14655
+ console.log();
14656
+ console.log(import_chalk22.default.bold("Server Configuration:"));
14657
+ console.log(import_chalk22.default.gray(` Port: ${config.port}`));
14658
+ console.log(import_chalk22.default.gray(` Host: ${config.host}`));
14659
+ console.log(import_chalk22.default.gray(` Auth: ${config.auth.type}`));
14660
+ console.log(import_chalk22.default.gray(` Rate Limit: ${config.rateLimit.enabled ? "enabled" : "disabled"}`));
14661
+ console.log(import_chalk22.default.gray(` Cache: ${config.cache.enabled ? `${config.cache.ttl}s` : "disabled"}`));
14662
+ console.log();
14663
+ if (config.auth.type === "api-key") {
14664
+ console.log(import_chalk22.default.yellow("\u26A0\uFE0F API Key Authentication Enabled"));
14665
+ console.log(import_chalk22.default.gray(" Use header: X-API-Key: your-api-key"));
14666
+ console.log();
14667
+ }
14668
+ console.log(import_chalk22.default.green("Press Ctrl+C to stop the server"));
14669
+ const shutdown = /* @__PURE__ */ __name(async () => {
14670
+ console.log(import_chalk22.default.yellow("\n\nShutting down server..."));
14671
+ await server.stop();
14672
+ if ((0, import_fs8.existsSync)(PID_FILE2)) {
14673
+ require("fs").unlinkSync(PID_FILE2);
14674
+ }
14675
+ process.exit(0);
14676
+ }, "shutdown");
14677
+ process.on("SIGINT", shutdown);
14678
+ process.on("SIGTERM", shutdown);
14679
+ } catch (error) {
14680
+ console.error(import_chalk22.default.red("\u274C Failed to start server:"), error.message);
14681
+ process.exit(1);
14682
+ }
14683
+ }
14684
+ __name(handleStart2, "handleStart");
14685
+ async function handleStop2() {
14686
+ try {
14687
+ if (!(0, import_fs8.existsSync)(PID_FILE2)) {
14688
+ console.log(import_chalk22.default.yellow("\u26A0\uFE0F Server is not running"));
14689
+ return;
14690
+ }
14691
+ const pid = parseInt((0, import_fs8.readFileSync)(PID_FILE2, "utf-8").trim(), 10);
14692
+ try {
14693
+ process.kill(pid, "SIGTERM");
14694
+ console.log(import_chalk22.default.green("\u2705 Server stopped"));
14695
+ setTimeout(() => {
14696
+ if ((0, import_fs8.existsSync)(PID_FILE2)) {
14697
+ require("fs").unlinkSync(PID_FILE2);
14698
+ }
14699
+ }, 1e3);
14700
+ } catch {
14701
+ console.log(import_chalk22.default.yellow("\u26A0\uFE0F Server process not found"));
14702
+ if ((0, import_fs8.existsSync)(PID_FILE2)) {
14703
+ require("fs").unlinkSync(PID_FILE2);
14704
+ }
14705
+ }
14706
+ } catch (error) {
14707
+ console.error(import_chalk22.default.red("\u274C Failed to stop server:"), error.message);
14708
+ process.exit(1);
14709
+ }
14710
+ }
14711
+ __name(handleStop2, "handleStop");
14712
+ async function handleStatus3() {
14713
+ try {
14714
+ if (!(0, import_fs8.existsSync)(PID_FILE2)) {
14715
+ console.log(import_chalk22.default.yellow("\u26A0\uFE0F Server is not running"));
14716
+ return;
14717
+ }
14718
+ const pid = parseInt((0, import_fs8.readFileSync)(PID_FILE2, "utf-8").trim(), 10);
14719
+ try {
14720
+ process.kill(pid, 0);
14721
+ const config = loadServerConfig();
14722
+ console.log(import_chalk22.default.green("\u2705 Server is running"));
14723
+ console.log(import_chalk22.default.gray(` PID: ${pid}`));
14724
+ console.log(import_chalk22.default.gray(` URL: http://${config.host}:${config.port}`));
14725
+ console.log(import_chalk22.default.gray(` Docs: http://${config.host}:${config.port}/api/docs`));
14726
+ } catch {
14727
+ console.log(import_chalk22.default.yellow("\u26A0\uFE0F Server is not running (stale PID file)"));
14728
+ require("fs").unlinkSync(PID_FILE2);
14729
+ }
14730
+ } catch (error) {
14731
+ console.error(import_chalk22.default.red("\u274C Failed to check status:"), error.message);
14732
+ process.exit(1);
14733
+ }
14734
+ }
14735
+ __name(handleStatus3, "handleStatus");
14736
+ async function handleConfigure2(options) {
14737
+ try {
14738
+ const config = loadServerConfig();
14739
+ if (options.port)
14740
+ config.port = parseInt(options.port, 10);
14741
+ if (options.host)
14742
+ config.host = options.host;
14743
+ if (options.enableCors !== void 0)
14744
+ config.cors.enabled = options.enableCors === "true";
14745
+ if (options.cacheEnabled !== void 0)
14746
+ config.cache.enabled = options.cacheEnabled === "true";
14747
+ if (options.cacheTtl)
14748
+ config.cache.ttl = parseInt(options.cacheTtl, 10);
14749
+ saveServerConfig(config);
14750
+ console.log(import_chalk22.default.green("\u2705 Server configuration updated"));
14751
+ console.log();
14752
+ console.log(JSON.stringify(config, null, 2));
14753
+ } catch (error) {
14754
+ console.error(import_chalk22.default.red("\u274C Failed to configure server:"), error.message);
14755
+ process.exit(1);
14756
+ }
14757
+ }
14758
+ __name(handleConfigure2, "handleConfigure");
14759
+ function registerServerCommands(program) {
14760
+ const server = program.command("server").description("API server mode for REST API access");
14761
+ server.command("start").description("Start the API server").option("-p, --port <port>", "Port to listen on", "3000").option("-h, --host <host>", "Host to bind to", "127.0.0.1").option("--api-key <key>", "API key for authentication").option("--api-key-required", "Require API key authentication").option("-d, --daemon", "Run in background/daemon mode").action(handleStart2);
14762
+ server.command("stop").description("Stop the running API server").action(handleStop2);
14763
+ server.command("status").description("Check server status").action(handleStatus3);
14764
+ server.command("configure").description("Configure server settings").option("-p, --port <port>", "Default port").option("-h, --host <host>", "Default host").option("--enable-cors <boolean>", "Enable/disable CORS").option("--cache-enabled <boolean>", "Enable/disable caching").option("--cache-ttl <seconds>", "Cache TTL in seconds").action(handleConfigure2);
14765
+ }
14766
+ __name(registerServerCommands, "registerServerCommands");
14767
+
14768
+ // src/cli/commands/scorecard/index.ts
14769
+ var import_chalk23 = __toESM(require("chalk"));
14770
+ var import_cli_table32 = __toESM(require("cli-table3"));
14771
+
14772
+ // src/core/scorecard.ts
14773
+ var DEFAULT_SCORECARD_CONFIG = {
14774
+ enabled: true,
14775
+ categories: {
14776
+ budgetAdherence: { weight: 25, target: 80 },
14777
+ costEfficiency: { weight: 20, target: 0 },
14778
+ taggingCompliance: { weight: 15, target: 90 },
14779
+ reservedCoverage: { weight: 15, target: 70 },
14780
+ wasteElimination: { weight: 15, target: 80 },
14781
+ optimizationActions: { weight: 10, target: 80 }
14782
+ },
14783
+ gradingScale: {
14784
+ A: 90,
14785
+ B: 80,
14786
+ C: 70,
14787
+ D: 60
14788
+ },
14789
+ notifications: {
14790
+ weekly: true,
14791
+ monthlyReport: true,
14792
+ awardBadges: true
14793
+ }
14794
+ };
14795
+ function calculateCategoryScore(currentValue, target, weight, isInverse = false) {
14796
+ const maxScore = weight;
14797
+ let performance;
14798
+ if (isInverse) {
14799
+ performance = target === 0 ? currentValue <= 0 ? 100 : 0 : Math.max(0, 100 - currentValue / target * 100);
14800
+ } else {
14801
+ performance = currentValue / target * 100;
14802
+ }
14803
+ performance = Math.min(100, Math.max(0, performance));
14804
+ const score = performance / 100 * maxScore;
14805
+ let status;
14806
+ if (performance >= 95)
14807
+ status = "excellent";
14808
+ else if (performance >= 80)
14809
+ status = "good";
14810
+ else if (performance >= 60)
14811
+ status = "warning";
14812
+ else
14813
+ status = "critical";
14814
+ return { score, status };
14815
+ }
14816
+ __name(calculateCategoryScore, "calculateCategoryScore");
14817
+ function calculateGrade(score, config) {
14818
+ if (score >= config.gradingScale.A)
14819
+ return "A";
14820
+ if (score >= config.gradingScale.B)
14821
+ return "B+";
14822
+ if (score >= config.gradingScale.C)
14823
+ return "C+";
14824
+ if (score >= config.gradingScale.D)
14825
+ return "D";
14826
+ return "F";
14827
+ }
14828
+ __name(calculateGrade, "calculateGrade");
14829
+ function generateQuickWins2(categories) {
14830
+ const quickWins = [];
14831
+ categories.forEach((category) => {
14832
+ if (category.status === "warning" || category.status === "critical") {
14833
+ const gap = category.target - category.currentValue;
14834
+ const potentialPoints = gap / category.target * category.maxScore;
14835
+ if (category.name === "Reserved Coverage") {
14836
+ quickWins.push({
14837
+ description: `Increase RI coverage by ${gap.toFixed(0)}%`,
14838
+ pointsGain: Math.round(potentialPoints),
14839
+ estimatedSavings: gap * 100,
14840
+ // Rough estimate
14841
+ difficulty: "medium"
14842
+ });
14843
+ } else if (category.name === "Tagging Compliance") {
14844
+ const missingTags = Math.round(gap / 100 * 100);
14845
+ quickWins.push({
14846
+ description: `Add missing tags to ~${missingTags} resources`,
14847
+ pointsGain: Math.round(potentialPoints),
14848
+ difficulty: "easy"
14849
+ });
14850
+ } else if (category.name === "Waste Elimination") {
14851
+ quickWins.push({
14852
+ description: `Clean up unused resources (${gap.toFixed(0)}% remaining)`,
14853
+ pointsGain: Math.round(potentialPoints),
14854
+ estimatedSavings: gap * 50,
14855
+ difficulty: "easy"
14856
+ });
14857
+ }
14858
+ }
14859
+ });
14860
+ return quickWins.sort((a, b) => b.pointsGain - a.pointsGain).slice(0, 5);
14861
+ }
14862
+ __name(generateQuickWins2, "generateQuickWins");
14863
+ function checkBadges(scorecard, historicalData) {
14864
+ const badges = [];
14865
+ const today = (/* @__PURE__ */ new Date()).toISOString();
14866
+ const budgetCategory = scorecard.categories.find((c) => c.name === "Budget Adherence");
14867
+ if (budgetCategory && budgetCategory.currentValue >= budgetCategory.target) {
14868
+ const last3Months = historicalData.slice(-3);
14869
+ if (last3Months.length === 3 && last3Months.every((m) => m.score >= 80)) {
14870
+ badges.push({
14871
+ name: "Budget Champion",
14872
+ emoji: "\u{1F3C5}",
14873
+ description: "Under budget for 3 consecutive months",
14874
+ awardedDate: today
14875
+ });
14876
+ }
14877
+ }
14878
+ const taggingCategory = scorecard.categories.find((c) => c.name === "Tagging Compliance");
14879
+ if (taggingCategory && taggingCategory.currentValue >= 100) {
14880
+ badges.push({
14881
+ name: "Tagging Star",
14882
+ emoji: "\u2B50",
14883
+ description: "100% tagging compliance",
14884
+ awardedDate: today
14885
+ });
14886
+ }
14887
+ const costCategory = scorecard.categories.find((c) => c.name === "Cost Efficiency");
14888
+ if (costCategory && costCategory.currentValue <= -20) {
14889
+ badges.push({
14890
+ name: "Green Team",
14891
+ emoji: "\u{1F331}",
14892
+ description: "Reduced costs 20% this quarter",
14893
+ awardedDate: today
14894
+ });
14895
+ }
14896
+ if (scorecard.totalScore >= 90) {
14897
+ const last6Months = historicalData.slice(-6);
14898
+ if (last6Months.length === 6 && last6Months.every((m) => m.score >= 90)) {
14899
+ badges.push({
14900
+ name: "Efficiency Expert",
14901
+ emoji: "\u{1F48E}",
14902
+ description: "Score above 90 for 6 months",
14903
+ awardedDate: today
14904
+ });
14905
+ }
14906
+ }
14907
+ return badges;
14908
+ }
14909
+ __name(checkBadges, "checkBadges");
14910
+ function buildTeamScorecard(teamName, metrics, config = DEFAULT_SCORECARD_CONFIG, historicalData = []) {
14911
+ const categories = [];
14912
+ const budgetTarget = config.categories.budgetAdherence.target;
14913
+ const budgetScore = calculateCategoryScore(
14914
+ Math.min(metrics.budgetUsage, 100),
14915
+ budgetTarget,
14916
+ config.categories.budgetAdherence.weight,
14917
+ false
14918
+ );
14919
+ categories.push({
14920
+ name: "Budget Adherence",
14921
+ weight: config.categories.budgetAdherence.weight,
14922
+ target: budgetTarget,
14923
+ currentValue: metrics.budgetUsage,
14924
+ score: budgetScore.score,
14925
+ maxScore: config.categories.budgetAdherence.weight,
14926
+ status: budgetScore.status
14927
+ });
14928
+ const costEffScore = calculateCategoryScore(
14929
+ Math.abs(metrics.costTrend),
14930
+ Math.abs(config.categories.costEfficiency.target),
14931
+ config.categories.costEfficiency.weight,
14932
+ true
14933
+ );
14934
+ categories.push({
14935
+ name: "Cost Efficiency",
14936
+ weight: config.categories.costEfficiency.weight,
14937
+ target: config.categories.costEfficiency.target,
14938
+ currentValue: metrics.costTrend,
14939
+ score: costEffScore.score,
14940
+ maxScore: config.categories.costEfficiency.weight,
14941
+ status: costEffScore.status
14942
+ });
14943
+ const taggingScore = calculateCategoryScore(
14944
+ metrics.taggingCompliance,
14945
+ config.categories.taggingCompliance.target,
14946
+ config.categories.taggingCompliance.weight
14947
+ );
14948
+ categories.push({
14949
+ name: "Tagging Compliance",
14950
+ weight: config.categories.taggingCompliance.weight,
14951
+ target: config.categories.taggingCompliance.target,
14952
+ currentValue: metrics.taggingCompliance,
14953
+ score: taggingScore.score,
14954
+ maxScore: config.categories.taggingCompliance.weight,
14955
+ status: taggingScore.status
14956
+ });
14957
+ const reservedScore = calculateCategoryScore(
14958
+ metrics.reservedCoverage,
14959
+ config.categories.reservedCoverage.target,
14960
+ config.categories.reservedCoverage.weight
14961
+ );
14962
+ categories.push({
14963
+ name: "Reserved Coverage",
14964
+ weight: config.categories.reservedCoverage.weight,
14965
+ target: config.categories.reservedCoverage.target,
14966
+ currentValue: metrics.reservedCoverage,
14967
+ score: reservedScore.score,
14968
+ maxScore: config.categories.reservedCoverage.weight,
14969
+ status: reservedScore.status
14970
+ });
14971
+ const wasteScore = calculateCategoryScore(
14972
+ 100 - metrics.wastePercentage,
14973
+ config.categories.wasteElimination.target,
14974
+ config.categories.wasteElimination.weight
14975
+ );
14976
+ categories.push({
14977
+ name: "Waste Elimination",
14978
+ weight: config.categories.wasteElimination.weight,
14979
+ target: config.categories.wasteElimination.target,
14980
+ currentValue: 100 - metrics.wastePercentage,
14981
+ score: wasteScore.score,
14982
+ maxScore: config.categories.wasteElimination.weight,
14983
+ status: wasteScore.status
14984
+ });
14985
+ const optActionsPercent = metrics.optimizationsActed / Math.max(metrics.totalOptimizations, 1) * 100;
14986
+ const optScore = calculateCategoryScore(
14987
+ optActionsPercent,
14988
+ config.categories.optimizationActions.target,
14989
+ config.categories.optimizationActions.weight
14990
+ );
14991
+ categories.push({
14992
+ name: "Optimization Actions",
14993
+ weight: config.categories.optimizationActions.weight,
14994
+ target: config.categories.optimizationActions.target,
14995
+ currentValue: optActionsPercent,
14996
+ score: optScore.score,
14997
+ maxScore: config.categories.optimizationActions.weight,
14998
+ status: optScore.status
14999
+ });
15000
+ const totalScore = Math.round(categories.reduce((sum, cat) => sum + cat.score, 0));
15001
+ const grade = calculateGrade(totalScore, config);
15002
+ const lastMonthScore = historicalData.length > 0 ? historicalData[historicalData.length - 1].score : totalScore;
15003
+ const trend = totalScore - lastMonthScore;
15004
+ const quickWins = generateQuickWins2(categories);
15005
+ const scorecard = {
15006
+ teamName,
15007
+ totalScore,
15008
+ grade,
15009
+ trend,
15010
+ categories,
15011
+ quickWins,
15012
+ badges: [],
15013
+ monthlyHistory: historicalData
15014
+ };
15015
+ scorecard.badges = checkBadges(scorecard, historicalData);
15016
+ return scorecard;
15017
+ }
15018
+ __name(buildTeamScorecard, "buildTeamScorecard");
15019
+ function generateLeaderboard(scorecards) {
15020
+ return scorecards.map((sc, index) => ({
15021
+ rank: index + 1,
15022
+ teamName: sc.teamName,
15023
+ score: sc.totalScore,
15024
+ grade: sc.grade,
15025
+ trend: sc.trend,
15026
+ savings: sc.quickWins.reduce((sum, qw) => sum + (qw.estimatedSavings || 0), 0)
15027
+ })).sort((a, b) => b.score - a.score).map((entry, index) => ({ ...entry, rank: index + 1 }));
15028
+ }
15029
+ __name(generateLeaderboard, "generateLeaderboard");
15030
+
15031
+ // src/cli/commands/scorecard/index.ts
15032
+ function getMockTeamMetrics(teamName) {
15033
+ const teams = {
15034
+ "Backend Engineering": {
15035
+ budgetUsage: 85,
15036
+ costTrend: -5,
15037
+ taggingCompliance: 92,
15038
+ reservedCoverage: 65,
15039
+ wastePercentage: 22,
15040
+ optimizationsActed: 4,
15041
+ totalOptimizations: 5
15042
+ },
15043
+ "Platform Team": {
15044
+ budgetUsage: 75,
15045
+ costTrend: -8,
15046
+ taggingCompliance: 98,
15047
+ reservedCoverage: 85,
15048
+ wastePercentage: 8,
15049
+ optimizationsActed: 5,
15050
+ totalOptimizations: 5
15051
+ },
15052
+ "Data Engineering": {
15053
+ budgetUsage: 82,
15054
+ costTrend: -3,
15055
+ taggingCompliance: 90,
15056
+ reservedCoverage: 70,
15057
+ wastePercentage: 15,
15058
+ optimizationsActed: 4,
15059
+ totalOptimizations: 5
15060
+ },
15061
+ "Frontend Team": {
15062
+ budgetUsage: 88,
15063
+ costTrend: 2,
15064
+ taggingCompliance: 85,
15065
+ reservedCoverage: 60,
15066
+ wastePercentage: 25,
15067
+ optimizationsActed: 3,
15068
+ totalOptimizations: 5
15069
+ },
15070
+ "ML Team": {
15071
+ budgetUsage: 95,
15072
+ costTrend: 5,
15073
+ taggingCompliance: 78,
15074
+ reservedCoverage: 55,
15075
+ wastePercentage: 30,
15076
+ optimizationsActed: 2,
15077
+ totalOptimizations: 5
15078
+ }
15079
+ };
15080
+ return teams[teamName] || teams["Backend Engineering"];
15081
+ }
15082
+ __name(getMockTeamMetrics, "getMockTeamMetrics");
15083
+ function getStatusEmoji(status) {
15084
+ switch (status) {
15085
+ case "excellent":
15086
+ return "\u2705";
15087
+ case "good":
15088
+ return "\u2705";
15089
+ case "warning":
15090
+ return "\u26A0\uFE0F";
15091
+ case "critical":
15092
+ return "\u274C";
15093
+ default:
15094
+ return "\u2022";
15095
+ }
15096
+ }
15097
+ __name(getStatusEmoji, "getStatusEmoji");
15098
+ function formatTrendArrow(trend) {
15099
+ if (trend > 0)
15100
+ return import_chalk23.default.green(`\u2B06\uFE0F +${trend}`);
15101
+ if (trend < 0)
15102
+ return import_chalk23.default.red(`\u2B07\uFE0F ${trend}`);
15103
+ return import_chalk23.default.gray("\u2192 0");
15104
+ }
15105
+ __name(formatTrendArrow, "formatTrendArrow");
15106
+ async function handleScorecard(options) {
15107
+ try {
15108
+ const teamName = options.team || "Backend Engineering";
15109
+ const metrics = getMockTeamMetrics(teamName);
15110
+ const historicalData = [
15111
+ { month: "July 2025", score: 77, grade: "B" },
15112
+ { month: "August 2025", score: 79, grade: "B" },
15113
+ { month: "September 2025", score: 80, grade: "B+" }
15114
+ ];
15115
+ const scorecard = buildTeamScorecard(teamName, metrics, DEFAULT_SCORECARD_CONFIG, historicalData);
15116
+ console.log();
15117
+ console.log(import_chalk23.default.bold.blue("\u2550".repeat(70)));
15118
+ console.log(import_chalk23.default.bold.white(` \u{1F3C6} FinOps Scorecard - ${(/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { month: "long", year: "numeric" })}`));
15119
+ console.log(import_chalk23.default.bold.blue("\u2550".repeat(70)));
15120
+ console.log();
15121
+ console.log(import_chalk23.default.bold.white(`Team: ${scorecard.teamName}`));
15122
+ console.log(
15123
+ import_chalk23.default.bold.cyan(
15124
+ `Score: ${scorecard.totalScore}/100 (Grade: ${scorecard.grade}) ${formatTrendArrow(scorecard.trend)} from last month`
15125
+ )
15126
+ );
15127
+ console.log(import_chalk23.default.gray("\u2500".repeat(70)));
15128
+ console.log();
15129
+ console.log(import_chalk23.default.bold("\u{1F4CA} Metrics:"));
15130
+ scorecard.categories.forEach((category) => {
15131
+ const emoji = getStatusEmoji(category.status);
15132
+ const percentage = (category.score / category.maxScore * 100).toFixed(0);
15133
+ const value = category.name === "Cost Efficiency" ? `${category.currentValue >= 0 ? "+" : ""}${category.currentValue}%` : category.name === "Optimization Actions" ? `${Math.round(category.currentValue)}%` : `${category.currentValue.toFixed(0)}%`;
15134
+ console.log(
15135
+ `\u251C\u2500\u2500 ${import_chalk23.default.white(category.name.padEnd(22))} ${value.padEnd(8)} ${emoji} (target: ${category.target}${category.name.includes("Efficiency") || category.name.includes("Actions") ? "%" : "%"}) [${Math.round(category.score)}/${category.maxScore} pts]`
15136
+ );
15137
+ });
15138
+ console.log();
15139
+ if (scorecard.quickWins.length > 0) {
15140
+ console.log(import_chalk23.default.bold("\u{1F3AF} Quick Wins to Improve Score:"));
15141
+ scorecard.quickWins.forEach((qw) => {
15142
+ const savingsText = qw.estimatedSavings ? import_chalk23.default.green(` (save ~$${qw.estimatedSavings.toFixed(0)})`) : "";
15143
+ console.log(`\u2022 ${qw.description} \u2192 +${qw.pointsGain} points${savingsText}`);
15144
+ });
15145
+ console.log();
15146
+ }
15147
+ if (scorecard.badges.length > 0) {
15148
+ console.log(import_chalk23.default.bold("\u{1F3C5} Badges Earned:"));
15149
+ scorecard.badges.forEach((badge) => {
15150
+ console.log(`${badge.emoji} ${import_chalk23.default.yellow(badge.name)} - ${import_chalk23.default.gray(badge.description)}`);
15151
+ });
15152
+ console.log();
15153
+ }
15154
+ console.log(import_chalk23.default.bold.blue("\u2550".repeat(70)));
15155
+ } catch (error) {
15156
+ console.error(import_chalk23.default.red("\u274C Failed to generate scorecard:"), error.message);
15157
+ process.exit(1);
15158
+ }
15159
+ }
15160
+ __name(handleScorecard, "handleScorecard");
15161
+ async function handleLeaderboard() {
15162
+ try {
15163
+ const teamNames = ["Platform Team", "Data Engineering", "Backend Engineering", "Frontend Team", "ML Team"];
15164
+ const scorecards = teamNames.map((name) => {
15165
+ const metrics = getMockTeamMetrics(name);
15166
+ return buildTeamScorecard(name, metrics);
15167
+ });
15168
+ const leaderboard = generateLeaderboard(scorecards);
15169
+ console.log();
15170
+ console.log(import_chalk23.default.bold.blue("\u2550".repeat(80)));
15171
+ console.log(import_chalk23.default.bold.white(` \u{1F3C6} FinOps Leaderboard - ${(/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { month: "long", year: "numeric" })}`));
15172
+ console.log(import_chalk23.default.bold.blue("\u2550".repeat(80)));
15173
+ console.log();
15174
+ const table = new import_cli_table32.default({
15175
+ head: [
15176
+ import_chalk23.default.bold("Rank"),
15177
+ import_chalk23.default.bold("Team"),
15178
+ import_chalk23.default.bold("Score"),
15179
+ import_chalk23.default.bold("Grade"),
15180
+ import_chalk23.default.bold("Trend"),
15181
+ import_chalk23.default.bold("Savings")
15182
+ ],
15183
+ colWidths: [8, 25, 10, 10, 12, 15]
15184
+ });
15185
+ leaderboard.forEach((entry) => {
15186
+ const rankEmoji = entry.rank === 1 ? "\u{1F947}" : entry.rank === 2 ? "\u{1F948}" : entry.rank === 3 ? "\u{1F949}" : ` ${entry.rank}`;
15187
+ table.push([
15188
+ rankEmoji,
15189
+ entry.teamName,
15190
+ entry.score.toString(),
15191
+ entry.grade,
15192
+ formatTrendArrow(entry.trend),
15193
+ import_chalk23.default.green(`$${entry.savings.toLocaleString()}`)
15194
+ ]);
15195
+ });
15196
+ console.log(table.toString());
15197
+ console.log();
15198
+ const mostImproved = leaderboard.reduce((max, entry) => entry.trend > max.trend ? entry : max, leaderboard[0]);
15199
+ const biggestSaver = leaderboard.reduce((max, entry) => entry.savings > max.savings ? entry : max, leaderboard[0]);
15200
+ console.log(import_chalk23.default.bold.blue("\u2550".repeat(80)));
15201
+ console.log(import_chalk23.default.yellow(`\u{1F396}\uFE0F Most Improved: ${mostImproved.teamName} (${formatTrendArrow(mostImproved.trend)} points)`));
15202
+ console.log(import_chalk23.default.green(`\u{1F4B0} Biggest Saver: ${biggestSaver.teamName} ($${biggestSaver.savings.toLocaleString()})`));
15203
+ console.log(import_chalk23.default.bold.blue("\u2550".repeat(80)));
15204
+ console.log();
15205
+ } catch (error) {
15206
+ console.error(import_chalk23.default.red("\u274C Failed to generate leaderboard:"), error.message);
15207
+ process.exit(1);
15208
+ }
15209
+ }
15210
+ __name(handleLeaderboard, "handleLeaderboard");
15211
+ function registerScorecardCommand(program) {
15212
+ const scorecard = program.command("scorecard").description("FinOps team performance scorecards and leaderboards");
15213
+ scorecard.command("show").description("Show team scorecard").option("-t, --team <name>", "Team name", "Backend Engineering").action(handleScorecard);
15214
+ scorecard.command("leaderboard").description("Show organization leaderboard").action(handleLeaderboard);
15215
+ scorecard.action(handleScorecard);
15216
+ }
15217
+ __name(registerScorecardCommand, "registerScorecardCommand");
15218
+
13890
15219
  // src/cli/middleware/auth.ts
13891
15220
  async function authMiddleware(thisCommand, actionCommand) {
13892
15221
  const isConfigCommand = actionCommand.name() === "config" || actionCommand.parent?.name() === "config";
@@ -13915,7 +15244,7 @@ async function validationMiddleware(thisCommand, actionCommand) {
13915
15244
  __name(validationMiddleware, "validationMiddleware");
13916
15245
 
13917
15246
  // src/cli/middleware/error-handler.ts
13918
- var import_chalk22 = __toESM(require("chalk"));
15247
+ var import_chalk24 = __toESM(require("chalk"));
13919
15248
  function errorHandler(error) {
13920
15249
  const message = error?.message ?? String(error);
13921
15250
  const stack = error?.stack;
@@ -13923,15 +15252,15 @@ function errorHandler(error) {
13923
15252
  return;
13924
15253
  }
13925
15254
  console.error("");
13926
- console.error(import_chalk22.default.red("\u2716"), import_chalk22.default.bold("Error:"), message);
15255
+ console.error(import_chalk24.default.red("\u2716"), import_chalk24.default.bold("Error:"), message);
13927
15256
  if (process.env.DEBUG || process.env.VERBOSE) {
13928
15257
  console.error("");
13929
15258
  if (stack) {
13930
- console.error(import_chalk22.default.gray(stack));
15259
+ console.error(import_chalk24.default.gray(stack));
13931
15260
  }
13932
15261
  } else {
13933
15262
  console.error("");
13934
- console.error(import_chalk22.default.gray("Run with --verbose for detailed error information"));
15263
+ console.error(import_chalk24.default.gray("Run with --verbose for detailed error information"));
13935
15264
  }
13936
15265
  console.error("");
13937
15266
  }
@@ -13941,7 +15270,7 @@ __name(errorHandler, "errorHandler");
13941
15270
  function createCLI() {
13942
15271
  const program = new import_commander.Command();
13943
15272
  program.exitOverride();
13944
- program.name("infra-cost").description(package_default.description).version(package_default.version);
15273
+ program.name("infra-cost").description(import_package.default.description).version(import_package.default.version);
13945
15274
  program.option("--provider <provider>", "Cloud provider (aws, gcp, azure, alibaba, oracle)", "aws").option("-p, --profile <profile>", "Cloud provider profile", "default").option("-r, --region <region>", "Cloud provider region", "us-east-1").option("-k, --access-key <key>", "Access key for cloud provider").option("-s, --secret-key <key>", "Secret key for cloud provider").option("-T, --session-token <token>", "Session token").option("--project-id <id>", "GCP Project ID").option("--key-file <path>", "Path to service account key file (GCP/Oracle)").option("--subscription-id <id>", "Azure Subscription ID").option("--tenant-id <id>", "Azure Tenant ID").option("--client-id <id>", "Azure Client ID").option("--client-secret <secret>", "Azure Client Secret").option("--user-id <id>", "Oracle User OCID").option("--tenancy-id <id>", "Oracle Tenancy OCID").option("--fingerprint <fp>", "Oracle Public Key Fingerprint").option("--config-file <path>", "Path to configuration file").option("--config-profile <name>", "Use named profile from config").option("--output <format>", "Output format (json, text, fancy, table)", "fancy").option("--no-color", "Disable colored output").option("--quiet", "Quiet mode - minimal output").option("--verbose", "Verbose output with debug information").option("--no-cache", "Disable caching").option("--cache-ttl <duration>", "Cache TTL (e.g., 4h, 30m)", "4h").option("--log-level <level>", "Logging level (debug, info, warn, error)", "info").option("--log-format <format>", "Log format (pretty, json)", "pretty");
13946
15275
  registerNowCommand(program);
13947
15276
  registerFreeTierCommand(program);
@@ -13960,6 +15289,8 @@ function createCLI() {
13960
15289
  registerRBACCommands(program);
13961
15290
  registerSSOCommands(program);
13962
15291
  registerPluginCommands(program);
15292
+ registerServerCommands(program);
15293
+ registerScorecardCommand(program);
13963
15294
  program.hook("preAction", async (thisCommand, actionCommand) => {
13964
15295
  const opts = thisCommand.opts();
13965
15296
  const logLevel = opts.verbose ? "debug" : opts.quiet ? "error" : opts.logLevel;