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/dist/init.js CHANGED
@@ -10,6 +10,7 @@ const path_1 = __importDefault(require("path"));
10
10
  const axios_1 = __importDefault(require("axios"));
11
11
  const prompts_1 = __importDefault(require("prompts"));
12
12
  const kleur_1 = require("kleur");
13
+ const power_profile_repository_1 = require("./lib/carbon/power-profile-repository");
13
14
  const hasGlab = () => {
14
15
  try {
15
16
  (0, child_process_1.execSync)('glab --version', { stdio: 'ignore' });
@@ -80,12 +81,19 @@ const setVariable = async (auth, project, key, value, masked = false) => {
80
81
  return setVariableApi(auth.baseUrl, auth.pat, project, key, value, masked);
81
82
  }
82
83
  };
83
- const generateCiJob = (opts = {}) => {
84
- const { runnerTag, carbonBudget, failOnBudget } = opts;
85
- let inputs = ` gcp_project_id: $GCP_PROJECT_ID
86
- gcp_instance_id: $GCP_INSTANCE_ID
87
- gcp_zone: $GCP_ZONE
84
+ const generateCiJob = (opts) => {
85
+ const { provider, runnerTag, carbonBudget, failOnBudget } = opts;
86
+ let inputs = ` provider: ${provider}
88
87
  machine_type: $MACHINE_TYPE`;
88
+ if (provider === 'gcp') {
89
+ inputs += `\n gcp_project_id: $GCP_PROJECT_ID\n gcp_instance_id: $GCP_INSTANCE_ID\n gcp_zone: $GCP_ZONE`;
90
+ }
91
+ else {
92
+ inputs += `\n aws_region: $AWS_REGION\n aws_instance_id: $AWS_INSTANCE_ID`;
93
+ if (opts.aws?.periodSeconds) {
94
+ inputs += `\n aws_period_seconds: "${opts.aws.periodSeconds}"`;
95
+ }
96
+ }
89
97
  if (carbonBudget) {
90
98
  inputs += `\n carbon_budget_grams: "${carbonBudget}"`;
91
99
  }
@@ -95,7 +103,7 @@ const generateCiJob = (opts = {}) => {
95
103
  const tagsSection = runnerTag ? `\n tags:\n - ${runnerTag}` : '';
96
104
  return `# GitGreen carbon analysis (generated by gitgreen init)
97
105
  include:
98
- - component: gitlab.com/youneslaaroussi/gitgreen/gitgreen-cli-component@0.1.0
106
+ - component: gitlab.com/youneslaaroussi/gitgreen/gitgreen-cli-component@main
99
107
  inputs:
100
108
  ${inputs}
101
109
 
@@ -103,6 +111,565 @@ carbon_analysis:
103
111
  extends: .gitgreen-carbon-analysis${tagsSection}
104
112
  `;
105
113
  };
114
+ const applyCiJobSnippet = async (ciJob) => {
115
+ const { addCiJob } = await (0, prompts_1.default)({
116
+ type: 'confirm',
117
+ name: 'addCiJob',
118
+ message: 'Add job to .gitlab-ci.yml?',
119
+ initial: true
120
+ });
121
+ const ciPath = path_1.default.join(process.cwd(), '.gitlab-ci.yml');
122
+ if (addCiJob) {
123
+ if (fs_1.default.existsSync(ciPath)) {
124
+ const backupPath = `/tmp/gitlab-ci-backup-${Date.now()}.yml`;
125
+ const existing = fs_1.default.readFileSync(ciPath, 'utf8');
126
+ fs_1.default.writeFileSync(backupPath, existing);
127
+ console.log((0, kleur_1.gray)(`Backup saved to: ${backupPath}`));
128
+ fs_1.default.writeFileSync(ciPath, existing + '\n' + ciJob);
129
+ }
130
+ else {
131
+ fs_1.default.writeFileSync(ciPath, 'stages:\n - test\n\n' + ciJob);
132
+ }
133
+ console.log((0, kleur_1.green)('Updated .gitlab-ci.yml'));
134
+ }
135
+ else {
136
+ console.log('\nCI job snippet:\n');
137
+ console.log(ciJob);
138
+ }
139
+ };
140
+ const getAwsMachineTypes = (() => {
141
+ let cached = null;
142
+ return () => {
143
+ if (cached)
144
+ return cached;
145
+ try {
146
+ const repo = new power_profile_repository_1.PowerProfileRepository(path_1.default.join(__dirname, '..', 'data'));
147
+ cached = repo.listMachines('aws');
148
+ return cached;
149
+ }
150
+ catch {
151
+ cached = [];
152
+ return cached;
153
+ }
154
+ };
155
+ })();
156
+ const promptMachineType = async (message, initial) => {
157
+ const machines = getAwsMachineTypes();
158
+ if (!machines.length) {
159
+ const { manualMachine } = await (0, prompts_1.default)({
160
+ type: 'text',
161
+ name: 'manualMachine',
162
+ message,
163
+ initial
164
+ });
165
+ return manualMachine || initial;
166
+ }
167
+ const choices = [
168
+ ...machines.map(m => ({ title: m, value: m })),
169
+ { title: 'Enter manually', value: '_manual_' }
170
+ ];
171
+ const { selectedType } = await (0, prompts_1.default)({
172
+ type: 'select',
173
+ name: 'selectedType',
174
+ message,
175
+ choices,
176
+ initial: Math.max(0, machines.indexOf(initial))
177
+ });
178
+ if (selectedType === '_manual_') {
179
+ const { manualMachine } = await (0, prompts_1.default)({
180
+ type: 'text',
181
+ name: 'manualMachine',
182
+ message,
183
+ initial
184
+ });
185
+ return manualMachine || initial;
186
+ }
187
+ return selectedType;
188
+ };
189
+ const listAwsInstances = (region, env) => {
190
+ try {
191
+ const query = "Reservations[].Instances[].{InstanceId:InstanceId,Type:InstanceType,State:State.Name,Name:Tags[?Key==\\`Name\\`]|[0].Value}";
192
+ const raw = (0, child_process_1.execSync)(`aws ec2 describe-instances --region ${region} --query "${query}" --output json`, { encoding: 'utf8', env });
193
+ const parsed = JSON.parse(raw);
194
+ return parsed
195
+ .filter(item => Boolean(item.InstanceId))
196
+ .map(item => ({
197
+ instanceId: String(item.InstanceId),
198
+ type: item.Type,
199
+ state: item.State,
200
+ name: item.Name
201
+ }));
202
+ }
203
+ catch {
204
+ return [];
205
+ }
206
+ };
207
+ const ensureAwsCli = () => {
208
+ try {
209
+ (0, child_process_1.execSync)('aws --version', { stdio: 'ignore' });
210
+ }
211
+ catch {
212
+ console.log((0, kleur_1.red)('aws CLI not found. Install from https://aws.amazon.com/cli/'));
213
+ process.exit(1);
214
+ }
215
+ };
216
+ const resolveAmazonLinuxAmi = (region, env) => {
217
+ try {
218
+ const ami = (0, child_process_1.execSync)(`aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64 --query 'Parameter.Value' --output text --region ${region}`, { encoding: 'utf8', env }).trim();
219
+ if (ami)
220
+ return ami;
221
+ }
222
+ catch { }
223
+ return '';
224
+ };
225
+ const createRunnerToken = (auth, projectPath, description, runnerTag) => {
226
+ if (auth.type !== 'glab')
227
+ return '';
228
+ try {
229
+ const projectEncoded = encodeURIComponent(projectPath);
230
+ const projectInfoJson = (0, child_process_1.execSync)(`glab api "projects/${projectEncoded}"`, { encoding: 'utf8' });
231
+ const projectInfo = JSON.parse(projectInfoJson);
232
+ const projectId = projectInfo.id;
233
+ const createResult = (0, child_process_1.execSync)(`glab api --method POST "user/runners" -f runner_type=project_type -f project_id=${projectId} -f description="${description}" -f tag_list="${runnerTag}" -f run_untagged=true`, { encoding: 'utf8' });
234
+ const runnerData = JSON.parse(createResult);
235
+ console.log((0, kleur_1.green)(`Runner token created (ID: ${runnerData.id})`));
236
+ return runnerData.token;
237
+ }
238
+ catch (err) {
239
+ console.log((0, kleur_1.gray)('Could not create token via API, falling back to manual...'));
240
+ return '';
241
+ }
242
+ };
243
+ const buildAwsUserData = (params) => {
244
+ const { region, runnerToken, runnerName, runnerTag, machineType, periodSeconds } = params;
245
+ const lines = [
246
+ '#!/usr/bin/env bash',
247
+ 'set -euo pipefail',
248
+ `REGION=${region}`,
249
+ `RUNNER_TOKEN="${runnerToken}"`,
250
+ `RUNNER_NAME="${runnerName}"`,
251
+ `RUNNER_TAGS="${runnerTag}"`,
252
+ `MACHINE_TYPE="${machineType}"`,
253
+ `PERIOD=${periodSeconds}`,
254
+ '',
255
+ 'dnf update -y',
256
+ 'dnf install -y amazon-cloudwatch-agent jq tar',
257
+ 'dnf install -y curl --allowerasing',
258
+ 'curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | bash',
259
+ 'dnf install -y gitlab-runner',
260
+ 'curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -',
261
+ 'dnf install -y nodejs',
262
+ '',
263
+ 'INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)',
264
+ '',
265
+ "cat > /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json <<'EOF'",
266
+ '{',
267
+ ' "metrics": {',
268
+ ' "append_dimensions": {',
269
+ ' "InstanceId": "${aws:InstanceId}"',
270
+ ' },',
271
+ ' "metrics_collected": {',
272
+ ' "mem": {',
273
+ ' "measurement": ["mem_used", "mem_total", "mem_used_percent"],',
274
+ ` "metrics_collection_interval": ${periodSeconds}`,
275
+ ' },',
276
+ ' "cpu": {',
277
+ ' "measurement": ["cpu_usage_active"],',
278
+ ` "metrics_collection_interval": ${periodSeconds}`,
279
+ ' }',
280
+ ' }',
281
+ ' }',
282
+ '}',
283
+ 'EOF',
284
+ '',
285
+ '/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json -s',
286
+ '',
287
+ 'gitlab-runner register --non-interactive --url https://gitlab.com/ --token "$RUNNER_TOKEN" --executor shell --description "$RUNNER_NAME"',
288
+ 'RUNNER_ID=$(gitlab-runner list | grep -Eo \'[^ ]+ \\(ID: [0-9]+\\)\' | grep "$RUNNER_NAME" | sed -E \'s/.*ID: ([0-9]+).*/\\1/\')',
289
+ 'if [ -n "$RUNNER_ID" ]; then',
290
+ ' gitlab-runner verify --run-untagged --add --name "$RUNNER_NAME" || true',
291
+ ' gitlab-runner verify --tag-list "$RUNNER_TAGS" --add --name "$RUNNER_NAME" || true',
292
+ 'fi',
293
+ 'echo "$INSTANCE_ID" > /opt/gitlab-runner-instance-id.txt',
294
+ 'systemctl enable amazon-cloudwatch-agent',
295
+ 'systemctl enable gitlab-runner',
296
+ 'systemctl start amazon-cloudwatch-agent',
297
+ 'systemctl start gitlab-runner'
298
+ ];
299
+ return lines.join('\n');
300
+ };
301
+ const provisionAwsRunnerInstance = (awsEnv, params) => {
302
+ const userData = buildAwsUserData({
303
+ region: params.region,
304
+ runnerToken: params.runnerToken,
305
+ runnerName: params.runnerName,
306
+ runnerTag: params.runnerTag,
307
+ machineType: params.machineType,
308
+ periodSeconds: params.periodSeconds
309
+ });
310
+ const tmpFile = `/tmp/gitgreen-aws-userdata-${Date.now()}.sh`;
311
+ fs_1.default.writeFileSync(tmpFile, userData);
312
+ const tagSpec = `ResourceType=instance,Tags=[{Key=Name,Value=${params.runnerName}},{Key=gitlab-runner,Value=gitgreen}]`;
313
+ const subnetFlag = params.subnetId ? `--subnet-id ${params.subnetId}` : '';
314
+ const sgFlag = params.securityGroupId ? `--security-group-ids ${params.securityGroupId}` : '';
315
+ const keyFlag = params.keyName ? `--key-name ${params.keyName}` : '';
316
+ const iamFlag = params.iamInstanceProfile ? `--iam-instance-profile Name=${params.iamInstanceProfile}` : '';
317
+ try {
318
+ const resultJson = (0, child_process_1.execSync)(`aws ec2 run-instances --image-id ${params.amiId} --count 1 --instance-type ${params.machineType} ${subnetFlag} ${sgFlag} ${keyFlag} ${iamFlag} --user-data file://${tmpFile} --tag-specifications '${tagSpec}' --region ${params.region} --output json`, { encoding: 'utf8', env: awsEnv, stdio: ['pipe', 'pipe', 'pipe'] });
319
+ const parsed = JSON.parse(resultJson);
320
+ return parsed?.Instances?.[0]?.InstanceId || '';
321
+ }
322
+ finally {
323
+ fs_1.default.unlinkSync(tmpFile);
324
+ }
325
+ };
326
+ const runAwsInit = async (auth, projectPath) => {
327
+ console.log((0, kleur_1.gray)('\nStep 4: AWS Runner'));
328
+ console.log((0, kleur_1.gray)('─'.repeat(40)));
329
+ console.log((0, kleur_1.gray)('Provide the details for your existing GitLab runner on AWS.'));
330
+ console.log((0, kleur_1.gray)('Ensure CloudWatch Agent publishes mem_used and mem_total for RAM data.'));
331
+ const { awsRegion } = await (0, prompts_1.default)({
332
+ type: 'text',
333
+ name: 'awsRegion',
334
+ message: 'AWS region (e.g., us-east-1)',
335
+ initial: 'us-east-1'
336
+ });
337
+ if (!awsRegion) {
338
+ console.log((0, kleur_1.red)('AWS region required'));
339
+ process.exit(1);
340
+ }
341
+ const { runnerSetup } = await (0, prompts_1.default)({
342
+ type: 'select',
343
+ name: 'runnerSetup',
344
+ message: 'Runner setup',
345
+ choices: [
346
+ { title: 'Use existing runner', value: 'existing' },
347
+ { title: 'Provision new AWS runner', value: 'provision' }
348
+ ]
349
+ });
350
+ let instanceId = '';
351
+ let machineType = '';
352
+ let periodSeconds = 60;
353
+ let runnerTag = 'aws';
354
+ let pendingInstanceSelection = false;
355
+ if (runnerSetup === 'existing') {
356
+ const existingAnswers = await (0, prompts_1.default)([
357
+ {
358
+ type: 'text',
359
+ name: 'runnerTag',
360
+ message: 'Runner tag (for routing CI jobs)',
361
+ initial: 'aws'
362
+ },
363
+ {
364
+ type: 'number',
365
+ name: 'periodSeconds',
366
+ message: 'CloudWatch metrics period (seconds)',
367
+ initial: 60
368
+ }
369
+ ]);
370
+ runnerTag = existingAnswers.runnerTag || 'aws';
371
+ periodSeconds = existingAnswers.periodSeconds || 60;
372
+ machineType = await promptMachineType('Instance type', 'm5.large');
373
+ pendingInstanceSelection = true;
374
+ }
375
+ else {
376
+ ensureAwsCli();
377
+ const selectedMachineType = await promptMachineType('Instance type', 'm5.large');
378
+ const provisionAnswers = await (0, prompts_1.default)([
379
+ {
380
+ type: 'text',
381
+ name: 'runnerName',
382
+ message: 'Runner name/description',
383
+ initial: 'aws-carbon-runner'
384
+ },
385
+ {
386
+ type: 'text',
387
+ name: 'runnerTag',
388
+ message: 'Runner tag (for routing CI jobs)',
389
+ initial: 'aws'
390
+ },
391
+ {
392
+ type: 'number',
393
+ name: 'periodSeconds',
394
+ message: 'CloudWatch metrics period (seconds)',
395
+ initial: 60
396
+ },
397
+ {
398
+ type: 'text',
399
+ name: 'subnetId',
400
+ message: 'Subnet ID (optional, leave empty for default VPC)',
401
+ initial: ''
402
+ },
403
+ {
404
+ type: 'text',
405
+ name: 'securityGroupId',
406
+ message: 'Security Group ID (optional)',
407
+ initial: ''
408
+ },
409
+ {
410
+ type: 'text',
411
+ name: 'keyName',
412
+ message: 'EC2 key pair name (optional)',
413
+ initial: ''
414
+ },
415
+ {
416
+ type: 'text',
417
+ name: 'iamInstanceProfile',
418
+ message: 'IAM instance profile name (recommended for CloudWatch Agent, optional)',
419
+ initial: ''
420
+ }
421
+ ]);
422
+ machineType = selectedMachineType;
423
+ periodSeconds = provisionAnswers.periodSeconds || 60;
424
+ runnerTag = provisionAnswers.runnerTag || 'aws';
425
+ const runnerName = provisionAnswers.runnerName || 'aws-carbon-runner';
426
+ const subnetId = provisionAnswers.subnetId || '';
427
+ const securityGroupId = provisionAnswers.securityGroupId || '';
428
+ const keyName = provisionAnswers.keyName || '';
429
+ const iamInstanceProfile = provisionAnswers.iamInstanceProfile || '';
430
+ let runnerToken = createRunnerToken(auth, projectPath, runnerName, runnerTag);
431
+ if (!runnerToken) {
432
+ const { manualToken } = await (0, prompts_1.default)({
433
+ type: 'password',
434
+ name: 'manualToken',
435
+ message: 'GitLab runner token (glrt-...)'
436
+ });
437
+ runnerToken = manualToken;
438
+ }
439
+ if (!runnerToken) {
440
+ console.log((0, kleur_1.red)('Runner token required to provision.'));
441
+ process.exit(1);
442
+ }
443
+ // Credentials will be collected below (Step 5) before provisioning
444
+ const provisionConfig = {
445
+ runnerName,
446
+ runnerToken,
447
+ subnetId,
448
+ securityGroupId,
449
+ keyName,
450
+ iamInstanceProfile
451
+ };
452
+ runAwsInit._provisionConfig = provisionConfig;
453
+ }
454
+ console.log((0, kleur_1.gray)('\nStep 5: AWS Credentials'));
455
+ console.log((0, kleur_1.gray)('─'.repeat(40)));
456
+ console.log((0, kleur_1.gray)('Use an access key with CloudWatch read permissions for the runner instance.'));
457
+ const { accessKeyId } = await (0, prompts_1.default)({
458
+ type: 'password',
459
+ name: 'accessKeyId',
460
+ message: 'AWS Access Key ID',
461
+ initial: process.env.AWS_ACCESS_KEY_ID || ''
462
+ });
463
+ if (!accessKeyId) {
464
+ console.log((0, kleur_1.red)('AWS Access Key ID required'));
465
+ process.exit(1);
466
+ }
467
+ const { secretAccessKey } = await (0, prompts_1.default)({
468
+ type: 'password',
469
+ name: 'secretAccessKey',
470
+ message: 'AWS Secret Access Key',
471
+ initial: process.env.AWS_SECRET_ACCESS_KEY || ''
472
+ });
473
+ if (!secretAccessKey) {
474
+ console.log((0, kleur_1.red)('AWS Secret Access Key required'));
475
+ process.exit(1);
476
+ }
477
+ const { sessionToken } = await (0, prompts_1.default)({
478
+ type: 'password',
479
+ name: 'sessionToken',
480
+ message: 'AWS Session Token (optional)',
481
+ initial: process.env.AWS_SESSION_TOKEN || ''
482
+ });
483
+ const awsEnv = {
484
+ ...process.env,
485
+ AWS_REGION: awsRegion,
486
+ AWS_DEFAULT_REGION: awsRegion,
487
+ AWS_ACCESS_KEY_ID: accessKeyId,
488
+ AWS_SECRET_ACCESS_KEY: secretAccessKey,
489
+ ...(sessionToken ? { AWS_SESSION_TOKEN: sessionToken } : {})
490
+ };
491
+ if (runAwsInit._provisionConfig) {
492
+ ensureAwsCli();
493
+ const cfg = runAwsInit._provisionConfig;
494
+ console.log((0, kleur_1.gray)('\nProvisioning AWS runner EC2 instance...'));
495
+ console.log((0, kleur_1.gray)('─'.repeat(40)));
496
+ let amiId = resolveAmazonLinuxAmi(awsRegion, awsEnv);
497
+ if (!amiId) {
498
+ const { manualAmi } = await (0, prompts_1.default)({
499
+ type: 'text',
500
+ name: 'manualAmi',
501
+ message: 'AMI ID (Amazon Linux 2023)',
502
+ initial: ''
503
+ });
504
+ amiId = manualAmi;
505
+ }
506
+ if (!amiId) {
507
+ console.log((0, kleur_1.red)('AMI ID required'));
508
+ process.exit(1);
509
+ }
510
+ let newInstanceId = '';
511
+ try {
512
+ newInstanceId = provisionAwsRunnerInstance(awsEnv, {
513
+ region: awsRegion,
514
+ amiId,
515
+ machineType,
516
+ runnerName: cfg.runnerName,
517
+ runnerTag,
518
+ runnerToken: cfg.runnerToken,
519
+ periodSeconds,
520
+ subnetId: cfg.subnetId || undefined,
521
+ securityGroupId: cfg.securityGroupId || undefined,
522
+ keyName: cfg.keyName || undefined,
523
+ iamInstanceProfile: cfg.iamInstanceProfile || undefined
524
+ });
525
+ }
526
+ catch (err) {
527
+ console.log((0, kleur_1.red)('Failed to create EC2 instance: ' + (err?.message || err)));
528
+ process.exit(1);
529
+ }
530
+ if (!newInstanceId) {
531
+ console.log((0, kleur_1.red)('Could not determine instance ID from run-instances output.'));
532
+ process.exit(1);
533
+ }
534
+ instanceId = newInstanceId;
535
+ console.log((0, kleur_1.green)(`Provisioned instance ${instanceId}. Runner will register shortly.`));
536
+ }
537
+ if (pendingInstanceSelection && !instanceId) {
538
+ ensureAwsCli();
539
+ const instances = listAwsInstances(awsRegion, awsEnv);
540
+ let chosenInstance = '_manual_';
541
+ if (instances.length > 0) {
542
+ const choices = [
543
+ ...instances.map(inst => ({
544
+ title: `${inst.instanceId}${inst.type ? ` (${inst.type})` : ''}${inst.name ? ` - ${inst.name}` : ''}${inst.state ? ` [${inst.state}]` : ''}`,
545
+ value: inst
546
+ })),
547
+ { title: 'Enter manually', value: '_manual_' }
548
+ ];
549
+ const { selectedInstance } = await (0, prompts_1.default)({
550
+ type: 'select',
551
+ name: 'selectedInstance',
552
+ message: 'Select EC2 runner instance',
553
+ choices
554
+ });
555
+ chosenInstance = selectedInstance;
556
+ }
557
+ else {
558
+ console.log((0, kleur_1.gray)('No EC2 instances found via AWS CLI. Please enter details manually.'));
559
+ }
560
+ if (chosenInstance === '_manual_') {
561
+ const { manualInstanceId } = await (0, prompts_1.default)({
562
+ type: 'text',
563
+ name: 'manualInstanceId',
564
+ message: 'EC2 instance ID (i-xxxxxxxx)',
565
+ initial: process.env.AWS_INSTANCE_ID || ''
566
+ });
567
+ instanceId = manualInstanceId;
568
+ machineType = await promptMachineType('Instance type', machineType || 'm5.large');
569
+ }
570
+ else {
571
+ instanceId = chosenInstance.instanceId;
572
+ machineType = chosenInstance.type || '';
573
+ if (!machineType) {
574
+ machineType = await promptMachineType('Instance type', 'm5.large');
575
+ }
576
+ else {
577
+ console.log((0, kleur_1.gray)(`Detected instance type: ${machineType}`));
578
+ }
579
+ }
580
+ if (!instanceId) {
581
+ console.log((0, kleur_1.red)('Instance ID required'));
582
+ process.exit(1);
583
+ }
584
+ }
585
+ console.log((0, kleur_1.gray)('\nStep 6: Electricity Maps API'));
586
+ console.log((0, kleur_1.gray)('─'.repeat(40)));
587
+ console.log((0, kleur_1.gray)('Get free key: https://api-portal.electricitymaps.com'));
588
+ const { electricityMapsKey } = await (0, prompts_1.default)({
589
+ type: 'password',
590
+ name: 'electricityMapsKey',
591
+ message: 'Electricity Maps API Key'
592
+ });
593
+ if (!electricityMapsKey) {
594
+ console.log((0, kleur_1.red)('API key required'));
595
+ process.exit(1);
596
+ }
597
+ console.log((0, kleur_1.gray)('\nStep 7: Optional Settings'));
598
+ console.log((0, kleur_1.gray)('─'.repeat(40)));
599
+ console.log((0, kleur_1.gray)('Set a carbon budget to track emissions against a limit.'));
600
+ console.log((0, kleur_1.gray)('Example: 10 grams CO2e per job. Leave empty to skip.\n'));
601
+ const { carbonBudget } = await (0, prompts_1.default)({
602
+ type: 'number',
603
+ name: 'carbonBudget',
604
+ message: 'Carbon budget (grams CO2e)',
605
+ initial: undefined
606
+ });
607
+ let failOnBudget = false;
608
+ if (carbonBudget) {
609
+ const { shouldFail } = await (0, prompts_1.default)({
610
+ type: 'confirm',
611
+ name: 'shouldFail',
612
+ message: 'Fail CI job if over budget?',
613
+ initial: false
614
+ });
615
+ failOnBudget = shouldFail;
616
+ }
617
+ console.log((0, kleur_1.gray)('\nStep 8: Setting CI/CD Variables'));
618
+ console.log((0, kleur_1.gray)('─'.repeat(40)));
619
+ const variables = [
620
+ { key: 'AWS_ACCESS_KEY_ID', value: accessKeyId, masked: true },
621
+ { key: 'AWS_SECRET_ACCESS_KEY', value: secretAccessKey, masked: true },
622
+ { key: 'AWS_REGION', value: awsRegion, masked: false },
623
+ { key: 'AWS_INSTANCE_ID', value: instanceId, masked: false },
624
+ { key: 'MACHINE_TYPE', value: machineType, masked: false },
625
+ { key: 'AWS_PERIOD_SECONDS', value: String(periodSeconds || 60), masked: false },
626
+ { key: 'ELECTRICITY_MAPS_API_KEY', value: electricityMapsKey, masked: true }
627
+ ];
628
+ if (sessionToken) {
629
+ variables.push({ key: 'AWS_SESSION_TOKEN', value: sessionToken, masked: true });
630
+ }
631
+ if (carbonBudget) {
632
+ variables.push({ key: 'CARBON_BUDGET_GRAMS', value: String(carbonBudget), masked: false });
633
+ }
634
+ if (failOnBudget) {
635
+ variables.push({ key: 'FAIL_ON_BUDGET', value: 'true', masked: false });
636
+ }
637
+ for (const v of variables) {
638
+ const ok = await setVariable(auth, projectPath, v.key, v.value, v.masked);
639
+ if (ok) {
640
+ console.log((0, kleur_1.green)(' Set ' + v.key));
641
+ }
642
+ else {
643
+ console.log((0, kleur_1.red)(' Failed: ' + v.key));
644
+ }
645
+ }
646
+ console.log((0, kleur_1.gray)('\nStep 9: CI Configuration'));
647
+ console.log((0, kleur_1.gray)('─'.repeat(40)));
648
+ let runnerTagForCi = runnerTag;
649
+ if (!runnerTagForCi) {
650
+ const { runnerTag: promptRunnerTag } = await (0, prompts_1.default)({
651
+ type: 'text',
652
+ name: 'runnerTag',
653
+ message: 'Runner tag (leave empty for any runner)',
654
+ initial: 'aws'
655
+ });
656
+ runnerTagForCi = promptRunnerTag || '';
657
+ }
658
+ else {
659
+ console.log((0, kleur_1.gray)(`Using runner tag: ${runnerTagForCi}`));
660
+ }
661
+ const ciJob = generateCiJob({
662
+ provider: 'aws',
663
+ machineType,
664
+ runnerTag: runnerTagForCi || undefined,
665
+ carbonBudget: carbonBudget || undefined,
666
+ failOnBudget,
667
+ aws: { region: awsRegion, instanceId, periodSeconds: periodSeconds || 60 }
668
+ });
669
+ await applyCiJobSnippet(ciJob);
670
+ console.log((0, kleur_1.bold)('\nDone'));
671
+ console.log('Commit and push to trigger the pipeline.\n');
672
+ };
106
673
  const runInit = async (opts = {}) => {
107
674
  console.log((0, kleur_1.bold)('\nGitGreen Setup\n'));
108
675
  // Step 1: GitLab Authentication
@@ -167,11 +734,12 @@ const runInit = async (opts = {}) => {
167
734
  ]
168
735
  });
