gitgreen 0.1.1 → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 GitGreen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,77 +1,241 @@
1
1
  # GitGreen CLI
2
2
 
3
- Self-contained carbon calculation CLI for GitLab jobs (no API server). It reuses the same power profiles and budget reporting as the existing implementation, pulls Electricity Maps intensity, and can post Merge Request notes via `CI_JOB_TOKEN`.
3
+ Self-contained carbon calculation CLI for GitLab jobs (no API server). It reuses the same power profiles and budget reporting as the existing implementation, pulls Electricity Maps intensity, and can post Merge Request notes via `CI_JOB_TOKEN`. Works with both GCP and AWS runners.
4
+
5
+ ## Prerequisites
6
+
7
+ Before installing GitGreen CLI, ensure you have the following installed and configured:
8
+
9
+ ### Required
10
+
11
+ - **Node.js** (v20 or higher) - [Download](https://nodejs.org/)
12
+ - Required to run the CLI tool
13
+ - Check version: `node --version`
14
+
15
+ - **npm** or **pnpm** - Package manager (comes with Node.js)
16
+ - This project uses `pnpm@8.15.4` as the package manager
17
+ - Install pnpm: `npm install -g pnpm`
18
+
19
+ - **Git** - [Download](https://git-scm.com/)
20
+ - Required for detecting GitLab project information during initialization
21
+ - Check version: `git --version`
22
+
23
+ ### Cloud Provider CLIs (Required based on provider)
24
+
25
+ - **Google Cloud SDK (gcloud CLI)** - [Installation Guide](https://cloud.google.com/sdk/docs/install-sdk)
26
+ - Required for GCP provider setup and authentication
27
+ - After installation, authenticate: `gcloud auth login`
28
+ - Set default project: `gcloud config set project YOUR_PROJECT_ID`
29
+ - Check version: `gcloud --version`
30
+
31
+ - **AWS Credentials** - [AWS CLI Installation](https://aws.amazon.com/cli/) (optional, but recommended)
32
+ - Required for AWS provider (can use environment variables or AWS CLI config)
33
+ - Configure credentials: `aws configure`
34
+ - Or set environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`
35
+
36
+ ### Optional (but recommended)
37
+
38
+ - **GitLab CLI (glab)** - [Installation Guide](https://docs.gitlab.com/cli/)
39
+ - Recommended for easier GitLab authentication and CI/CD variable management
40
+ - After installation, authenticate: `glab auth login`
41
+ - Check version: `glab --version`
42
+ - If not installed, you'll need to provide a GitLab Personal Access Token manually
43
+
44
+ ### API Keys
45
+
46
+ - **Electricity Maps API Key** - [Get free key](https://api-portal.electricitymaps.com)
47
+ - Required for carbon intensity data
48
+ - You'll be prompted for this during `gitgreen init`
4
49
 
5
50
  ## Install
6
51
  - From npm (global CLI):
7
52
  ```bash
8
- pnpm add -g gitgreen-cli
9
- # or: npm install -g gitgreen-cli
53
+ npm install -g gitgreen
10
54
  gitgreen --help
11
55
  ```
12
56
 
13
- - From this repo:
14
- ```bash
15
- # from repo root
16
- pnpm -C node-module install
17
- # build happens via prepare; dist/ is ready for CI
18
- ```
19
-
20
- Run tests (pnpm preferred):
57
+ Run tests:
21
58
  ```bash
22
- pnpm -C node-module test
23
- # or: npm --prefix node-module test
59
+ npm test
24
60
  ```
25
61
 
26
62
  Stress test multiple live configs (build first, real APIs):
27
63
  ```bash
28
- pnpm -C node-module build
29
- pnpm -C node-module stress
64
+ npm build
65
+ npm stress
30
66
  ```
31
67
 
32
68
  ## Usage
33
- ```bash
34
- gitgreen \
35
- --provider gcp \
36
- --machine e2-standard-4 \
37
- --region us-central1-a \
38
- --duration 1800 \
39
- --cpu 55 \
40
- --budget 500 \
41
- --fail-on-budget \
42
- --out-md carbon-report.md \
43
- --out-json carbon-report.json \
44
- --post-note
45
- ```
46
69
 
47
- ### Interactive GitLab setup
70
+ ### Initializing a New GitLab Project
71
+
72
+ To get started with carbon tracking in any GitLab project (any project with a remote pointing to a GitLab instance), run:
73
+
48
74
  ```bash
49
75
  # In your repo
50
76
  gitgreen init
51
77
  ```
52
- The wizard asks for provider/machine/region, budgets, MR note preference, then offers to append a ready-made job to `.gitlab-ci.yml` and prints the CI/CD variable checklist (ELECTRICITY_MAPS_API_KEY, provider creds, budget flags).
53
-
54
- Key environment variables (loaded automatically via dotenv):
55
- - `ELECTRICITY_MAPS_API_KEY` (required)
56
- - `ELECTRICITY_MAPS_BASE_URL` (optional, defaults to public endpoint)
57
- - `GCP_PROJECT_ID` / `GOOGLE_CLOUD_PROJECT` (for GCP metric pull)
58
- - `AWS_*` credentials (for CloudWatch metric pull)
59
- - `DATA_DIR` (override bundled data)
60
- - `DEFAULT_PROVIDER`, `PUE_FALLBACK`, `GITLAB_BASE_URL`
61
-
62
- GitLab CI auto-detection uses:
63
- - `CI_JOB_TOKEN`, `CI_PROJECT_ID`, `CI_JOB_ID`, `CI_PIPELINE_ID`, `CI_MERGE_REQUEST_IID`, `CI_SERVER_URL`
64
- - `CI_RUNNER_TAGS` (comma/space separated) to infer machine/region and instance IDs
65
-
66
- ## GitLab snippet
67
- Add variables `ELECTRICITY_MAPS_API_KEY`, `CARBON_BUDGET` (optional), `FAIL_ON_BUDGET` (true/false). Then call:
68
- ```bash
69
- ./scripts/carbon/install-cli.sh
70
- ./scripts/carbon/run-analysis.sh
78
+
79
+ The initialization wizard will guide you through the setup process:
80
+ - Configure provider/machine/region settings
81
+ - Set carbon budgets
82
+ - Configure MR note preferences
83
+ - Choose to use an existing runner or spin up a new one with supported providers
84
+
85
+ After initialization, the wizard will:
86
+ - Append a ready-made job to your `.gitlab-ci.yml`
87
+ - Print the CI/CD variable checklist (ELECTRICITY_MAPS_API_KEY, provider credentials, budget flags)
88
+
89
+ ### How It Works
90
+
91
+ Once initialized, all subsequent pipelines will run on the configured runner, and their performance will be automatically measured. The carbon tracking is implemented using GitLab CI/CD components as a final step in your pipeline. The carbon tracking job itself is not computationally expensive, so it adds minimal overhead to your CI/CD workflows.
92
+
93
+ Key environment variables:
94
+ - `ELECTRICITY_MAPS_API_KEY` (required) - Get a free key from https://api-portal.electricitymaps.com
95
+ - `GCP_PROJECT_ID` / `GOOGLE_CLOUD_PROJECT` (required for GCP) - Your GCP project ID
96
+ - `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (required for AWS CloudWatch) - Credentials with CloudWatch read access on the runner
97
+ - `AWS_REGION` (for AWS) - Region of the runner EC2 instance
98
+
99
+ The `gitgreen init` wizard will automatically set all necessary GitLab CI/CD variables for you.
100
+
101
+ ## Providers
102
+
103
+ GitGreen supports multiple cloud providers:
104
+
105
+ - **GCP**: Fully wired — the CLI parser expects GCP Monitoring JSON, the GitLab component polls GCE metrics, and `gitgreen init` provisions or reuses GCP runners.
106
+ - **AWS**: CloudWatch-backed — the CLI can pull metrics directly via `--from-cloudwatch`, the GitLab component fetches CloudWatch CPU/RAM data, and `gitgreen init` can configure or provision EC2 runners.
107
+
108
+ ## Architecture
109
+ Runtime flow:
110
+ ```
111
+ CPU/RAM timeseries (GCP Monitoring JSON or custom collector)
112
+ |
113
+ v
114
+ +------------------+ +----------------------------+
115
+ | CLI (gitgreen) |--->| CarbonCalculator |
116
+ | - parse metrics | | - PowerProfileRepository |<- machine data in data/*.json
117
+ | - CLI/env opts | | - ZoneMapper (region→zone) |<- static + runtime PUE mapping
118
+ +------------------+ | - IntensityProvider |<- Electricity Maps API
119
+ +-------------+------------+
120
+ |
121
+ v
122
+ +-----------------------------+
123
+ | Report formatter |
124
+ | - Markdown/JSON artifacts |
125
+ | - Budget evaluation |
126
+ +-------------+---------------+
127
+ |
128
+ v
129
+ +-----------------------------+
130
+ | GitLab client (optional) |
131
+ | - MR note via CI_JOB_TOKEN |
132
+ +-----------------------------+
133
+ ```
134
+
135
+ GitLab CI path:
136
+ ```
137
+ Pipeline starts → component script fetches CPU/RAM timeseries from GCP Monitoring or AWS CloudWatch
138
+ → writes JSON files → runs `gitgreen --provider gcp|aws ...`
139
+ → emits `carbon-report.md` / `carbon-report.json`
140
+ → optional MR note when CI_JOB_TOKEN is present
71
141
  ```
72
- Artifacts `carbon-report.md` and `carbon-report.json` will be produced; `POST_MR_NOTE=true` will post to the MR when `CI_JOB_TOKEN` is available.
142
+
143
+ ## Adding a provider
144
+ 1. Extend `CloudProvider` and the provider guard in `src/index.ts` so the calculator accepts the new key.
145
+ 2. Add machine power data (`<provider>_machine_power_profiles.json`) and, if needed, CPU profiles to `data/`, then update `PowerProfileRepository.loadMachineData` to load it.
146
+ 3. Map regions to Electricity Maps zones and a PUE default in `ZoneMapper` (or via `data/runtime-pue-mappings.json` for runtime overrides).
147
+ 4. Parse that provider's metrics into the `TimeseriesPoint` shape (timestamp + numeric value) alongside RAM size/usage, and update the CLI/init/templates to pull those metrics.
148
+ 5. Wire any CI automation (runner tags, MR note flags) to pass the correct provider, machine type, and region strings.
73
149
 
74
150
  ## Publish
75
151
  - Ensure version bump in `package.json`
76
152
  - Run `pnpm -C node-module build && pnpm -C node-module test`
77
153
  - Publish: `pnpm -C node-module publish --access public`
154
+
155
+ ## FAQ
156
+
157
+ ### Are my API keys and credentials secure?
158
+
159
+ Yes. GitGreen does not save your keys locally. During `gitgreen init`, all sensitive credentials (API keys, service account keys, AWS credentials) are stored only in GitLab CI/CD variables, which are encrypted and managed by GitLab. The CLI never writes credentials to disk on your local machine.
160
+
161
+ ### Does GitGreen call any third-party APIs?
162
+
163
+ GitGreen only calls APIs that you explicitly configure and authorize:
164
+
165
+ - **Electricity Maps API** - For carbon intensity data (requires your API key)
166
+ - **GitLab API** - Only when using `glab` CLI or providing a PAT, to set CI/CD variables
167
+ - **GCP Monitoring API** - To fetch CPU/RAM metrics from your GCP project (requires your service account)
168
+ - **AWS CloudWatch API** - To fetch CPU/RAM metrics from your AWS account (requires your AWS credentials)
169
+
170
+ GitGreen does not operate any backend services or call any APIs owned by the GitGreen project. All API calls are made directly from your environment or CI/CD pipeline using your credentials.
171
+
172
+ ### How do I know GitGreen won't destroy my `.gitlab-ci.yml` file?
173
+
174
+ GitGreen takes safety precautions when modifying your `.gitlab-ci.yml`:
175
+
176
+ 1. **Automatic backups** - Before making any changes, GitGreen creates a backup of your existing `.gitlab-ci.yml` file
177
+ 2. **Backup location** - The backup path is printed to the console (e.g., `/tmp/gitlab-ci-backup-{timestamp}.yml`)
178
+ 3. **Append-only changes** - GitGreen only appends new content to your CI file; it never removes or modifies existing jobs
179
+ 4. **Confirmation prompt** - You're asked to confirm before any changes are made
180
+
181
+ If something goes wrong, you can restore your file from the backup location that was printed.
182
+
183
+ ### Can I use GitGreen without the `init` wizard?
184
+
185
+ Yes. You can configure GitGreen manually by:
186
+
187
+ 1. Setting the required CI/CD variables in your GitLab project settings
188
+ 2. Adding the GitLab CI/CD component to your `.gitlab-ci.yml` manually
189
+ 3. Running `gitgreen` directly with command-line options instead of using the component
190
+
191
+ The `init` wizard is a convenience tool, but all functionality is available through manual configuration.
192
+
193
+ ### What data does GitGreen collect?
194
+
195
+ GitGreen only collects:
196
+
197
+ - **CPU and RAM usage metrics** from your cloud provider (GCP Monitoring or AWS CloudWatch)
198
+ - **Pipeline metadata** (start/end times) from GitLab CI/CD environment variables
199
+
200
+ GitGreen does not collect any personal information, code, or data from your repositories. All calculations happen locally in your CI/CD pipeline.
201
+
202
+ ### Can I run GitGreen locally for testing?
203
+
204
+ Yes. You can run `gitgreen` directly with your own metrics files:
205
+
206
+ **GCP example:**
207
+ ```bash
208
+ gitgreen --provider gcp \
209
+ --machine e2-standard-4 \
210
+ --region us-central1-a \
211
+ --cpu-timeseries cpu.json \
212
+ --ram-used-timeseries ram-used.json \
213
+ --ram-size-timeseries ram-size.json \
214
+ --out-md report.md
215
+ ```
216
+
217
+ **AWS example:**
218
+ ```bash
219
+ gitgreen --provider aws \
220
+ --machine t3.medium \
221
+ --region us-east-1 \
222
+ --cpu-timeseries cpu.json \
223
+ --ram-used-timeseries ram-used.json \
224
+ --ram-size-timeseries ram-size.json \
225
+ --out-md report.md
226
+ ```
227
+
228
+ Or use `--from-cloudwatch` to fetch metrics directly from AWS CloudWatch:
229
+ ```bash
230
+ gitgreen --provider aws \
231
+ --machine t3.medium \
232
+ --region us-east-1 \
233
+ --from-cloudwatch \
234
+ --out-md report.md
235
+ ```
236
+
237
+ This is useful for testing before integrating into your CI/CD pipeline.
238
+
239
+ ## License
240
+
241
+ MIT License - see [LICENSE](LICENSE) file for details.
package/dist/cli.js CHANGED
@@ -8,47 +8,217 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const commander_1 = require("commander");
9
9
  const kleur_1 = require("kleur");
10
10
  const asciichart_1 = __importDefault(require("asciichart"));
11
+ const config_1 = require("./config");
11
12
  const index_1 = require("./index");
12
13
  const init_1 = require("./init");
14
+ const cloudwatch_1 = require("./lib/aws/cloudwatch");
15
+ const power_profile_repository_1 = require("./lib/carbon/power-profile-repository");
13
16
  const program = new commander_1.Command();
14
- function parseGcpTimeseries(filePath) {
17
+ const toIsoTimestamp = (input) => {
18
+ if (input instanceof Date) {
19
+ return Number.isNaN(input.getTime()) ? undefined : input.toISOString();
20
+ }
21
+ if (typeof input === 'string' || typeof input === 'number') {
22
+ const date = new Date(input);
23
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
24
+ }
25
+ return undefined;
26
+ };
27
+ const toNumericValue = (input) => {
28
+ if (typeof input === 'number') {
29
+ return Number.isFinite(input) ? input : undefined;
30
+ }
31
+ if (typeof input === 'string') {
32
+ const parsed = Number(input);
33
+ return Number.isFinite(parsed) ? parsed : undefined;
34
+ }
35
+ return undefined;
36
+ };
37
+ const formatErrorMessage = (value) => {
38
+ if (value instanceof Error)
39
+ return value.message;
40
+ if (value === null || value === undefined)
41
+ return 'Unknown error';
42
+ return String(value);
43
+ };
44
+ const parseGcpTimeseries = (raw) => {
45
+ const points = [];
46
+ const response = raw;
47
+ const timeSeries = response.timeSeries;
48
+ if (!Array.isArray(timeSeries))
49
+ return points;
50
+ for (const ts of timeSeries) {
51
+ const series = ts;
52
+ if (!Array.isArray(series.points))
53
+ continue;
54
+ for (const point of series.points) {
55
+ const timestamp = toIsoTimestamp(point.interval?.endTime || point.interval?.startTime);
56
+ const value = point.value?.doubleValue !== undefined
57
+ ? toNumericValue(point.value.doubleValue)
58
+ : toNumericValue(point.value?.int64Value);
59
+ if (timestamp && value !== undefined)
60
+ points.push({ timestamp, value });
61
+ }
62
+ }
63
+ return points;
64
+ };
65
+ const parseCloudWatchMetricData = (raw) => {
66
+ const data = raw.MetricDataResults || raw.metricDataResults;
67
+ const results = Array.isArray(data) ? data : [];
68
+ if (!results.length)
69
+ return [];
70
+ const first = results[0];
71
+ const timestamps = Array.isArray(first?.Timestamps) ? first.Timestamps : [];
72
+ const values = Array.isArray(first?.Values) ? first.Values : [];
73
+ const len = Math.min(timestamps.length, values.length);
74
+ const points = [];
75
+ for (let i = 0; i < len; i++) {
76
+ const timestamp = toIsoTimestamp(timestamps[i]);
77
+ const value = toNumericValue(values[i]);
78
+ if (timestamp && value !== undefined) {
79
+ points.push({ timestamp, value });
80
+ }
81
+ }
82
+ return points;
83
+ };
84
+ const parseTimeseriesFile = (filePath) => {
15
85
  const raw = JSON.parse(fs_1.default.readFileSync(filePath, 'utf8'));
86
+ const gcp = parseGcpTimeseries(raw);
87
+ if (gcp.length)
88
+ return gcp;
89
+ const cw = parseCloudWatchMetricData(raw);
90
+ if (cw.length)
91
+ return cw;
92
+ if (Array.isArray(raw)) {
93
+ const cwFromArray = parseCloudWatchMetricData({ MetricDataResults: raw });
94
+ if (cwFromArray.length)
95
+ return cwFromArray;
96
+ const legacy = parseLegacyTimeseries(raw);
97
+ if (legacy.length)
98
+ return legacy;
99
+ }
100
+ const datapointResponse = raw;
101
+ if (Array.isArray(datapointResponse.Datapoints)) {
102
+ const datapointSeries = convertCloudWatchDatapoints(datapointResponse.Datapoints);
103
+ if (datapointSeries.length)
104
+ return datapointSeries;
105
+ }
106
+ return [];
107
+ };
108
+ const parseLegacyTimeseries = (entries) => {
16
109
  const points = [];
17
- for (const ts of raw.timeSeries || []) {
18
- for (const p of ts.points || []) {
19
- const timestamp = p.interval?.endTime || p.interval?.startTime;
20
- let value;
21
- if (p.value?.doubleValue !== undefined) {
22
- value = p.value.doubleValue;
23
- }
24
- else if (p.value?.int64Value !== undefined) {
25
- value = Number(p.value.int64Value);
26
- }
27
- else {
28
- continue;
29
- }
110
+ for (const entry of entries) {
111
+ const timestamp = toIsoTimestamp((entry.timestamp ?? entry.Timestamp));
112
+ const value = toNumericValue((entry.value ?? entry.Value));
113
+ if (timestamp && value !== undefined) {
30
114
  points.push({ timestamp, value });
31
115
  }
32
116
  }
33
117
  return points;
34
- }
118
+ };
119
+ const convertCloudWatchDatapoints = (entries) => {
120
+ const points = [];
121
+ for (const entry of entries) {
122
+ const timestamp = toIsoTimestamp(entry.Timestamp);
123
+ const value = toNumericValue((entry.Average ?? entry.Maximum ?? entry.Minimum ?? entry.Sum));
124
+ if (timestamp && value !== undefined) {
125
+ points.push({ timestamp, value });
126
+ }
127
+ }
128
+ return points;
129
+ };
35
130
  const runCalculate = async (opts) => {
36
131
  const gitlabDisabled = opts.gitlab === false;
37
- if (!opts.cpuTimeseries || !opts.ramUsedTimeseries || !opts.ramSizeTimeseries) {
38
- console.error((0, kleur_1.red)('Error: --cpu-timeseries, --ram-used-timeseries, and --ram-size-timeseries are required'));
132
+ const provider = (opts.provider || config_1.config.defaultProvider);
133
+ const regionInput = opts.region || opts.awsRegion;
134
+ const awsEnvRegion = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
135
+ const calculationRegion = provider === 'aws' ? (regionInput || awsEnvRegion) : regionInput;
136
+ if (!opts.machine) {
137
+ console.error((0, kleur_1.red)('Error: --machine is required'));
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ if (!calculationRegion) {
142
+ console.error((0, kleur_1.red)('Error: --region (or --aws-region) is required'));
39
143
  process.exitCode = 1;
40
144
  return;
41
145
  }
42
- const cpuTimeseries = parseGcpTimeseries(opts.cpuTimeseries);
43
- const ramUsedTimeseries = parseGcpTimeseries(opts.ramUsedTimeseries);
44
- const ramSizeTimeseries = parseGcpTimeseries(opts.ramSizeTimeseries);
146
+ let cpuTimeseries = [];
147
+ let ramUsedTimeseries = [];
148
+ let ramSizeTimeseries = [];
149
+ const timeseriesProvided = Boolean(opts.cpuTimeseries && opts.ramUsedTimeseries && opts.ramSizeTimeseries);
150
+ const shouldFetchCloudWatch = provider === 'aws' && (opts.fromCloudwatch || (!timeseriesProvided && opts.awsInstanceId));
151
+ if (shouldFetchCloudWatch) {
152
+ const instanceId = opts.awsInstanceId;
153
+ const resolvedRegion = calculationRegion || awsEnvRegion;
154
+ if (!resolvedRegion || !instanceId) {
155
+ console.error((0, kleur_1.red)('Error: --aws-instance-id and --region/--aws-region are required for AWS provider'));
156
+ process.exitCode = 1;
157
+ return;
158
+ }
159
+ const awsInstanceId = instanceId;
160
+ const awsRegionValue = resolvedRegion;
161
+ const startTime = opts.awsStartTime
162
+ ? new Date(opts.awsStartTime)
163
+ : process.env.CI_PIPELINE_CREATED_AT
164
+ ? new Date(process.env.CI_PIPELINE_CREATED_AT)
165
+ : new Date(Date.now() - 60 * 60 * 1000);
166
+ const endTime = opts.awsEndTime ? new Date(opts.awsEndTime) : new Date();
167
+ const periodSeconds = opts.awsPeriod ? Number(opts.awsPeriod) : 60;
168
+ let memoryBytesFallback;
169
+ try {
170
+ const repo = new power_profile_repository_1.PowerProfileRepository(config_1.config.dataDir);
171
+ const profile = await repo.getMachineProfile('aws', opts.machine);
172
+ if (profile?.memoryGb) {
173
+ memoryBytesFallback = profile.memoryGb * 1024 * 1024 * 1024;
174
+ }
175
+ }
176
+ catch { }
177
+ try {
178
+ const cw = await (0, cloudwatch_1.fetchCloudWatchTimeseries)({
179
+ region: awsRegionValue,
180
+ instanceId: awsInstanceId,
181
+ startTime,
182
+ endTime,
183
+ periodSeconds,
184
+ memoryBytesFallback
185
+ });
186
+ cpuTimeseries = cw.cpuUtilization;
187
+ ramUsedTimeseries = cw.ramUsed;
188
+ ramSizeTimeseries = cw.ramTotal;
189
+ if (!ramSizeTimeseries.length && memoryBytesFallback) {
190
+ const ts = ramUsedTimeseries[0]?.timestamp || new Date().toISOString();
191
+ ramSizeTimeseries = [{ timestamp: ts, value: memoryBytesFallback }];
192
+ }
193
+ }
194
+ catch (errorValue) {
195
+ console.error((0, kleur_1.red)('Error fetching CloudWatch metrics:'), formatErrorMessage(errorValue));
196
+ process.exitCode = 1;
197
+ return;
198
+ }
199
+ }
200
+ else {
201
+ if (!timeseriesProvided) {
202
+ console.error((0, kleur_1.red)('Error: --cpu-timeseries, --ram-used-timeseries, and --ram-size-timeseries are required'));
203
+ process.exitCode = 1;
204
+ return;
205
+ }
206
+ cpuTimeseries = parseTimeseriesFile(opts.cpuTimeseries);
207
+ ramUsedTimeseries = parseTimeseriesFile(opts.ramUsedTimeseries);
208
+ ramSizeTimeseries = parseTimeseriesFile(opts.ramSizeTimeseries);
209
+ }
45
210
  console.log((0, kleur_1.gray)(`Loaded ${cpuTimeseries.length} CPU points, ${ramUsedTimeseries.length} RAM points`));
46
211
  // Sort by timestamp for charts
47
212
  const cpuSorted = [...cpuTimeseries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
48
213
  const ramSorted = [...ramUsedTimeseries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
49
214
  const ramSizeSorted = [...ramSizeTimeseries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
50
215
  // CPU chart (only if more than 1 point)
51
- const cpuValues = cpuSorted.map(p => p.value * 100);
216
+ const cpuValues = cpuSorted.map(p => {
217
+ if (provider === 'aws') {
218
+ return p.value <= 1 ? p.value * 100 : p.value;
219
+ }
220
+ return p.value * 100;
221
+ });
52
222
  if (cpuValues.length > 1) {
53
223
  console.log((0, kleur_1.bold)('\nCPU Utilization (%)'));
54
224
  console.log(asciichart_1.default.plot(cpuValues, { height: 8, format: (x) => x.toFixed(1).padStart(6) }));
@@ -62,9 +232,9 @@ const runCalculate = async (opts) => {
62
232
  }
63
233
  try {
64
234
  const { result, budget, markdown } = await (0, index_1.calculate)({
65
- provider: opts.provider,
235
+ provider,
66
236
  machineType: opts.machine,
67
- region: opts.region,
237
+ region: calculationRegion,
68
238
  cpuTimeseries,
69
239
  ramUsedTimeseries,
70
240
  ramSizeTimeseries,
@@ -102,20 +272,26 @@ const runCalculate = async (opts) => {
102
272
  process.exitCode = 1;
103
273
  }
104
274
  }
105
- catch (err) {
106
- console.error((0, kleur_1.red)('Error:'), err?.message || err);
275
+ catch (errorValue) {
276
+ console.error((0, kleur_1.red)('Error:'), formatErrorMessage(errorValue));
107
277
  process.exitCode = 1;
108
278
  }
109
279
  };
110
280
  program
111
281
  .name('gitgreen')
112
282
  .description('GitGreen carbon calculator using real timeseries metrics')
113
- .option('-p, --provider <provider>', 'Cloud provider (gcp)')
283
+ .option('-p, --provider <provider>', 'Cloud provider (gcp|aws)', config_1.config.defaultProvider)
114
284
  .option('-m, --machine <type>', 'Machine type (e.g., e2-standard-4)')
115
- .option('-r, --region <region>', 'Region/zone (e.g., us-central1-a)')
116
- .option('--cpu-timeseries <path>', 'Path to CPU timeseries JSON (GCP Monitoring format)')
285
+ .option('-r, --region <region>', 'Region/zone (e.g., us-central1-a or us-east-1)')
286
+ .option('--cpu-timeseries <path>', 'Path to CPU timeseries JSON (GCP Monitoring or CloudWatch)')
117
287
  .option('--ram-used-timeseries <path>', 'Path to RAM used timeseries JSON')
118
288
  .option('--ram-size-timeseries <path>', 'Path to RAM size timeseries JSON')
289
+ .option('--aws-instance-id <id>', 'AWS EC2 instance ID (for CloudWatch fetch)')
290
+ .option('--aws-region <region>', 'AWS region (overrides --region)')
291
+ .option('--aws-start-time <iso>', 'Custom start time (ISO) for CloudWatch metrics')
292
+ .option('--aws-end-time <iso>', 'Custom end time (ISO) for CloudWatch metrics')
293
+ .option('--aws-period <seconds>', 'CloudWatch metric period in seconds', v => Number(v))
294
+ .option('--from-cloudwatch', 'Fetch CPU/RAM metrics from CloudWatch (AWS only)')
119
295
  .option('--budget <grams>', 'Carbon budget in grams CO2e', v => Number(v))
120
296
  .option('--fail-on-budget', 'Exit non-zero when budget exceeded')
121
297
  .option('--out-md <path>', 'Write markdown report')