@paulduvall/claude-dev-toolkit 0.0.1-alpha.11 ā 0.0.1-alpha.12
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/lib/oidc-command.js +385 -8
- package/package.json +1 -1
package/lib/oidc-command.js
CHANGED
|
@@ -292,28 +292,81 @@ Run 'claude-commands oidc --help' for complete setup requirements.`;
|
|
|
292
292
|
const { dryRun = false } = options;
|
|
293
293
|
|
|
294
294
|
if (dryRun) {
|
|
295
|
-
this.
|
|
295
|
+
await this.showDryRunWithDetection(options);
|
|
296
296
|
return {
|
|
297
297
|
message: 'ā
Dry run completed successfully',
|
|
298
298
|
dryRun: true
|
|
299
299
|
};
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
//
|
|
302
|
+
// Phase 2: Auto-detect configuration
|
|
303
303
|
this.showProgress('š Initializing OIDC command...', options);
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
const configResult = await this.autoDetectConfiguration(options);
|
|
306
|
+
if (!configResult.success) {
|
|
307
|
+
console.log(`ā Configuration detection failed: ${configResult.error}`);
|
|
308
|
+
if (configResult.suggestions) {
|
|
309
|
+
configResult.suggestions.forEach(suggestion => console.log(suggestion));
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
message: 'ā OIDC setup failed during configuration detection',
|
|
313
|
+
error: configResult.error
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Display detected configuration
|
|
318
|
+
console.log('ā
Configuration detected successfully:');
|
|
319
|
+
console.log(` š Repository: ${configResult.git.owner}/${configResult.git.repo}`);
|
|
320
|
+
console.log(` š AWS Region: ${configResult.aws.region} (${configResult.aws.source})`);
|
|
321
|
+
console.log(` š IAM Role: ${configResult.roleName}`);
|
|
322
|
+
console.log('');
|
|
323
|
+
|
|
324
|
+
// For now, show that detection is working but full implementation is still in development
|
|
325
|
+
console.log('š OIDC Setup Status: Auto-detection implemented ā
');
|
|
326
|
+
console.log('ā ļø AWS resource creation is in development (Phase 3)');
|
|
327
|
+
console.log('š” Use --dry-run to preview complete functionality');
|
|
309
328
|
|
|
310
329
|
return {
|
|
311
|
-
message: 'ā
OIDC command executed successfully (
|
|
330
|
+
message: 'ā
OIDC command executed successfully (Phase 2: Detection completed)',
|
|
331
|
+
configuration: configResult
|
|
312
332
|
};
|
|
313
333
|
}
|
|
314
334
|
|
|
315
335
|
/**
|
|
316
|
-
* Show dry run preview
|
|
336
|
+
* Show dry run preview with Phase 2 detection
|
|
337
|
+
*/
|
|
338
|
+
async showDryRunWithDetection(options) {
|
|
339
|
+
console.log('š Dry Run - Preview of OIDC configuration actions:\n');
|
|
340
|
+
|
|
341
|
+
// Show what detection would find
|
|
342
|
+
console.log('š Phase 2: Auto-Detection Preview:');
|
|
343
|
+
try {
|
|
344
|
+
const configResult = await this.autoDetectConfiguration(options);
|
|
345
|
+
if (configResult.success) {
|
|
346
|
+
console.log(` ā
Repository: ${configResult.git.owner}/${configResult.git.repo}`);
|
|
347
|
+
console.log(` ā
AWS Region: ${configResult.aws.region} (${configResult.aws.source})`);
|
|
348
|
+
console.log(` ā
IAM Role: ${configResult.roleName}`);
|
|
349
|
+
console.log(` ā
Policy Template: ${configResult.policyTemplate.name}`);
|
|
350
|
+
} else {
|
|
351
|
+
console.log(` ā Configuration detection would fail: ${configResult.error}`);
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.log(` ā Detection error: ${error.message}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log('\nš Phase 3: AWS Resource Creation (Planned):');
|
|
358
|
+
console.log(' ⢠Create AWS OIDC Identity Provider for GitHub');
|
|
359
|
+
console.log(' ⢠Create IAM role with trust policy for GitHub Actions');
|
|
360
|
+
console.log(' ⢠Attach permission policies to IAM role');
|
|
361
|
+
console.log(' ⢠Set up GitHub repository variables (AWS_DEPLOYMENT_ROLE, AWS_REGION)');
|
|
362
|
+
console.log('\nš” This was a dry run - no changes were made');
|
|
363
|
+
console.log(' Run without --dry-run to execute OIDC setup (Phase 2 detection only)');
|
|
364
|
+
|
|
365
|
+
return { dryRun: true, message: 'Dry run completed' };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Show dry run preview (legacy method)
|
|
317
370
|
*/
|
|
318
371
|
showDryRun(options) {
|
|
319
372
|
console.log('š Dry Run - Preview of OIDC configuration actions:\n');
|
|
@@ -329,6 +382,330 @@ Run 'claude-commands oidc --help' for complete setup requirements.`;
|
|
|
329
382
|
return { dryRun: true, message: 'Dry run completed' };
|
|
330
383
|
}
|
|
331
384
|
|
|
385
|
+
/**
|
|
386
|
+
* REQ-DETECT-001: Git Repository Detection
|
|
387
|
+
* Auto-detect GitHub org/repo from git remote
|
|
388
|
+
*/
|
|
389
|
+
async detectGitRepository(options = {}) {
|
|
390
|
+
try {
|
|
391
|
+
const { execSync } = require('child_process');
|
|
392
|
+
|
|
393
|
+
// Get git remotes
|
|
394
|
+
const remotesOutput = execSync('git remote -v', {
|
|
395
|
+
encoding: 'utf8',
|
|
396
|
+
cwd: options.repositoryPath || process.cwd()
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const remotes = this.parseGitRemotes(remotesOutput);
|
|
400
|
+
const selectedRemote = this.selectPreferredRemote(remotes);
|
|
401
|
+
|
|
402
|
+
if (!selectedRemote) {
|
|
403
|
+
throw new Error('No GitHub remote found. Please add a GitHub remote origin.');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const repoInfo = this.parseGitRemote(selectedRemote.url);
|
|
407
|
+
return {
|
|
408
|
+
success: true,
|
|
409
|
+
owner: repoInfo.owner,
|
|
410
|
+
repo: repoInfo.repo,
|
|
411
|
+
remote: selectedRemote.name,
|
|
412
|
+
url: selectedRemote.url
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return {
|
|
417
|
+
success: false,
|
|
418
|
+
error: error.message,
|
|
419
|
+
suggestions: [
|
|
420
|
+
'š§ Ensure you are in a git repository',
|
|
421
|
+
'š Add a GitHub remote: git remote add origin <github-url>',
|
|
422
|
+
'ā
Verify remote: git remote -v'
|
|
423
|
+
]
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Parse git remotes output into structured format
|
|
430
|
+
*/
|
|
431
|
+
parseGitRemotes(remotesOutput) {
|
|
432
|
+
const remotes = [];
|
|
433
|
+
const lines = remotesOutput.split('\n').filter(line => line.trim());
|
|
434
|
+
|
|
435
|
+
lines.forEach(line => {
|
|
436
|
+
const match = line.match(/^(\w+)\s+(.+?)\s+\((fetch|push)\)$/);
|
|
437
|
+
if (match && match[3] === 'fetch') { // Only use fetch URLs
|
|
438
|
+
const [, name, url] = match;
|
|
439
|
+
if (url.includes('github.com')) {
|
|
440
|
+
remotes.push({ name, url });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return remotes;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Select preferred remote (prioritize 'origin')
|
|
450
|
+
*/
|
|
451
|
+
selectPreferredRemote(remotes) {
|
|
452
|
+
if (remotes.length === 0) return null;
|
|
453
|
+
|
|
454
|
+
// Prefer 'origin' remote
|
|
455
|
+
const origin = remotes.find(remote => remote.name === 'origin');
|
|
456
|
+
if (origin) return origin;
|
|
457
|
+
|
|
458
|
+
// Fall back to first GitHub remote
|
|
459
|
+
return remotes[0];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Parse git remote URL to extract owner/repo
|
|
464
|
+
* Supports both SSH and HTTPS formats
|
|
465
|
+
*/
|
|
466
|
+
parseGitRemote(url) {
|
|
467
|
+
// SSH format: git@github.com:owner/repo.git
|
|
468
|
+
const sshMatch = url.match(/^git@github\.com:([^\/]+)\/(.+?)(?:\.git)?$/);
|
|
469
|
+
if (sshMatch) {
|
|
470
|
+
return {
|
|
471
|
+
owner: sshMatch[1],
|
|
472
|
+
repo: sshMatch[2]
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// HTTPS format: https://github.com/owner/repo.git
|
|
477
|
+
const httpsMatch = url.match(/^https:\/\/github\.com\/([^\/]+)\/(.+?)(?:\.git)?$/);
|
|
478
|
+
if (httpsMatch) {
|
|
479
|
+
return {
|
|
480
|
+
owner: httpsMatch[1],
|
|
481
|
+
repo: httpsMatch[2]
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
throw new Error(`Unsupported git remote URL format: ${url}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* REQ-DETECT-002: AWS Configuration Detection
|
|
490
|
+
* Read AWS CLI config and environment variables
|
|
491
|
+
*/
|
|
492
|
+
async detectAWSConfiguration(options = {}) {
|
|
493
|
+
try {
|
|
494
|
+
const fs = require('fs');
|
|
495
|
+
const path = require('path');
|
|
496
|
+
const os = require('os');
|
|
497
|
+
|
|
498
|
+
let region = null;
|
|
499
|
+
let profile = 'default';
|
|
500
|
+
|
|
501
|
+
// 1. Check environment variable first (highest priority)
|
|
502
|
+
region = this.getAWSRegionFromEnvironment();
|
|
503
|
+
|
|
504
|
+
// 2. If no env var, try to read from AWS config files
|
|
505
|
+
if (!region) {
|
|
506
|
+
const awsConfigResult = this.readAWSConfigFiles();
|
|
507
|
+
region = awsConfigResult.region;
|
|
508
|
+
profile = awsConfigResult.profile;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 3. Default to us-east-1 if nothing found
|
|
512
|
+
if (!region) {
|
|
513
|
+
region = 'us-east-1';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 4. Validate region
|
|
517
|
+
const regionValid = this.validateAWSRegion(region);
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
success: true,
|
|
521
|
+
region: region,
|
|
522
|
+
profile: profile,
|
|
523
|
+
source: region === this.getAWSRegionFromEnvironment() ? 'environment' :
|
|
524
|
+
region === 'us-east-1' ? 'default' : 'config-file',
|
|
525
|
+
valid: regionValid
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
} catch (error) {
|
|
529
|
+
return {
|
|
530
|
+
success: false,
|
|
531
|
+
error: error.message,
|
|
532
|
+
region: 'us-east-1', // Fallback
|
|
533
|
+
suggestions: [
|
|
534
|
+
'š§ Set AWS region: export AWS_DEFAULT_REGION=us-east-1',
|
|
535
|
+
'āļø Configure AWS CLI: aws configure',
|
|
536
|
+
'ā
Verify config: aws configure list'
|
|
537
|
+
]
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Get AWS region from environment variables
|
|
544
|
+
*/
|
|
545
|
+
getAWSRegionFromEnvironment() {
|
|
546
|
+
return process.env.AWS_DEFAULT_REGION || process.env.AWS_REGION || null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Read AWS CLI config files (~/.aws/config and ~/.aws/credentials)
|
|
551
|
+
*/
|
|
552
|
+
readAWSConfigFiles() {
|
|
553
|
+
const fs = require('fs');
|
|
554
|
+
const path = require('path');
|
|
555
|
+
const os = require('os');
|
|
556
|
+
|
|
557
|
+
const awsDir = path.join(os.homedir(), '.aws');
|
|
558
|
+
const configPath = path.join(awsDir, 'config');
|
|
559
|
+
const credentialsPath = path.join(awsDir, 'credentials');
|
|
560
|
+
|
|
561
|
+
let region = null;
|
|
562
|
+
let profile = 'default';
|
|
563
|
+
|
|
564
|
+
// Try to read config file first
|
|
565
|
+
try {
|
|
566
|
+
if (fs.existsSync(configPath)) {
|
|
567
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
568
|
+
const regionMatch = configContent.match(/^\s*region\s*=\s*(.+)$/m);
|
|
569
|
+
if (regionMatch) {
|
|
570
|
+
region = regionMatch[1].trim();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
} catch (error) {
|
|
574
|
+
// Ignore config file read errors
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return { region, profile };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Validate AWS region format and existence
|
|
582
|
+
*/
|
|
583
|
+
validateAWSRegion(region) {
|
|
584
|
+
if (!region) return false;
|
|
585
|
+
|
|
586
|
+
// Basic format validation: region should be like us-east-1, eu-west-1, etc.
|
|
587
|
+
const regionPattern = /^[a-z]{2,3}-[a-z]+-\d+$/;
|
|
588
|
+
if (!regionPattern.test(region)) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// List of valid AWS regions (simplified list for validation)
|
|
593
|
+
const validRegions = [
|
|
594
|
+
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
|
|
595
|
+
'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-central-1',
|
|
596
|
+
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2',
|
|
597
|
+
'sa-east-1', 'ca-central-1', 'ap-south-1'
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
return validRegions.includes(region);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* REQ-CLI-003: Zero Configuration Mode
|
|
605
|
+
* Combine git detection + AWS detection for zero-config experience
|
|
606
|
+
*/
|
|
607
|
+
async autoDetectConfiguration(options = {}) {
|
|
608
|
+
try {
|
|
609
|
+
this.showProgress('š Auto-detecting project configuration...', options);
|
|
610
|
+
|
|
611
|
+
// Detect Git repository information
|
|
612
|
+
const gitResult = await this.detectGitRepository(options);
|
|
613
|
+
if (!gitResult.success) {
|
|
614
|
+
return {
|
|
615
|
+
success: false,
|
|
616
|
+
error: 'Git repository detection failed',
|
|
617
|
+
details: gitResult,
|
|
618
|
+
suggestions: gitResult.suggestions
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Detect AWS configuration
|
|
623
|
+
const awsResult = await this.detectAWSConfiguration(options);
|
|
624
|
+
if (!awsResult.success) {
|
|
625
|
+
return {
|
|
626
|
+
success: false,
|
|
627
|
+
error: 'AWS configuration detection failed',
|
|
628
|
+
details: awsResult,
|
|
629
|
+
suggestions: awsResult.suggestions
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Generate role name based on repository
|
|
634
|
+
const roleName = this.generateRoleName(gitResult.owner, gitResult.repo, options);
|
|
635
|
+
|
|
636
|
+
// Get default policy template
|
|
637
|
+
const policyTemplate = this.getDefaultPolicyTemplate();
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
success: true,
|
|
641
|
+
git: gitResult,
|
|
642
|
+
aws: awsResult,
|
|
643
|
+
roleName: roleName,
|
|
644
|
+
policyTemplate: policyTemplate,
|
|
645
|
+
zeroConfig: true
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
} catch (error) {
|
|
649
|
+
return {
|
|
650
|
+
success: false,
|
|
651
|
+
error: error.message,
|
|
652
|
+
suggestions: [
|
|
653
|
+
'š§ Ensure you are in a git repository with GitHub remote',
|
|
654
|
+
'āļø Configure AWS CLI or set AWS_DEFAULT_REGION',
|
|
655
|
+
'ā
Run with --dry-run to see what would be configured'
|
|
656
|
+
]
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Auto-generate role name based on repository
|
|
663
|
+
*/
|
|
664
|
+
generateRoleName(owner, repo, options = {}) {
|
|
665
|
+
// Use provided role name if specified
|
|
666
|
+
if (options.roleName && options.roleName !== 'GitHubActionsRole') {
|
|
667
|
+
return options.roleName;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Generate role name: GitHubActions-owner-repo
|
|
671
|
+
const safeName = `GitHubActions-${owner}-${repo}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
672
|
+
|
|
673
|
+
// Ensure it meets IAM role name requirements (max 64 chars, alphanumeric + hyphens)
|
|
674
|
+
if (safeName.length > 64) {
|
|
675
|
+
return `GitHubActions-${owner}`.substring(0, 64);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return safeName;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get default policy template for standard use cases
|
|
683
|
+
*/
|
|
684
|
+
getDefaultPolicyTemplate() {
|
|
685
|
+
return {
|
|
686
|
+
name: 'standard',
|
|
687
|
+
description: 'Standard permissions for common GitHub Actions workflows',
|
|
688
|
+
policies: [
|
|
689
|
+
{
|
|
690
|
+
name: 'GitHubActionsBasePolicy',
|
|
691
|
+
document: {
|
|
692
|
+
Version: '2012-10-17',
|
|
693
|
+
Statement: [
|
|
694
|
+
{
|
|
695
|
+
Effect: 'Allow',
|
|
696
|
+
Action: [
|
|
697
|
+
'sts:GetCallerIdentity',
|
|
698
|
+
'sts:TagSession'
|
|
699
|
+
],
|
|
700
|
+
Resource: '*'
|
|
701
|
+
}
|
|
702
|
+
]
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
]
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
332
709
|
/**
|
|
333
710
|
* Get help text for OIDC command
|
|
334
711
|
*/
|
package/package.json
CHANGED