@jaguilar87/gaia-ops 1.3.4 → 1.3.6

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/CHANGELOG.md CHANGED
@@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.3.6] - 2025-11-10
11
+
12
+ ### Fixed
13
+ - **Installer:** Skip questions when project context already has the answers
14
+ - **Smart Detection:** Only ask what's missing or needs confirmation (paths)
15
+ - **User Experience:** Show config summary when context is loaded
16
+ - **Directory Creation:** Auto-create missing directories without prompting
17
+
18
+ ### Changed
19
+ - When project context loads successfully, only asks to confirm/adjust paths
20
+ - Cloud provider, credentials, region, and cluster name auto-applied from context
21
+ - Clearer feedback showing what was loaded from project context
22
+ - Missing directories (gitops, terraform, app-services) now created automatically
23
+
24
+ ### Improved
25
+ - Eliminates ALL redundant questions when context exists
26
+ - Better UX: "Here's what we loaded, just confirm the paths"
27
+ - Faster setup for teams with complete project contexts
28
+ - No interruptions for directory creation confirmations
29
+
30
+ ---
31
+
32
+ ## [1.3.5] - 2025-11-10
33
+
34
+ ### Added
35
+ - **Smart Installer Flow:** Project context repo now asked FIRST, with auto-population of all config
36
+ - **Input Sanitization:** Handles "git clone <url>" pastes automatically (extracts just URL)
37
+ - **Auto-Configuration:** Parses project-context.json and pre-fills all wizard questions
38
+ - **Better Error Messages:** Clear troubleshooting tips for git clone failures (SSH keys, access, URL)
39
+
40
+ ### Changed
41
+ - **Wizard Question Order:** Project context moved from last to first question
42
+ - **User Experience:** Reduced manual input when project context exists
43
+ - **Clone Strategy:** Validates project context early, then sets up in final location
44
+ - **Error Handling:** Installation continues even if project context clone fails
45
+
46
+ ### Improved
47
+ - Eliminates typos and configuration errors by pre-filling from existing context
48
+ - Saves time for users with existing project-context repos
49
+ - Better guidance when git operations fail
50
+
51
+ ---
52
+
10
53
  ## [1.3.4] - 2025-11-10
11
54
 
12
55
  ### Fixed
