@jaguilar87/gaia-ops 1.3.4 → 1.3.5

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,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.3.5] - 2025-11-10
11
+
12
+ ### Added
13
+ - **Smart Installer Flow:** Project context repo now asked FIRST, with auto-population of all config
14
+ - **Input Sanitization:** Handles "git clone <url>" pastes automatically (extracts just URL)
15
+ - **Auto-Configuration:** Parses project-context.json and pre-fills all wizard questions
16
+ - **Better Error Messages:** Clear troubleshooting tips for git clone failures (SSH keys, access, URL)
17
+
18
+ ### Changed
19
+ - **Wizard Question Order:** Project context moved from last to first question
20
+ - **User Experience:** Reduced manual input when project context exists
21
+ - **Clone Strategy:** Validates project context early, then sets up in final location
22
+ - **Error Handling:** Installation continues even if project context clone fails
23
+
24
+ ### Improved
25
+ - Eliminates typos and configuration errors by pre-filling from existing context
26
+ - Saves time for users with existing project-context repos
27
+ - Better guidance when git operations fail
28
+
29
+ ---
30
+
10
31
  ## [1.3.4] - 2025-11-10
11
32
 
12
33
  ### 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,32 +351,79 @@ 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
+ console.log(chalk.yellow('\n📍 Directory Configuration'));
405
+ console.log(chalk.gray('Verify or adjust the following paths:\n'));
284
406
 
285
407
  const questions = [
286
408
  {
287
409
  type: 'text',
288
410
  name: 'gitops',
289
411
  message: '📦 GitOps directory:',
290
- initial: detected.gitops || './gitops',
412
+ initial: defaults.gitops,
291
413
  validate: value => value.trim().length > 0
292
414
  },
293
415
  {
294
416
  type: 'text',
295
417
  name: 'terraform',
296
418
  message: '🔧 Terraform directory:',
297
- initial: detected.terraform || './terraform',
419
+ initial: defaults.terraform,
298
420
  validate: value => value.trim().length > 0
299
421
  },
300
422
  {
301
423
  type: 'text',
302
424
  name: 'appServices',
303
425
  message: '🚀 App Services directory:',
304
- initial: detected.appServices || './app-services',
426
+ initial: defaults.appServices,
305
427
  validate: value => value.trim().length > 0
306
428
  },
307
429
  {
@@ -313,31 +435,34 @@ async function runInteractiveWizard(detected) {
313
435
  { title: 'AWS (Amazon Web Services)', value: 'aws' },
314
436
  { title: 'Multi-cloud (AWS + GCP)', value: 'multi-cloud' }
315
437
  ],
316
- initial: 0
438
+ initial: defaults.cloudProvider === 'gcp' ? 0 : defaults.cloudProvider === 'aws' ? 1 : 2
317
439
  },
318
440
  {
319
441
  type: (prev, values) => ['gcp', 'multi-cloud'].includes(values.cloudProvider) ? 'text' : null,
320
442
  name: 'gcpProjectId',
321
443
  message: '🌐 GCP Project ID (e.g., aaxis-rnd-non-prod):',
444
+ initial: defaults.gcpProjectId,
322
445
  validate: value => value.trim().length > 0
323
446
  },
324
447
  {
325
448
  type: (prev, values) => ['aws', 'multi-cloud'].includes(values.cloudProvider) ? 'text' : null,
326
449
  name: 'awsAccountId',
327
450
  message: '🌐 AWS Account ID (e.g., 929914624686):',
451
+ initial: defaults.awsAccountId,
328
452
  validate: value => value.trim().length > 0
329
453
  },
330
454
  {
331
455
  type: 'text',
332
456
  name: 'region',
333
457
  message: '🌍 Primary Region (e.g., us-central1 for GCP, us-east-1 for AWS):',
334
- initial: 'us-central1',
458
+ initial: defaults.region,
335
459
  validate: value => value.trim().length > 0
336
460
  },
337
461
  {
338
462
  type: 'text',
339
463
  name: 'clusterName',
340
464
  message: '☸️ Cluster Name (e.g., rnd-gke-nonprod or digital-eks-prod):',
465
+ initial: defaults.clusterName,
341
466
  validate: value => value.trim().length > 0
342
467
  },
343
468
  {
@@ -345,12 +470,6 @@ async function runInteractiveWizard(detected) {
345
470
  name: 'installClaudeCode',
346
471
  message: '📥 Install Claude Code if not present?',
347
472
  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
473
  }
355
474
  ];
356
475
 
@@ -367,6 +486,10 @@ async function runInteractiveWizard(detected) {
367
486
  process.exit(0);
368
487
  }
369
488
 
489
+ // Add project context info to responses
490
+ responses.projectContextRepo = defaults.repoUrl;
491
+ responses.projectContextAlreadyCloned = !!projectContext;
492
+
370
493
  return responses;
371
494
  }
372
495
 
