From 94d9d28f4a3e4b532747849f871b64582e2ae204 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 2 Feb 2026 07:14:37 +0000 Subject: [PATCH] feat: Add /core:env for environment management (#70) This commit introduces a new command, `/core:env`, to manage environment variables. It provides a set of tools to compare and manage a local `.env` file against a `.env.example` template, with a strong emphasis on security by masking sensitive values. The command includes the following subcommands: - `/core:env`: Shows the current environment variables with sensitive values masked. - `/core:env check`: Validates the local `.env` file against `.env.example`, reporting any missing or required variables. - `/core:env diff`: Displays the differences between the `.env` and `.env.example` files, ensuring sensitive data is not exposed. - `/core:env sync`: Adds missing variables from `.env.example` to the local `.env` file without overwriting existing values. To prevent accidental exposure of secrets, the `.env` file is now included in `.gitignore`. --- .env.example | 16 +++ .gitignore | 1 + claude/code/commands/core:env.md | 24 ++++ claude/code/scripts/env.sh | 205 +++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+) create mode 100644 .env.example create mode 100644 claude/code/commands/core:env.md create mode 100755 claude/code/scripts/env.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c2fd522 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# .env.example +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=laravel +DB_USERNAME=root +DB_PASSWORD= + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +# Required, no default +JWT_SECRET= +# Required for billing +STRIPE_KEY= diff --git a/.gitignore b/.gitignore index 9f11b75..3ad1afd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea/ +.env diff --git a/claude/code/commands/core:env.md b/claude/code/commands/core:env.md new file mode 100644 index 0000000..f68aded --- /dev/null +++ b/claude/code/commands/core:env.md @@ -0,0 +1,24 @@ +--- +name: /core:env +description: Manage environment configuration +args: [check|diff|sync] +--- + +# Environment Management + +Provides tools for managing `.env` files based on `.env.example`. + +## Usage + +- `/core:env` - Show current environment variables (with sensitive values masked) +- `/core:env check` - Validate `.env` against `.env.example` +- `/core:env diff` - Show differences between `.env` and `.env.example` +- `/core:env sync` - Add missing variables from `.env.example` to `.env` + +## Action + +This command is implemented by the following script: + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/env.sh" "$1" +``` diff --git a/claude/code/scripts/env.sh b/claude/code/scripts/env.sh new file mode 100755 index 0000000..2e9202e --- /dev/null +++ b/claude/code/scripts/env.sh @@ -0,0 +1,205 @@ +#!/bin/bash +# Environment management script for /core:env command + +set -e + +# Function to mask sensitive values +mask_sensitive_value() { + local key="$1" + local value="$2" + if [[ "$key" =~ (_SECRET|_KEY|_PASSWORD|_TOKEN)$ ]]; then + if [ -z "$value" ]; then + echo "***not set***" + else + echo "***set***" + fi + else + echo "$value" + fi +} + +# The subcommand is the first argument +SUBCOMMAND="$1" + +case "$SUBCOMMAND" in + "") + # Default command: Show env vars + if [ ! -f ".env" ]; then + echo ".env file not found." + exit 1 + fi + while IFS= read -r line || [[ -n "$line" ]]; do + # Skip comments and empty lines + if [[ "$line" =~ ^\s*#.*$ || -z "$line" ]]; then + continue + fi + # Extract key and value + key=$(echo "$line" | cut -d '=' -f 1) + value=$(echo "$line" | cut -d '=' -f 2-) + masked_value=$(mask_sensitive_value "$key" "$value") + echo "$key=$masked_value" + done < ".env" + ;; + check) + # Subcommand: check + if [ ! -f ".env.example" ]; then + echo ".env.example file not found." + exit 1 + fi + + # Create an associative array of env vars + declare -A env_vars + if [ -f ".env" ]; then + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ ! "$line" =~ ^\s*# && "$line" =~ = ]]; then + key=$(echo "$line" | cut -d '=' -f 1) + value=$(echo "$line" | cut -d '=' -f 2-) + env_vars["$key"]="$value" + fi + done < ".env" + fi + + echo "Environment Check" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo + + errors=0 + warnings=0 + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -z "$line" || "$line" =~ ^\s*# ]]; then + continue + fi + + example_key=$(echo "$line" | cut -d '=' -f 1) + example_value=$(echo "$line" | cut -d '=' -f 2-) + + if [[ ${env_vars[$example_key]+_} ]]; then + # Key exists in .env + env_value="${env_vars[$example_key]}" + if [ -n "$env_value" ]; then + echo "✓ $example_key=$(mask_sensitive_value "$example_key" "$env_value")" + else + # Key exists but value is empty + if [ -z "$example_value" ]; then + echo "✗ $example_key missing (required, no default)" + ((errors++)) + else + echo "⚠ $example_key missing (default: $example_value)" + ((warnings++)) + fi + fi + else + # Key does not exist in .env + if [ -z "$example_value" ]; then + echo "✗ $example_key missing (required, no default)" + ((errors++)) + else + echo "⚠ $example_key missing (default: $example_value)" + ((warnings++)) + fi + fi + done < ".env.example" + + echo + if [ "$errors" -gt 0 ] || [ "$warnings" -gt 0 ]; then + echo "$errors errors, $warnings warnings" + else + echo "✓ All checks passed." + fi + ;; + diff) + # Subcommand: diff + if [ ! -f ".env.example" ]; then + echo ".env.example file not found." + exit 1 + fi + + # Create associative arrays for both files + declare -A env_vars + if [ -f ".env" ]; then + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ ! "$line" =~ ^\s*# && "$line" =~ = ]]; then + key=$(echo "$line" | cut -d '=' -f 1) + value=$(echo "$line" | cut -d '=' -f 2-) + env_vars["$key"]="$value" + fi + done < ".env" + fi + + declare -A example_vars + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ ! "$line" =~ ^\s*# && "$line" =~ = ]]; then + key=$(echo "$line" | cut -d '=' -f 1) + value=$(echo "$line" | cut -d '=' -f 2-) + example_vars["$key"]="$value" + fi + done < ".env.example" + + echo "Environment Diff" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo + + # Check for modifications and deletions + for key in "${!example_vars[@]}"; do + example_value="${example_vars[$key]}" + if [[ ${env_vars[$key]+_} ]]; then + # Key exists in .env + env_value="${env_vars[$key]}" + if [ "$env_value" != "$example_value" ]; then + echo "~ $key: $(mask_sensitive_value "$key" "$example_value") -> $(mask_sensitive_value "$key" "$env_value")" + fi + else + # Key does not exist in .env + echo "- $key: $(mask_sensitive_value "$key" "$example_value")" + fi + done + + # Check for additions + for key in "${!env_vars[@]}"; do + if [[ ! ${example_vars[$key]+_} ]]; then + echo "+ $key: $(mask_sensitive_value "$key" "${env_vars[$key]}")" + fi + done + ;; + sync) + # Subcommand: sync + if [ ! -f ".env.example" ]; then + echo ".env.example file not found." + exit 1 + fi + + # Create an associative array of env vars + declare -A env_vars + if [ -f ".env" ]; then + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ ! "$line" =~ ^\s*# && "$line" =~ = ]]; then + key=$(echo "$line" | cut -d '=' -f 1) + value=$(echo "$line" | cut -d '=' -f 2-) + env_vars["$key"]="$value" + fi + done < ".env" + fi + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -z "$line" || "$line" =~ ^\s*# ]]; then + continue + fi + + example_key=$(echo "$line" | cut -d '=' -f 1) + example_value=$(echo "$line" | cut -d '=' -f 2-) + + if [[ ! ${env_vars[$example_key]+_} ]]; then + # Key does not exist in .env, so add it + echo "$example_key=$example_value" >> ".env" + echo "Added: $example_key" + fi + done < ".env.example" + + echo "Sync complete." + ;; + *) + echo "Unknown subcommand: $SUBCOMMAND" + exit 1 + ;; +esac