package/bin/gaia-init.js CHANGED
@@ -254,6 +254,81 @@ function validateConfiguration(config, nonInteractive) {
254
254
  // Interactive prompts
255
255
  // ============================================================================
256
256
 
257
+ /**
258
+ * Sanitize git repo URL input
259
+ * Handles cases where user pastes "git clone <url>" instead of just "<url>"
260
+ */
261
+ function sanitizeGitUrl(input) {
262
+ if (!input || typeof input !== 'string') return '';
263
+
264
+ // Trim whitespace
265
+ let sanitized = input.trim();
266
+
267
+ // Remove "git clone" prefix if present
268
+ if (sanitized.toLowerCase().startsWith('git clone ')) {
269
+ sanitized = sanitized.substring('git clone '.length).trim();
270
+ }
271
+
272
+ // Remove quotes if present
273
+ sanitized = sanitized.replace(/^["']|["']$/g, '');
274
+
275
+ return sanitized;
276
+ }
277
+
278
+ /**
279
+ * Try to clone project context repo early and parse config
280
+ * Returns parsed config or null if clone fails
281
+ */
282
+ async function tryCloneProjectContext(repoUrl) {
283
+ if (!repoUrl || repoUrl.trim() === '') {
284
+ return null;
285
+ }
286
+
287
+ const sanitizedUrl = sanitizeGitUrl(repoUrl);
288
+
289
+ const spinner = ora('Cloning project context repository...').start();
290
+
291
+ try {
292
+ const tempDir = join(CWD, '.claude-temp-context');
293
+
294
+ // Remove temp dir if exists
295
+ if (existsSync(tempDir)) {
296
+ await fs.rm(tempDir, { recursive: true, force: true });
297
+ }
298
+
299
+ // Clone to temp directory
300
+ await execAsync(`git clone ${sanitizedUrl} ${tempDir}`, { timeout: 30000 });
301
+
302
+ // Try to read project-context.json
303
+ const contextPath = join(tempDir, 'project-context.json');
304
+
305
+ if (existsSync(contextPath)) {
306
+ const contextData = JSON.parse(await fs.readFile(contextPath, 'utf-8'));
307
+
308
+ // Clean up temp dir
309
+ await fs.rm(tempDir, { recursive: true, force: true });
310
+
311
+ spinner.succeed('Project context loaded successfully');
312
+ console.log(chalk.green(' ✓ Auto-populated configuration from project context\n'));
313
+
314
+ return {
315
+ contextData,
316
+ repoUrl: sanitizedUrl
317
+ };
318
+ } else {
319
+ // No project-context.json found
320
+ await fs.rm(tempDir, { recursive: true, force: true });
321
+ spinner.warn('Repository cloned but no project-context.json found');
322
+ return null;
323
+ }
324
+ } catch (error) {
325
+ spinner.fail('Failed to clone project context repository');
326
+ console.log(chalk.yellow(`\n⚠️ Error: ${error.message}`));
327
+ console.log(chalk.gray(' Continuing with manual configuration...\n'));
328
+ return null;
329
+ }
330
+ }
331
+
257
332
  /**
258
333
  * Present interactive wizard to user
259
334
  */
@@ -276,36 +351,97 @@ async function runInteractiveWizard(detected) {
276
351
 
277
352
  console.log(chalk.gray('This wizard will set up the Gaia-Ops agent system for your project.\n'));
278
353
 
279
- console.log(chalk.yellow('📍 Directory Configuration'));
280
- console.log(chalk.gray('Gaia-Ops agents need to know where your code lives:\n'));
281
- console.log(chalk.gray(' • GitOps: Kubernetes manifests that agents will monitor and deploy'));
282
- console.log(chalk.gray(' Terraform: Infrastructure code that agents will plan and apply'));
283
- console.log(chalk.gray(' App Services: Application code that agents will build and test\n'));
354
+ // =========================================================================
355
+ // STEP 1: Ask for project context repo first (if available)
356
+ // =========================================================================
357
+ console.log(chalk.yellow('🔗 Project Context (Optional but Recommended)'));
358
+ console.log(chalk.gray('If you have a project context repo, we can auto-populate your configuration.\n'));
359
+
360
+ const contextQuestion = await prompts({
361
+ type: 'text',
362
+ name: 'projectContextRepo',
363
+ message: '📦 Project context Git repo (e.g., git@bitbucket.org:org/context.git):',
364
+ initial: ''
365
+ }, {
366
+ onCancel: () => {
367
+ console.log(chalk.yellow('\n⚠️ Installation cancelled by user\n'));
368
+ process.exit(0);
369
+ }
370
+ });
371
+
372
+ // Try to clone and parse project context
373
+ let projectContext = null;
374
+ if (contextQuestion.projectContextRepo) {
375
+ projectContext = await tryCloneProjectContext(contextQuestion.projectContextRepo);
376
+ }
377
+
378
+ // Extract defaults from project context if available
379
+ const defaults = projectContext ? {
380
+ gitops: projectContext.contextData.paths?.gitops || detected.gitops || './gitops',
381
+ terraform: projectContext.contextData.paths?.terraform || detected.terraform || './terraform',
382
+ appServices: projectContext.contextData.paths?.app_services || detected.appServices || './app-services',
383
+ cloudProvider: projectContext.contextData.sections?.project_details?.cloud_provider || 'gcp',
384
+ gcpProjectId: projectContext.contextData.sections?.project_details?.project_id || projectContext.contextData.metadata?.project_id || '',
385
+ awsAccountId: projectContext.contextData.sections?.project_details?.aws_account || projectContext.contextData.metadata?.aws_account || '',
386
+ region: projectContext.contextData.sections?.project_details?.region || projectContext.contextData.metadata?.primary_region || 'us-central1',
387
+ clusterName: projectContext.contextData.sections?.project_details?.cluster_name || '',
388
+ repoUrl: projectContext?.repoUrl || contextQuestion.projectContextRepo
389
+ } : {
390
+ gitops: detected.gitops || './gitops',
391
+ terraform: detected.terraform || './terraform',
392
+ appServices: detected.appServices || './app-services',
393
+ cloudProvider: 'gcp',
394
+ gcpProjectId: '',
395
+ awsAccountId: '',
396
+ region: 'us-central1',
397
+ clusterName: '',
398
+ repoUrl: contextQuestion.projectContextRepo
399
+ };
400
+
401
+ // =========================================================================
402
+ // STEP 2: Ask remaining questions (with smart defaults from context)
403
+ // =========================================================================
404
+
405
+ // If we have project context, show a summary and only ask to confirm paths
406
+ if (projectContext) {
407
+ console.log(chalk.green('\n✅ Configuration loaded from project context:'));
408
+ console.log(chalk.gray(` • Cloud: ${defaults.cloudProvider.toUpperCase()}`));
409
+ if (defaults.gcpProjectId) console.log(chalk.gray(` • GCP Project: ${defaults.gcpProjectId}`));
410
+ if (defaults.awsAccountId) console.log(chalk.gray(` • AWS Account: ${defaults.awsAccountId}`));
411
+ console.log(chalk.gray(` • Region: ${defaults.region}`));
412
+ console.log(chalk.gray(` • Cluster: ${defaults.clusterName}`));
413
+ console.log(chalk.yellow('\n📍 Directory Configuration'));
414
+ console.log(chalk.gray('Verify or adjust paths if needed:\n'));
415
+ } else {
416
+ console.log(chalk.yellow('\n📍 Directory Configuration'));
417
+ console.log(chalk.gray('Please provide your project configuration:\n'));
418
+ }
284
419
 
285
420
  const questions = [
286
421
  {
287
422
  type: 'text',
288
423
  name: 'gitops',
289
424
  message: '📦 GitOps directory:',
290
- initial: detected.gitops || './gitops',
425
+ initial: defaults.gitops,
291
426
  validate: value => value.trim().length > 0
292
427
  },
293
428
  {
294
429
  type: 'text',
295
430
  name: 'terraform',
296
431
  message: '🔧 Terraform directory:',
297
- initial: detected.terraform || './terraform',
432
+ initial: defaults.terraform,
298
433
  validate: value => value.trim().length > 0
299
434
  },
300
435
  {
301
436
  type: 'text',
302
437
  name: 'appServices',
303
438
  message: '🚀 App Services directory:',
304
- initial: detected.appServices || './app-services',
439
+ initial: defaults.appServices,
305
440
  validate: value => value.trim().length > 0
306
441
  },
442
+ // Only ask cloud provider if not loaded from context
307
443
  {
308
- type: 'select',
444
+ type: projectContext ? null : 'select',
309
445
  name: 'cloudProvider',
310
446
  message: '☁️ Cloud provider:',
311
447
  choices: [
@@ -313,31 +449,48 @@ async function runInteractiveWizard(detected) {
313
449
  { title: 'AWS (Amazon Web Services)', value: 'aws' },
314
450
  { title: 'Multi-cloud (AWS + GCP)', value: 'multi-cloud' }
315
451
  ],
316
- initial: 0
452
+ initial: defaults.cloudProvider === 'gcp' ? 0 : defaults.cloudProvider === 'aws' ? 1 : 2
317
453
  },
454
+ // Only ask GCP Project ID if not loaded from context
318
455
  {
319
- type: (prev, values) => ['gcp', 'multi-cloud'].includes(values.cloudProvider) ? 'text' : null,
456
+ type: (prev, values) => {
457
+ const provider = values.cloudProvider || defaults.cloudProvider;
458
+ const needsGcp = ['gcp', 'multi-cloud'].includes(provider);
459
+ const hasValue = projectContext && defaults.gcpProjectId;
460
+ return (needsGcp && !hasValue) ? 'text' : null;
461
+ },
320
462
  name: 'gcpProjectId',
321
463
  message: '🌐 GCP Project ID (e.g., aaxis-rnd-non-prod):',
464
+ initial: defaults.gcpProjectId,
322
465
  validate: value => value.trim().length > 0
323
466
  },
467
+ // Only ask AWS Account if not loaded from context
324
468
  {
325
- type: (prev, values) => ['aws', 'multi-cloud'].includes(values.cloudProvider) ? 'text' : null,
469
+ type: (prev, values) => {
470
+ const provider = values.cloudProvider || defaults.cloudProvider;
471
+ const needsAws = ['aws', 'multi-cloud'].includes(provider);
472
+ const hasValue = projectContext && defaults.awsAccountId;
473
+ return (needsAws && !hasValue) ? 'text' : null;
474
+ },
326
475
  name: 'awsAccountId',
327
476
  message: '🌐 AWS Account ID (e.g., 929914624686):',
477
+ initial: defaults.awsAccountId,
328
478
  validate: value => value.trim().length > 0
329
479
  },
480
+ // Only ask region if not loaded from context
330
481
  {
331
- type: 'text',
482
+ type: projectContext && defaults.region ? null : 'text',
332
483
  name: 'region',
333
484
  message: '🌍 Primary Region (e.g., us-central1 for GCP, us-east-1 for AWS):',
334
- initial: 'us-central1',
485
+ initial: defaults.region,
335
486
  validate: value => value.trim().length > 0
336
487
  },
488
+ // Only ask cluster name if not loaded from context
337
489
  {
338
- type: 'text',
490
+ type: projectContext && defaults.clusterName ? null : 'text',
339
491
  name: 'clusterName',
340
492
  message: '☸️ Cluster Name (e.g., rnd-gke-nonprod or digital-eks-prod):',
493
+ initial: defaults.clusterName,
341
494
  validate: value => value.trim().length > 0
342
495
  },
343
496
  {
@@ -345,12 +498,6 @@ async function runInteractiveWizard(detected) {
345
498
  name: 'installClaudeCode',
346
499
  message: '📥 Install Claude Code if not present?',
347
500
  initial: true
348
- },
349
- {
350
- type: 'text',
351
- name: 'projectContextRepo',
352
- message: '🔗 Project context Git repo (optional, e.g., git@bitbucket.org:org/context.git):',
353
- initial: ''
354
501
  }
355
502
  ];
356
503
 
@@ -361,13 +508,21 @@ async function runInteractiveWizard(detected) {
361
508
  }
362
509
  });
363
510
 
364
- // Verify critical responses are present (some questions are conditional)
365
- if (!responses.cloudProvider || !responses.clusterName) {
511
+ // Merge responses with defaults (for values skipped because they were in context)
512
+ const finalConfig = {
513
+ ...defaults,
514
+ ...responses,
515
+ projectContextRepo: defaults.repoUrl,
516
+ projectContextAlreadyCloned: !!projectContext
517
+ };
518
+
519
+ // Verify critical responses are present
520
+ if (!finalConfig.cloudProvider || !finalConfig.clusterName) {
366
521
  console.log(chalk.yellow('\n⚠️ Installation cancelled or incomplete\n'));
367
522
  process.exit(0);
368
523
  }
369
524
 
370
- return responses;
525
+ return finalConfig;
371
526
  }
372
527
 
373
528
  // ============================================================================
@@ -507,9 +662,9 @@ async function generateAgentsMd() {
507
662
 
508
663
  /**
509
664
  * Validate and setup project paths (gitops, terraform, app-services)
510
- * Creates directories if they don't exist (with user confirmation in interactive mode)
665
+ * Creates directories automatically if they don't exist
511
666
  */
512
- async function validateAndSetupProjectPaths(config, nonInteractive) {
667
+ async function validateAndSetupProjectPaths(config) {
513
668
  console.log(chalk.cyan('\n📁 Setting up project directories...\n'));
514
669
 
515
670
  const paths = {
@@ -527,30 +682,13 @@ async function validateAndSetupProjectPaths(config, nonInteractive) {
527
682
  continue;
528
683
  }
529
684
 
530
- // Path doesn't exist - decide what to do
531
- let shouldCreate = nonInteractive; // Auto-create in non-interactive mode
532
-
533
- if (!nonInteractive) {
534
- // Ask user in interactive mode
535
- const response = await prompts({
536
- type: 'confirm',
537
- name: 'create',
538
- message: `Directory ${chalk.yellow(userPath)} doesn't exist. Create it?`,
539
- initial: true
540
- });
541
- shouldCreate = response.create;
542
- }
543
-
544
- if (shouldCreate) {
545
- await fs.mkdir(absPath, { recursive: true });
546
- console.log(chalk.green(` ✓ ${name}: ${userPath} (created)`));
547
- } else {
548
- console.log(chalk.yellow(` ⚠ ${name}: ${userPath} (skipped - agents may create it later if needed)`));
549
- }
685
+ // Create directory automatically
686
+ await fs.mkdir(absPath, { recursive: true });
687
+ console.log(chalk.green(` ✓ ${name}: ${userPath} (created)`));
550
688
 
551
689
  // Warn about absolute paths (portability concern)
552
690
  if (isAbsolute(userPath)) {
553
- console.log(chalk.yellow(` Warning: Absolute path "${userPath}" may not work on other machines`));
691
+ console.log(chalk.yellow(` Note: Absolute path may not work on other machines`));
554
692
  }
555
693
  }
556
694
 
@@ -698,13 +836,58 @@ async function installClaudeAgentsPackage() {
698
836
 
699
837
  /**
700
838
  * Clone project context repository (optional)
839
+ * If already cloned during wizard, skip clone and just set it up in final location
701
840
  */
702
- async function cloneProjectContextRepo(repoUrl) {
841
+ async function cloneProjectContextRepo(repoUrl, alreadyCloned = false) {
703
842
  if (!repoUrl || repoUrl.trim() === '') {
704
843
  console.log(chalk.gray('\n✓ Skipping project context repo clone (not provided)\n'));
705
844
  return;
706
845
  }
707
846
 
847
+ const sanitizedUrl = sanitizeGitUrl(repoUrl);
848
+
849
+ if (alreadyCloned) {
850
+ // Context was already cloned during wizard, just re-clone to final location
851
+ const spinner = ora('Setting up project context...').start();
852
+
853
+ try {
854
+ const projectContextDir = join(CWD, '.claude', 'project-context');
855
+
856
+ // Remove the generated project-context.json as it will be replaced by the cloned repo
857
+ const generatedFile = join(projectContextDir, 'project-context.json');
858
+ if (existsSync(generatedFile)) {
859
+ await fs.unlink(generatedFile);
860
+ }
861
+
862
+ // Clone fresh to final location (we already validated it works)
863
+ const tempDir = `${projectContextDir}-temp`;
864
+ await execAsync(`git clone ${sanitizedUrl} ${tempDir}`, { timeout: 30000 });
865
+
866
+ // Move contents from temp to project-context
867
+ const files = await fs.readdir(tempDir);
868
+ for (const file of files) {
869
+ const src = join(tempDir, file);
870
+ const dest = join(projectContextDir, file);
871
+ await fs.rename(src, dest);
872
+ }
873
+
874
+ // Remove temp directory
875
+ await fs.rm(tempDir, { recursive: true, force: true });
876
+
877
+ spinner.succeed('Project context repository configured');
878
+ console.log(chalk.green(` → Location: .claude/project-context/\n`));
879
+ } catch (error) {
880
+ spinner.fail('Failed to setup project context repository');
881
+ console.log(chalk.yellow(`\n⚠️ You can clone it manually with:`));
882
+ console.log(chalk.gray(` cd .claude`));
883
+ console.log(chalk.gray(` rm -rf project-context`));
884
+ console.log(chalk.gray(` git clone ${sanitizedUrl} project-context\n`));
885
+ // Don't throw - allow installation to continue
886
+ }
887
+ return;
888
+ }
889
+
890
+ // Not cloned yet during wizard, try to clone now
708
891
  const spinner = ora('Cloning project context repository...').start();
709
892
 
710
893
  try {
@@ -716,11 +899,11 @@ async function cloneProjectContextRepo(repoUrl) {
716
899
  await fs.unlink(generatedFile);
717
900
  }
718
901
 
719
- // Clone repo directly into project-context directory
720
- await execAsync(`git clone ${repoUrl} ${projectContextDir}-temp`);
902
+ // Clone repo
903
+ const tempDir = `${projectContextDir}-temp`;
904
+ await execAsync(`git clone ${sanitizedUrl} ${tempDir}`, { timeout: 30000 });
721
905
 
722
906
  // Move contents from temp to project-context
723
- const tempDir = `${projectContextDir}-temp`;
724
907
  const files = await fs.readdir(tempDir);
725
908
  for (const file of files) {
726
909
  const src = join(tempDir, file);
@@ -729,17 +912,22 @@ async function cloneProjectContextRepo(repoUrl) {
729
912
  }
730
913
 
731
914
  // Remove temp directory
732
- await fs.rmdir(tempDir);
915
+ await fs.rm(tempDir, { recursive: true, force: true });
733
916
 
734
917
  spinner.succeed('Project context repository cloned');
735
- console.log(chalk.green(` → Cloned from: ${repoUrl}`));
918
+ console.log(chalk.green(` → Cloned from: ${sanitizedUrl}`));
736
919
  console.log(chalk.gray(` → Location: .claude/project-context/\n`));
737
920
  } catch (error) {
738
921
  spinner.fail('Failed to clone project context repository');
739
- console.log(chalk.yellow(`\n⚠️ You can clone it manually later with:`));
740
- console.log(chalk.gray(` cd .claude`));
741
- console.log(chalk.gray(` rm -rf project-context`));
742
- console.log(chalk.gray(` git clone ${repoUrl} project-context\n`));
922
+ console.log(chalk.yellow(`\n⚠️ Error: ${error.message}`));
923
+ console.log(chalk.gray('\n Common issues:'));
924
+ console.log(chalk.gray(' Check SSH keys are configured: ssh -T git@bitbucket.org'));
925
+ console.log(chalk.gray(' Verify repository URL is correct'));
926
+ console.log(chalk.gray(' • Ensure you have access to the repository\n'));
927
+ console.log(chalk.yellow(` You can clone it manually later with:`));
928
+ console.log(chalk.gray(` cd .claude`));
929
+ console.log(chalk.gray(` rm -rf project-context`));
930
+ console.log(chalk.gray(` git clone ${sanitizedUrl} project-context\n`));
743
931
  // Don't throw - allow installation to continue
744
932
  }
745
933
  }
@@ -798,7 +986,7 @@ async function main() {
798
986
  await installClaudeAgentsPackage();
799
987
 
800
988
  // Step 5.5: Validate and setup project paths (gitops, terraform, app-services)
801
- await validateAndSetupProjectPaths(config, args.nonInteractive);
989
+ await validateAndSetupProjectPaths(config);
802
990
 
803
991
  // Step 6: Create .claude/ directory with symlinks
804
992
  await createClaudeDirectory();
@@ -814,7 +1002,7 @@ async function main() {
814
1002
 
815
1003
  // Step 10: Clone project context repository (optional)
816
1004
  if (config.projectContextRepo) {
817
- await cloneProjectContextRepo(config.projectContextRepo);
1005
+ await cloneProjectContextRepo(config.projectContextRepo, config.projectContextAlreadyCloned);
818
1006
  }
819
1007
 
820
1008
  // Success message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaguilar87/gaia-ops",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "Multi-agent orchestration system for Claude Code - DevOps automation toolkit",
5
5
  "main": "index.js",
6
6
  "type": "module",