169
736
  if (provider === 'aws') {
170
- console.log((0, kleur_1.red)('\nAWS support coming soon.'));
171
- process.exit(1);
737
+ await runAwsInit(auth, projectPath);
738
+ return;
172
739
  }
173
740
  if (provider === 'manual') {
174
741
  console.log((0, kleur_1.red)('\nManual configuration coming soon.'));
742
+ console.log((0, kleur_1.gray)('Docs: https://gitlab.com/youneslaaroussi/gitgreen (README covers manual metrics/power profile ingestion)'));
175
743
  process.exit(1);
176
744
  }
177
745
  // Check if gcloud is available
@@ -236,14 +804,9 @@ const runInit = async (opts = {}) => {
236
804
  message: 'GitLab Runner',
237
805
  choices: [
238
806
  { title: 'Use existing runner', value: 'existing' },
239
- { title: 'Provision new GCP runner VM', value: 'provision-gcp' },
240
- { title: 'Provision new AWS runner (coming soon)', value: 'provision-aws' }
807
+ { title: 'Provision new GCP runner VM', value: 'provision-gcp' }
241
808
  ]
242
809
  });
243
- if (runnerSetup === 'provision-aws') {
244
- console.log((0, kleur_1.red)('\nAWS runner provisioning coming soon.'));
245
- process.exit(1);
246
- }
247
810
  let gcpZone = '';
