@jaguilar87/gaia-ops 1.0.1 → 1.0.2

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/bin/gaia-init.js CHANGED
@@ -445,6 +445,58 @@ async function generateAgentsMd() {
445
445
  }
446
446
  }
447
447
 
448
+ /**
449
+ * Validate and setup project paths (gitops, terraform, app-services)
450
+ * Creates directories if they don't exist (with user confirmation in interactive mode)
451
+ */
452
+ async function validateAndSetupProjectPaths(config, nonInteractive) {
453
+ console.log(chalk.cyan('\nšŸ“ Setting up project directories...\n'));
454
+
455
+ const paths = {
456
+ gitops: { path: config.gitops, name: 'GitOps' },
457
+ terraform: { path: config.terraform, name: 'Terraform' },
458
+ appServices: { path: config.appServices, name: 'App Services' }
459
+ };
460
+
461
+ for (const [, { path: userPath, name }] of Object.entries(paths)) {
462
+ const absPath = resolve(CWD, userPath);
463
+
464
+ // Check if path exists
465
+ if (existsSync(absPath)) {
466
+ console.log(chalk.gray(` āœ“ ${name}: ${userPath} (exists)`));
467
+ continue;
468
+ }
469
+
470
+ // Path doesn't exist - decide what to do
471
+ let shouldCreate = nonInteractive; // Auto-create in non-interactive mode
472
+
473
+ if (!nonInteractive) {
474
+ // Ask user in interactive mode
475
+ const response = await prompts({
476
+ type: 'confirm',
477
+ name: 'create',
478
+ message: `Directory ${chalk.yellow(userPath)} doesn't exist. Create it?`,
479
+ initial: true
480
+ });
481
+ shouldCreate = response.create;
482
+ }
483
+
484
+ if (shouldCreate) {
485
+ await fs.mkdir(absPath, { recursive: true });
486
+ console.log(chalk.green(` āœ“ ${name}: ${userPath} (created)`));
487
+ } else {
488
+ console.log(chalk.yellow(` ⚠ ${name}: ${userPath} (skipped - agents may create it later if needed)`));
489
+ }
490
+
491
+ // Warn about absolute paths (portability concern)
492
+ if (isAbsolute(userPath)) {
493
+ console.log(chalk.yellow(` ⚠ Warning: Absolute path "${userPath}" may not work on other machines`));
494
+ }
495
+ }
496
+
497
+ console.log('');
498
+ }
499
+
448
500
  /**
449
501
  * Generate project-context.json
450
502
  */
