Files
english/.opencode/scripts/resolve_env.py
2026-04-12 01:06:31 +07:00

342 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Centralized environment variable resolver for Claude Code skills.
Resolves environment variables following the Claude Code hierarchy:
1. process.env - Runtime environment (HIGHEST)
2. .opencode/skills/<skill>/.env - Project skill-specific
3. .opencode/skills/.env - Project shared
4. .opencode/.env - Project global
5. ~/.opencode/skills/<skill>/.env - User skill-specific
6. ~/.opencode/skills/.env - User shared
7. ~/.opencode/.env - User global (LOWEST)
Usage:
from resolve_env import resolve_env
api_key = resolve_env('GEMINI_API_KEY', skill='ai-multimodal')
api_key = resolve_env('GEMINI_API_KEY') # Without skill context
"""
import os
import sys
from pathlib import Path
from typing import Optional, Dict, List, Tuple
def _parse_env_file_fallback(path) -> Dict[str, str]:
"""
Pure-Python fallback .env parser when python-dotenv is not installed.
Handles basic .env format:
- KEY=value
- KEY="quoted value"
- KEY='single quoted'
- # comments (full line)
- Empty lines ignored
Args:
path: Path to .env file (str or Path)
Returns:
Dictionary of environment variables
"""
env_vars = {}
try:
with open(path, 'r') as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith('#'):
continue
# Parse KEY=value
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# Remove surrounding quotes
if (value.startswith('"') and value.endswith('"')) or \
(value.startswith("'") and value.endswith("'")):
value = value[1:-1]
env_vars[key] = value
except Exception:
pass
return env_vars
try:
from dotenv import dotenv_values
except ImportError:
# Use fallback parser when python-dotenv not installed
dotenv_values = _parse_env_file_fallback
def find_project_root() -> Optional[Path]:
"""Find project root by looking for .git or .claude directory."""
current = Path.cwd()
# Check current directory and all parents
for directory in [current] + list(current.parents):
if (directory / '.git').exists() or (directory / '.claude').exists():
return directory
return None
def get_env_file_paths(skill: Optional[str] = None) -> List[Tuple[str, Path]]:
"""
Get all potential .env file paths in priority order.
Args:
skill: Optional skill name for skill-specific configs
Returns:
List of (description, path) tuples in priority order (highest to lowest)
"""
paths = []
# Find project root
project_root = find_project_root()
# User home directory
home = Path.home()
# Priority 2-4: Project-level configs (if project root found)
if project_root:
if skill:
paths.append((
f"Project skill-specific ({skill})",
project_root / '.claude' / 'skills' / skill / '.env'
))
paths.append((
"Project skills shared",
project_root / '.claude' / 'skills' / '.env'
))
paths.append((
"Project global",
project_root / '.claude' / '.env'
))
# Priority 5-7: User-level configs
if skill:
paths.append((
f"User skill-specific ({skill})",
home / '.claude' / 'skills' / skill / '.env'
))
paths.append((
"User skills shared",
home / '.claude' / 'skills' / '.env'
))
paths.append((
"User global",
home / '.claude' / '.env'
))
return paths
def resolve_env(
var_name: str,
skill: Optional[str] = None,
default: Optional[str] = None,
verbose: bool = False
) -> Optional[str]:
"""
Resolve environment variable following Claude Code hierarchy.
Args:
var_name: Name of the environment variable to resolve
skill: Optional skill name for skill-specific resolution
default: Default value if variable not found anywhere
verbose: If True, print resolution details
Returns:
Resolved value or default if not found
"""
# Priority 1: Check process environment (HIGHEST)
value = os.getenv(var_name)
if value:
if verbose:
print(f"{var_name} found in: Runtime environment (process.env)")
return value
if verbose:
print(f"{var_name} not in: Runtime environment")
# Note: dotenv_values is always available (uses fallback if python-dotenv not installed)
# Priority 2-7: Check .env files in order
env_paths = get_env_file_paths(skill)
for description, path in env_paths:
if path.exists():
try:
env_vars = dotenv_values(path)
value = env_vars.get(var_name)
if value:
if verbose:
print(f"{var_name} found in: {description}")
print(f" Path: {path}")
return value
else:
if verbose:
print(f"{var_name} not in: {description} (file exists)")
except Exception as e:
if verbose:
print(f"⚠ Error reading {description}: {e}")
else:
if verbose:
print(f"{var_name} not in: {description} (file not found)")
# Not found anywhere — always show checked locations to help users debug
checked_files = [str(p) for _, p in env_paths if p.exists()]
missing_files = [str(p) for _, p in env_paths if not p.exists()]
print(f"[!] {var_name} not found in any location", file=sys.stderr)
if checked_files:
print(f" Checked (file exists, key absent):", file=sys.stderr)
for f in checked_files:
print(f" - {f}", file=sys.stderr)
if missing_files and verbose:
print(f" Not found (file missing):", file=sys.stderr)
for f in missing_files:
print(f" - {f}", file=sys.stderr)
print(f" Tip: Add {var_name}=<value> to one of the .env files above", file=sys.stderr)
if default:
if verbose:
print(f" Using default: {default}", file=sys.stderr)
return default
def find_all(var_name: str, skill: Optional[str] = None) -> List[Tuple[str, str, Path]]:
"""
Find all locations where a variable is defined.
Args:
var_name: Name of the environment variable
skill: Optional skill name
Returns:
List of (description, value, path) tuples for all found locations
"""
results = []
# Check process environment
value = os.getenv(var_name)
if value:
results.append(("Runtime environment", value, None))
# Check all .env files (dotenv_values always available via fallback)
env_paths = get_env_file_paths(skill)
for description, path in env_paths:
if path.exists():
try:
env_vars = dotenv_values(path)
value = env_vars.get(var_name)
if value:
results.append((description, value, path))
except Exception:
pass
return results
def show_hierarchy(skill: Optional[str] = None):
"""Print the environment variable resolution hierarchy."""
print("Environment Variable Resolution Hierarchy")
print("=" * 60)
print("\nPriority order (highest to lowest):")
print("1. process.env - Runtime environment")
env_paths = get_env_file_paths(skill)
for i, (description, path) in enumerate(env_paths, start=2):
exists = "" if path.exists() else ""
print(f"{i}. {description:30} {exists} {path}")
print("\n" + "=" * 60)
def main():
"""CLI interface for environment variable resolution."""
import argparse
parser = argparse.ArgumentParser(
description='Resolve environment variables following Claude Code hierarchy',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Resolve GEMINI_API_KEY for ai-multimodal skill
%(prog)s GEMINI_API_KEY --skill ai-multimodal
# Resolve with verbose output
%(prog)s GEMINI_API_KEY --skill ai-multimodal --verbose
# Find all locations where variable is defined
%(prog)s GEMINI_API_KEY --find-all
# Show hierarchy
%(prog)s --show-hierarchy --skill ai-multimodal
"""
)
parser.add_argument('var_name', nargs='?', help='Environment variable name to resolve')
parser.add_argument('--skill', help='Skill name for skill-specific resolution')
parser.add_argument('--default', help='Default value if not found')
parser.add_argument('--verbose', '-v', action='store_true', help='Show resolution details')
parser.add_argument('--find-all', action='store_true', help='Find all locations where variable is defined')
parser.add_argument('--show-hierarchy', action='store_true', help='Show resolution hierarchy')
parser.add_argument('--export', action='store_true', help='Output in export format for shell sourcing')
args = parser.parse_args()
if args.show_hierarchy:
show_hierarchy(args.skill)
sys.exit(0)
if not args.var_name:
parser.error("var_name is required unless --show-hierarchy is used")
if args.find_all:
results = find_all(args.var_name, args.skill)
if results:
print(f"Variable '{args.var_name}' found in {len(results)} location(s):")
print("=" * 60)
for i, (description, value, path) in enumerate(results, start=1):
priority = i if i == 1 else i + 1 # Account for process.env being priority 1
print(f"\n{priority}. {description}")
if path:
print(f" Path: {path}")
print(f" Value: {value[:50]}{'...' if len(value) > 50 else ''}")
print("\n" + "=" * 60)
print(f"✓ Resolved value (highest priority): {results[0][1][:50]}{'...' if len(results[0][1]) > 50 else ''}")
else:
print(f"❌ Variable '{args.var_name}' not found in any location")
sys.exit(1)
else:
value = resolve_env(args.var_name, args.skill, args.default, args.verbose)
if value:
if args.export:
print(f"export {args.var_name}='{value}'")
else:
print(value)
sys.exit(0)
else:
if not args.verbose:
print(f"Error: {args.var_name} not found", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()