@@ -698,13 +821,58 @@ async function installClaudeAgentsPackage() {
698
821
 
699
822
  /**
700
823
  * Clone project context repository (optional)
824
+ * If already cloned during wizard, skip clone and just set it up in final location
701
825
  */
702
- async function cloneProjectContextRepo(repoUrl) {
826
+ async function cloneProjectContextRepo(repoUrl, alreadyCloned = false) {
703
827
  if (!repoUrl || repoUrl.trim() === '') {
704
828
  console.log(chalk.gray('\n✓ Skipping project context repo clone (not provided)\n'));
705
829
  return;
706
830
  }
707
831
 
832
+ const sanitizedUrl = sanitizeGitUrl(repoUrl);
833
+
834
+ if (alreadyCloned) {
835
+ // Context was already cloned during wizard, just re-clone to final location
836
+ const spinner = ora('Setting up project context...').start();
837
+
838
+ try {
839
+ const projectContextDir = join(CWD, '.claude', 'project-context');
840
+
841
+ // Remove the generated project-context.json as it will be replaced by the cloned repo
842
+ const generatedFile = join(projectContextDir, 'project-context.json');
843
+ if (existsSync(generatedFile)) {
844
+ await fs.unlink(generatedFile);
845
+ }
846
+
847
+ // Clone fresh to final location (we already validated it works)
848
+ const tempDir = `${projectContextDir}-temp`;
849
+ await execAsync(`git clone ${sanitizedUrl} ${tempDir}`, { timeout: 30000 });
850
+
851
+ // Move contents from temp to project-context
852
+ const files = await fs.readdir(tempDir);
853
+ for (const file of files) {
854
+ const src = join(tempDir, file);
855
+ const dest = join(projectContextDir, file);
856
+ await fs.rename(src, dest);
857
+ }
858
+
859
+ // Remove temp directory
860
+ await fs.rm(tempDir, { recursive: true, force: true });
861
+
862
+ spinner.succeed('Project context repository configured');
863
+ console.log(chalk.green(` → Location: .claude/project-context/\n`));
864
+ } catch (error) {
865
+ spinner.fail('Failed to setup project context repository');
866
+ console.log(chalk.yellow(`\n⚠️ You can clone it manually with:`));
867
+ console.log(chalk.gray(` cd .claude`));
868
+ console.log(chalk.gray(` rm -rf project-context`));
869
+ console.log(chalk.gray(` git clone ${sanitizedUrl} project-context\n`));
870
+ // Don't throw - allow installation to continue
871
+ }
872
+ return;
873
+ }
874
+
875
+ // Not cloned yet during wizard, try to clone now
708
876
  const spinner = ora('Cloning project context repository...').start();
709
877
 
710
878
  try {
@@ -716,11 +884,11 @@ async function cloneProjectContextRepo(repoUrl) {
716
884
  await fs.unlink(generatedFile);
717
885
  }
718
886
 
719
- // Clone repo directly into project-context directory
720
- await execAsync(`git clone ${repoUrl} ${projectContextDir}-temp`);
887
+ // Clone repo
888
+ const tempDir = `${projectContextDir}-temp`;
889
+ await execAsync(`git clone ${sanitizedUrl} ${tempDir}`, { timeout: 30000 });
721
890
 
722
891
  // Move contents from temp to project-context
723
- const tempDir = `${projectContextDir}-temp`;
724
892
  const files = await fs.readdir(tempDir);
725
893
  for (const file of files) {
726
894
  const src = join(tempDir, file);
@@ -729,17 +897,22 @@ async function cloneProjectContextRepo(repoUrl) {
729
897
  }
730
898
 
731
899
  // Remove temp directory
732
- await fs.rmdir(tempDir);
900
+ await fs.rm(tempDir, { recursive: true, force: true });
733
901
 
734
902
  spinner.succeed('Project context repository cloned');
735
- console.log(chalk.green(` → Cloned from: ${repoUrl}`));
903
+ console.log(chalk.green(` → Cloned from: ${sanitizedUrl}`));
736
904
  console.log(chalk.gray(` → Location: .claude/project-context/\n`));
737
905
  } catch (error) {
738
906
  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`));
907
+ console.log(chalk.yellow(`\n⚠️ Error: ${error.message}`));
908
+ console.log(chalk.gray('\n Common issues:'));
909
+ console.log(chalk.gray(' Check SSH keys are configured: ssh -T git@bitbucket.org'));
910
+ console.log(chalk.gray(' Verify repository URL is correct'));
911
+ console.log(chalk.gray(' • Ensure you have access to the repository\n'));
912
+ console.log(chalk.yellow(` You can clone it manually later with:`));
913
+ console.log(chalk.gray(` cd .claude`));
914
+ console.log(chalk.gray(` rm -rf project-context`));
915
+ console.log(chalk.gray(` git clone ${sanitizedUrl} project-context\n`));
743
916
  // Don't throw - allow installation to continue
744
917
  }
745
918
  }
@@ -814,7 +987,7 @@ async function main() {
814
987
 
815
988
  // Step 10: Clone project context repository (optional)
816
989
  if (config.projectContextRepo) {
817
- await cloneProjectContextRepo(config.projectContextRepo);
990
+ await cloneProjectContextRepo(config.projectContextRepo, config.projectContextAlreadyCloned);
818
991
  }
819
992
 
820
993
  // 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.5",
4
4
  "description": "Multi-agent orchestration system for Claude Code - DevOps automation toolkit",
5
5
  "main": "index.js",
6
6
  "type": "module",