@@ -453,8 +505,17 @@ async function generateProjectContext(config) {
453
505
 
454
506
  try {
455
507
  const projectContext = {
456
- version: '1.0',
457
- last_updated: new Date().toISOString(),
508
+ metadata: {
509
+ version: '1.0',
510
+ last_updated: new Date().toISOString(),
511
+ project_root: '.', // Reference point: where CLAUDE.md is located
512
+ created_by: 'gaia-init'
513
+ },
514
+ paths: {
515
+ gitops: config.gitops,
516
+ terraform: config.terraform,
517
+ app_services: config.appServices
518
+ },
458
519
  project_details: {
459
520
  id: config.projectId,
460
521
  region: config.region,
@@ -590,6 +651,9 @@ async function main() {
590
651
  // Step 5: Install @jaguilar87/gaia-ops package
591
652
  await installClaudeAgentsPackage();
592
653
 
654
+ // Step 5.5: Validate and setup project paths (gitops, terraform, app-services)
655
+ await validateAndSetupProjectPaths(config, args.nonInteractive);
656
+
593
657
  // Step 6: Create .claude/ directory with symlinks
594
658
  await createClaudeDirectory();
595
659
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaguilar87/gaia-ops",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Multi-agent orchestration system for Claude Code - DevOps automation toolkit",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,8 +1,9 @@
1
1
  import json
2
2
  import argparse
3
3
  import sys
4
+ import os
4
5
  from pathlib import Path
5
- from typing import Dict, List, Any
6
+ from typing import Dict, List, Any, Optional
6
7
 
7
8
  # This script is expected to be run from the root of the repository.
8
9
  # The project context file is located at `.claude/project-context.json`.
@@ -57,6 +58,108 @@ def load_project_context(context_path: Path) -> Dict[str, Any]:
57
58
  with open(context_path, 'r', encoding='utf-8') as f:
58
59
  return json.load(f)
59
60
 
61
+
62
+ def get_project_root() -> Path:
63
+ """
64
+ Finds the project root by looking for CLAUDE.md.
65
+ This is the reference point for resolving relative paths.
66
+ Similar to JAVA_HOME or GAIA_HOME concept.
67
+ """
68
+ current = Path.cwd()
69
+
70
+ # Walk up the directory tree looking for CLAUDE.md
71
+ while current != current.parent:
72
+ claude_md = current / "CLAUDE.md"
73
+ if claude_md.is_file():
74
+ return current
75
+ current = current.parent
76
+
77
+ # If not found, assume current directory is project root
78
+ print("Warning: CLAUDE.md not found, using current directory as project root", file=sys.stderr)
79
+ return Path.cwd()
80
+
81
+
82
+ def resolve_path(relative_path: str, project_root: Optional[Path] = None) -> Path:
83
+ """
84
+ Resolves a path to absolute, handling both relative and absolute paths.
85
+
86
+ Examples:
87
+ ./gitops → /home/jaguilar/project/gitops
88
+ ../shared-infra → /home/jaguilar/shared-infra
89
+ /abs/path → /abs/path (unchanged)
90
+
91
+ Args:
92
+ relative_path: Path from project-context.json
93
+ project_root: Project root (where CLAUDE.md is). If None, auto-detected.
94
+
95
+ Returns:
96
+ Absolute Path object
97
+ """
98
+ if project_root is None:
99
+ project_root = get_project_root()
100
+
101
+ path = Path(relative_path)
102
+
103
+ # If already absolute, return as-is
104
+ if path.is_absolute():
105
+ return path
106
+
107
+ # Resolve relative to project root
108
+ return (project_root / path).resolve()
109
+
110
+
111
+ def validate_project_paths(project_context: Dict[str, Any], auto_create: bool = True) -> List[str]:
112
+ """
113
+ Validates that all critical paths in project-context.json exist.
114
+ Optionally creates missing directories with a warning.
115
+
116
+ Args:
117
+ project_context: The loaded project context
118
+ auto_create: If True, creates missing directories (default: True for agents)
119
+
120
+ Returns:
121
+ List of warning messages (empty if all OK)
122
+ """
123
+ warnings = []
124
+ project_root = get_project_root()
125
+
126
+ # Get paths section
127
+ paths = project_context.get("paths", {})
128
+ if not paths:
129
+ # Fallback to old format (backward compatibility)
130
+ paths = {
131
+ "gitops": project_context.get("gitops_configuration", {}).get("repository", {}).get("path"),
132
+ "terraform": project_context.get("terraform_infrastructure", {}).get("layout", {}).get("base_path"),
133
+ "app_services": project_context.get("application_services", {}).get("base_path")
134
+ }
135
+ # Filter out None values
136
+ paths = {k: v for k, v in paths.items() if v}
137
+
138
+ for path_name, path_value in paths.items():
139
+ if not path_value:
140
+ continue
141
+
142
+ abs_path = resolve_path(path_value, project_root)
143
+
144
+ if not abs_path.exists():
145
+ if auto_create:
146
+ try:
147
+ abs_path.mkdir(parents=True, exist_ok=True)
148
+ msg = f"Created missing directory: {path_name} at {abs_path}"
149
+ print(f"āš ļø {msg}", file=sys.stderr)
150
+ warnings.append(msg)
151
+ except Exception as e:
152
+ msg = f"Failed to create {path_name} at {abs_path}: {e}"
153
+ print(f"āŒ {msg}", file=sys.stderr)
154
+ warnings.append(msg)
155
+ else:
156
+ msg = f"Path does not exist: {path_name} = {path_value} (resolved to {abs_path})"
157
+ print(f"āš ļø {msg}", file=sys.stderr)
158
+ warnings.append(msg)
159
+
160
+ return warnings
161
+
162
+
60
163
  def get_contract_context(project_context: Dict[str, Any], agent_name: str) -> Dict[str, Any]:
61
164
  """Extracts the contract-defined context for a given agent."""
62
165
  contract_keys = AGENT_CONTRACTS.get(agent_name)
@@ -161,7 +264,10 @@ def main():
161
264
  args = parser.parse_args()
162
265
 
163
266
  project_context = load_project_context(args.context_file)
164
-
267
+
268
+ # Validate project paths and auto-create missing directories
269
+ validate_project_paths(project_context, auto_create=True)
270
+
165
271
  contract_context = get_contract_context(project_context, args.agent_name)
166
272
 
167
273
  enrichment_context = get_semantic_enrichment(