248
811
  let gcpInstanceId = '';
249
812
  let machineType = '';
@@ -688,36 +1251,15 @@ systemctl start gitlab-runner
688
1251
  else {
689
1252
  console.log((0, kleur_1.gray)(`Using runner tag: ${runnerTag}`));
690
1253
  }
691
- const { addCiJob } = await (0, prompts_1.default)({
692
- type: 'confirm',
693
- name: 'addCiJob',
694
- message: 'Add job to .gitlab-ci.yml?',
695
- initial: true
696
- });
697
1254
  const ciJob = generateCiJob({
1255
+ provider: 'gcp',
1256
+ machineType,
698
1257
  runnerTag: runnerTag || undefined,
699
1258
  carbonBudget: carbonBudget || undefined,
700
- failOnBudget
1259
+ failOnBudget,
1260
+ gcp: { projectId: gcpProjectId, zone: gcpZone, instanceId: gcpInstanceId }
701
1261
  });
702
- if (addCiJob) {
703
- const ciPath = path_1.default.join(process.cwd(), '.gitlab-ci.yml');
704
- if (fs_1.default.existsSync(ciPath)) {
705
- // Backup existing file before modifying
706
- const backupPath = `/tmp/gitlab-ci-backup-${Date.now()}.yml`;
707
- const existing = fs_1.default.readFileSync(ciPath, 'utf8');
708
- fs_1.default.writeFileSync(backupPath, existing);
709
- console.log((0, kleur_1.gray)(`Backup saved to: ${backupPath}`));
710
- fs_1.default.writeFileSync(ciPath, existing + '\n' + ciJob);
711
- }
712
- else {
713
- fs_1.default.writeFileSync(ciPath, 'stages:\n - test\n\n' + ciJob);
714
- }
715
- console.log((0, kleur_1.green)('Updated .gitlab-ci.yml'));
716
- }
717
- else {
718
- console.log('\nCI job snippet:\n');
719
- console.log(ciJob);
720
- }
1262
+ await applyCiJobSnippet(ciJob);
721
1263
  console.log((0, kleur_1.bold)('\nDone'));
722
1264
  console.log('Commit and push to trigger the pipeline.\n');
723
1265
  };