diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 0000000..db7b01b --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,308 @@ +# Environment Variables + +StructKit supports environment variables to configure CLI arguments without requiring them to be specified on the command line. This is particularly useful for CI/CD pipelines, containerized environments, and automation workflows. + +## Overview + +Environment variables allow you to set default values for CLI arguments. Command-line arguments always take precedence over environment variables, ensuring flexibility when needed. + +## High Priority Environment Variables + +### `STRUCTKIT_GLOBAL_SYSTEM_PROMPT` + +Sets the global system prompt for OpenAI integration. + +**CLI Equivalent:** `--global-system-prompt` / `-p` + +**Use Case:** Typically a long or sensitive value that users want to set once. Avoids repeating the same prompt across multiple invocations. Ideal for CI/CD workflows and container initialization. + +**Example:** +```bash +export STRUCTKIT_GLOBAL_SYSTEM_PROMPT="You are a helpful assistant for generating project structures." +structkit generate my-structure ./output +``` + +### `STRUCTKIT_INPUT_STORE` + +Sets the path to the input store for template variables. + +**CLI Equivalent:** `--input-store` / `-n` + +**Default:** `/tmp/structkit/input.json` + +**Use Case:** Allows users to set a consistent default location for input data. Useful for workflows that need persistent input across multiple runs. + +**Example:** +```bash +export STRUCTKIT_INPUT_STORE="/home/user/structkit-inputs/data.json" +structkit generate my-structure ./output +``` + +### `STRUCTKIT_BACKUP_PATH` + +Sets the default backup location for file backups. + +**CLI Equivalent:** `--backup` / `-b` + +**Use Case:** Set a default backup location project-wide or environment-wide. Saves typing in repetitive operations. Useful for ensuring backups go to a specific location (e.g., mounted volume in containers). + +**Example:** +```bash +export STRUCTKIT_BACKUP_PATH="/backups/structkit" +structkit generate my-structure ./output +``` + +## Medium Priority Environment Variables + +### `STRUCTKIT_FILE_STRATEGY` + +Sets the default strategy for handling existing files. + +**CLI Equivalent:** `--file-strategy` / `-f` + +**Valid Values:** `overwrite`, `skip`, `append`, `rename`, `backup` + +**Default:** `overwrite` + +**Use Case:** Let users set a preferred default strategy. Could prevent accidental data loss if set to 'skip' or 'backup' by default. + +**Example:** +```bash +export STRUCTKIT_FILE_STRATEGY="backup" +structkit generate my-structure ./output +``` + +### `STRUCTKIT_NON_INTERACTIVE` + +Enables or disables interactive mode for all commands. + +**CLI Equivalent:** `--non-interactive` + +**Valid Values:** `true`, `1`, `yes` (case-insensitive) for enabled; any other value for disabled + +**Default:** `false` + +**Use Case:** Boolean flag useful for CI/CD pipelines. Could be set in environment and applied across all commands. + +**Example:** +```bash +export STRUCTKIT_NON_INTERACTIVE=true +structkit generate my-structure ./output +``` + +### `STRUCTKIT_OUTPUT_MODE` + +Sets the default output mode for the generate command. + +**CLI Equivalent:** `--output` / `-o` + +**Valid Values:** `console`, `file` + +**Default:** `file` + +**Use Case:** Some users might prefer 'console' output by default. Useful for pipeline integration. + +**Example:** +```bash +export STRUCTKIT_OUTPUT_MODE="console" +structkit generate my-structure ./output +``` + +## Shared Environment Variables + +### `STRUCTKIT_STRUCTURES_PATH` + +Sets the path to custom structure definitions. + +**CLI Equivalent:** `--structures-path` / `-s` + +**Use Case:** Allows specifying custom structures directory that applies across all commands (generate, list, info, generate-schema). + +**Example:** +```bash +export STRUCTKIT_STRUCTURES_PATH="/home/user/my-structures" +structkit list +structkit generate my-custom-structure ./output +``` + +### `STRUCTKIT_LOG_LEVEL` + +Sets the logging level for all commands. + +**CLI Equivalent:** `--log` (generate command) + +**Valid Values:** `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` + +**Default:** `INFO` + +**Use Case:** Control verbosity of output for debugging or production deployments. + +**Example:** +```bash +export STRUCTKIT_LOG_LEVEL="DEBUG" +structkit generate my-structure ./output +``` + +## Precedence Rules + +Command-line arguments **always take precedence** over environment variables. This allows environment variables to set sensible defaults while maintaining the ability to override them when needed. + +**Precedence Order (highest to lowest):** +1. Command-line arguments +2. Environment variables +3. Built-in defaults + +**Example:** +```bash +# Set default via environment variable +export STRUCTKIT_FILE_STRATEGY="backup" + +# Override with CLI argument +structkit generate --file-strategy skip my-structure ./output +# Uses 'skip', not 'backup' + +# Use default from environment +structkit generate my-structure ./output +# Uses 'backup' from STRUCTKIT_FILE_STRATEGY +``` + +## Docker and Containerization + +Environment variables are particularly useful when running StructKit in containers: + +**Docker Example:** +```bash +docker run \ + -e STRUCTKIT_STRUCTURES_PATH=/custom/structures \ + -e STRUCTKIT_NON_INTERACTIVE=true \ + -e STRUCTKIT_FILE_STRATEGY=backup \ + -v /custom/structures:/custom/structures \ + -v $(pwd):/workdir \ + ghcr.io/httpdss/structkit:main generate my-structure /workdir/output +``` + +**Docker Compose Example:** +```yaml +version: '3' +services: + structkit: + image: ghcr.io/httpdss/structkit:main + environment: + STRUCTKIT_STRUCTURES_PATH: /custom/structures + STRUCTKIT_NON_INTERACTIVE: "true" + STRUCTKIT_FILE_STRATEGY: backup + STRUCTKIT_LOG_LEVEL: DEBUG + volumes: + - /custom/structures:/custom/structures + - ./output:/workdir + command: generate my-structure /workdir/output +``` + +## CI/CD Pipeline Integration + +### GitHub Actions - Basic Example + +```yaml +name: Generate Project Structure + +on: [push, pull_request] + +jobs: + generate: + runs-on: ubuntu-latest + env: + STRUCTKIT_NON_INTERACTIVE: "true" + STRUCTKIT_BACKUP_PATH: /tmp/backups + STRUCTKIT_FILE_STRATEGY: backup + steps: + - uses: actions/checkout@v3 + + - name: Install StructKit + run: pip install structkit + + - name: Generate structure + run: structkit generate my-structure ./generated-project + + - name: Upload generated files + uses: actions/upload-artifact@v3 + with: + name: generated-project + path: generated-project/ +``` + +### GitHub Actions - Advanced Example with Custom Structures + +```yaml +name: Generate with Custom Structures + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + generate: + runs-on: ubuntu-latest + env: + STRUCTKIT_NON_INTERACTIVE: "true" + STRUCTKIT_LOG_LEVEL: DEBUG + STRUCTKIT_OUTPUT_MODE: console + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install StructKit + run: pip install structkit + + - name: Generate default structure + run: structkit generate python-basic ./my-project + + - name: Generate with custom backup strategy + env: + STRUCTKIT_BACKUP_PATH: ./backups + STRUCTKIT_FILE_STRATEGY: backup + run: structkit generate terraform-module ./my-infrastructure + + - name: Create summary + run: | + echo "## Generated Structures" >> $GITHUB_STEP_SUMMARY + echo "- Python project generated" >> $GITHUB_STEP_SUMMARY + echo "- Terraform module generated" >> $GITHUB_STEP_SUMMARY +``` + +## Best Practices + +1. **Use Environment Variables for Defaults** - Set environment variables for values that don't change frequently +2. **Override When Needed** - Use CLI arguments for one-off changes or specific use cases +3. **Document Configuration** - Document which environment variables are used in your project +4. **Sensitive Data** - Store sensitive data (like API keys) in environment variables, not in configuration files +5. **Validation** - Test environment variable configuration to ensure it works as expected + +## Troubleshooting + +### Environment variable not being picked up + +1. Verify the environment variable is set: `echo $VARIABLE_NAME` +2. Ensure you're using the correct variable name (case-sensitive on Linux/macOS) +3. If running in Docker, check that the environment variable is passed correctly with `-e` +4. Restart your terminal or shell session after setting the variable + +### CLI argument not overriding environment variable + +This should not happen - CLI arguments always take precedence. If you're experiencing this: +1. Verify the CLI argument is correctly formatted +2. Check that you're using the correct argument name (e.g., `--file-strategy` not `--strategy`) +3. Ensure there are no spaces or special characters in the argument value + +### Boolean environment variables not working correctly + +For `STRUCTKIT_NON_INTERACTIVE`, only `true`, `1`, and `yes` (case-insensitive, e.g., `"True"`, `"TRUE"`, `"YeS"`) are recognized as true values. All other values are treated as false, including: +- `"true "` (with trailing space) +- `"on"` or `"enable"` + +Use one of the recognized values for reliable behavior. diff --git a/structkit/commands/generate.py b/structkit/commands/generate.py index 0822600..37bee42 100644 --- a/structkit/commands/generate.py +++ b/structkit/commands/generate.py @@ -21,27 +21,27 @@ def __init__(self, parser): '-s', '--structures-path', type=str, - help='Path to structure definitions', + help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH)', default=os.getenv('STRUCTKIT_STRUCTURES_PATH', None) ) - parser.add_argument('-n', '--input-store', type=str, help='Path to the input store', default='/tmp/structkit/input.json') + parser.add_argument('-n', '--input-store', type=str, help='Path to the input store (env: STRUCTKIT_INPUT_STORE)', default=os.getenv('STRUCTKIT_INPUT_STORE', '/tmp/structkit/input.json')) parser.add_argument('-d', '--dry-run', action='store_true', help='Perform a dry run without creating any files or directories') parser.add_argument('--diff', action='store_true', help='Show unified diffs for files that would change during dry-run or console output') parser.add_argument('-v', '--vars', type=str, help='Template variables in the format KEY1=value1,KEY2=value2') - parser.add_argument('-b', '--backup', type=str, help='Path to the backup folder') + parser.add_argument('-b', '--backup', type=str, help='Path to the backup folder (env: STRUCTKIT_BACKUP_PATH)', default=os.getenv('STRUCTKIT_BACKUP_PATH', None)) parser.add_argument( '-f', '--file-strategy', type=str, choices=['overwrite', 'skip', 'append', 'rename', 'backup'], - default='overwrite', - help='Strategy for handling existing files').completer = file_strategy_completer - parser.add_argument('-p', '--global-system-prompt', type=str, help='Global system prompt for OpenAI') - parser.add_argument('--non-interactive', action='store_true', help='Run the command in non-interactive mode') + default=os.getenv('STRUCTKIT_FILE_STRATEGY', 'overwrite'), + help='Strategy for handling existing files (env: STRUCTKIT_FILE_STRATEGY)').completer = file_strategy_completer + parser.add_argument('-p', '--global-system-prompt', type=str, help='Global system prompt for OpenAI (env: STRUCTKIT_GLOBAL_SYSTEM_PROMPT)', default=os.getenv('STRUCTKIT_GLOBAL_SYSTEM_PROMPT', None)) + parser.add_argument('--non-interactive', action='store_true', help='Run the command in non-interactive mode (env: STRUCTKIT_NON_INTERACTIVE)', default=os.getenv('STRUCTKIT_NON_INTERACTIVE', '').lower() in ('true', '1', 'yes')) parser.add_argument('--mappings-file', type=str, action='append', help='Path to a YAML file containing mappings to be used in templates (can be specified multiple times)') parser.add_argument('-o', '--output', type=str, - choices=['console', 'file'], default='file', help='Output mode') + choices=['console', 'file'], default=os.getenv('STRUCTKIT_OUTPUT_MODE', 'file'), help='Output mode (env: STRUCTKIT_OUTPUT_MODE)') parser.set_defaults(func=self.execute) def _parse_template_vars(self, vars_str): diff --git a/structkit/commands/generate_schema.py b/structkit/commands/generate_schema.py index f0fe69c..c113a97 100644 --- a/structkit/commands/generate_schema.py +++ b/structkit/commands/generate_schema.py @@ -7,7 +7,7 @@ class GenerateSchemaCommand(Command): def __init__(self, parser): super().__init__(parser) parser.description = "Generate JSON schema for available structures" - parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions') + parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH)', default=os.getenv('STRUCTKIT_STRUCTURES_PATH', None)) parser.add_argument('-o', '--output', type=str, help='Output file path for the schema (default: stdout)') parser.set_defaults(func=self.execute) diff --git a/structkit/commands/info.py b/structkit/commands/info.py index f2e27a9..db7dfe4 100644 --- a/structkit/commands/info.py +++ b/structkit/commands/info.py @@ -13,7 +13,7 @@ def __init__(self, parser): parser.description = "Show information about the package or structure definition" parser.add_argument('structure_definition', type=str, help='Name of the structure definition') parser.add_argument( - '-s', '--structures-path', type=str, help='Path to structure definitions', + '-s', '--structures-path', type=str, help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH)', default=os.getenv('STRUCTKIT_STRUCTURES_PATH', None) ) parser.add_argument('--mcp', action='store_true', help='Enable MCP (Model Context Protocol) integration') diff --git a/structkit/commands/list.py b/structkit/commands/list.py index a73442e..41130dc 100644 --- a/structkit/commands/list.py +++ b/structkit/commands/list.py @@ -9,7 +9,7 @@ def __init__(self, parser): super().__init__(parser) parser.description = "List available structures" parser.add_argument( - '-s', '--structures-path', type=str, help='Path to structure definitions', + '-s', '--structures-path', type=str, help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH)', default=os.getenv('STRUCTKIT_STRUCTURES_PATH', None) ) parser.add_argument('--names-only', action='store_true', help='Print only structure names, one per line (for shell completion)') diff --git a/tests/test_commands.py b/tests/test_commands.py index 254b4c4..0f9598f 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -90,7 +90,7 @@ def test_generate_schema_command_init(parser): assert 'output' in actions # Check help text - assert actions['structures_path'].help == 'Path to structure definitions' + assert actions['structures_path'].help == 'Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH)' assert actions['output'].help == 'Output file path for the schema (default: stdout)' diff --git a/tests/test_env_var_cli_args.py b/tests/test_env_var_cli_args.py new file mode 100644 index 0000000..8455320 --- /dev/null +++ b/tests/test_env_var_cli_args.py @@ -0,0 +1,293 @@ +import pytest +from unittest.mock import patch, MagicMock +from structkit.commands.generate import GenerateCommand +import argparse +import os + + +class TestGlobalSystemPromptEnvVar: + """Tests for STRUCTKIT_GLOBAL_SYSTEM_PROMPT environment variable.""" + + def test_env_var_used_when_no_cli_arg(self): + """Test that STRUCTKIT_GLOBAL_SYSTEM_PROMPT is used when --global-system-prompt is not provided.""" + with patch.dict(os.environ, {'STRUCTKIT_GLOBAL_SYSTEM_PROMPT': 'System prompt from env'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + with patch('os.path.exists', return_value=True), \ + patch('builtins.open', new_callable=MagicMock), \ + patch('yaml.safe_load', return_value={'files': []}), \ + patch.object(command, '_create_structure') as mock_create_structure: + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.global_system_prompt == 'System prompt from env' + + def test_cli_arg_takes_precedence_over_env_var(self): + """Test that CLI --global-system-prompt takes precedence over env var.""" + with patch.dict(os.environ, {'STRUCTKIT_GLOBAL_SYSTEM_PROMPT': 'System prompt from env'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + with patch('os.path.exists', return_value=True), \ + patch('builtins.open', new_callable=MagicMock), \ + patch('yaml.safe_load', return_value={'files': []}), \ + patch.object(command, '_create_structure') as mock_create_structure: + + args = parser.parse_args(['--global-system-prompt', 'CLI prompt', 'structure.yaml', 'base_path']) + assert args.global_system_prompt == 'CLI prompt' + + +class TestInputStoreEnvVar: + """Tests for STRUCTKIT_INPUT_STORE environment variable.""" + + def test_env_var_overrides_default(self): + """Test that STRUCTKIT_INPUT_STORE overrides the default value.""" + with patch.dict(os.environ, {'STRUCTKIT_INPUT_STORE': '/custom/input.json'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.input_store == '/custom/input.json' + + def test_default_used_when_env_var_not_set(self): + """Test that default value is used when env var is not set.""" + env = os.environ.copy() + env.pop('STRUCTKIT_INPUT_STORE', None) + + with patch.dict(os.environ, env, clear=True): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.input_store == '/tmp/structkit/input.json' + + def test_cli_arg_takes_precedence(self): + """Test that CLI -n/--input-store takes precedence over env var.""" + with patch.dict(os.environ, {'STRUCTKIT_INPUT_STORE': '/env/input.json'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['--input-store', '/cli/input.json', 'structure.yaml', 'base_path']) + assert args.input_store == '/cli/input.json' + + +class TestBackupPathEnvVar: + """Tests for STRUCTKIT_BACKUP_PATH environment variable.""" + + def test_env_var_used_when_no_cli_arg(self): + """Test that STRUCTKIT_BACKUP_PATH is used when --backup is not provided.""" + with patch.dict(os.environ, {'STRUCTKIT_BACKUP_PATH': '/env/backup'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.backup == '/env/backup' + + def test_none_when_env_var_not_set(self): + """Test that backup is None when env var is not set.""" + env = os.environ.copy() + env.pop('STRUCTKIT_BACKUP_PATH', None) + + with patch.dict(os.environ, env, clear=True): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.backup is None + + def test_cli_arg_takes_precedence(self): + """Test that CLI -b/--backup takes precedence over env var.""" + with patch.dict(os.environ, {'STRUCTKIT_BACKUP_PATH': '/env/backup'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['--backup', '/cli/backup', 'structure.yaml', 'base_path']) + assert args.backup == '/cli/backup' + + +class TestFileStrategyEnvVar: + """Tests for STRUCTKIT_FILE_STRATEGY environment variable.""" + + def test_env_var_overrides_default(self): + """Test that STRUCTKIT_FILE_STRATEGY overrides the default 'overwrite' value.""" + with patch.dict(os.environ, {'STRUCTKIT_FILE_STRATEGY': 'skip'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.file_strategy == 'skip' + + def test_default_when_env_var_not_set(self): + """Test that default 'overwrite' is used when env var is not set.""" + env = os.environ.copy() + env.pop('STRUCTKIT_FILE_STRATEGY', None) + + with patch.dict(os.environ, env, clear=True): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.file_strategy == 'overwrite' + + def test_cli_arg_takes_precedence(self): + """Test that CLI -f/--file-strategy takes precedence over env var.""" + with patch.dict(os.environ, {'STRUCTKIT_FILE_STRATEGY': 'skip'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['--file-strategy', 'backup', 'structure.yaml', 'base_path']) + assert args.file_strategy == 'backup' + + def test_all_valid_strategies_from_env(self): + """Test that all valid strategies can be set via env var.""" + strategies = ['overwrite', 'skip', 'append', 'rename', 'backup'] + for strategy in strategies: + with patch.dict(os.environ, {'STRUCTKIT_FILE_STRATEGY': strategy}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.file_strategy == strategy + + +class TestNonInteractiveEnvVar: + """Tests for STRUCTKIT_NON_INTERACTIVE environment variable.""" + + @pytest.mark.parametrize("value,expected", [ + ('true', True), + ('1', True), + ('yes', True), + ('True', True), + ('TRUE', True), + ('Yes', True), + ('false', False), + ('0', False), + ('no', False), + ('', False), + ]) + def test_env_var_parsing(self, value, expected): + """Test that STRUCTKIT_NON_INTERACTIVE is correctly parsed for various values.""" + with patch.dict(os.environ, {'STRUCTKIT_NON_INTERACTIVE': value}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.non_interactive == expected + + def test_false_when_env_var_not_set(self): + """Test that non_interactive is False when env var is not set.""" + env = os.environ.copy() + env.pop('STRUCTKIT_NON_INTERACTIVE', None) + + with patch.dict(os.environ, env, clear=True): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.non_interactive is False + + def test_cli_flag_takes_precedence(self): + """Test that CLI --non-interactive flag takes precedence over env var.""" + with patch.dict(os.environ, {'STRUCTKIT_NON_INTERACTIVE': 'false'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['--non-interactive', 'structure.yaml', 'base_path']) + assert args.non_interactive is True + + +class TestOutputModeEnvVar: + """Tests for STRUCTKIT_OUTPUT_MODE environment variable.""" + + def test_env_var_overrides_default(self): + """Test that STRUCTKIT_OUTPUT_MODE overrides the default 'file' value.""" + with patch.dict(os.environ, {'STRUCTKIT_OUTPUT_MODE': 'console'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.output == 'console' + + def test_default_when_env_var_not_set(self): + """Test that default 'file' is used when env var is not set.""" + env = os.environ.copy() + env.pop('STRUCTKIT_OUTPUT_MODE', None) + + with patch.dict(os.environ, env, clear=True): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + assert args.output == 'file' + + def test_cli_arg_takes_precedence(self): + """Test that CLI -o/--output takes precedence over env var.""" + with patch.dict(os.environ, {'STRUCTKIT_OUTPUT_MODE': 'console'}): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['--output', 'file', 'structure.yaml', 'base_path']) + assert args.output == 'file' + + +class TestEnvVarCombinations: + """Tests for multiple environment variables used together.""" + + def test_multiple_env_vars_set_simultaneously(self): + """Test that multiple env vars work correctly when set together.""" + env_vars = { + 'STRUCTKIT_GLOBAL_SYSTEM_PROMPT': 'Test prompt', + 'STRUCTKIT_INPUT_STORE': '/custom/input.json', + 'STRUCTKIT_BACKUP_PATH': '/custom/backup', + 'STRUCTKIT_FILE_STRATEGY': 'backup', + 'STRUCTKIT_NON_INTERACTIVE': 'true', + 'STRUCTKIT_OUTPUT_MODE': 'console', + 'STRUCTKIT_STRUCTURES_PATH': '/custom/structures', + } + + with patch.dict(os.environ, env_vars): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args(['structure.yaml', 'base_path']) + + assert args.global_system_prompt == 'Test prompt' + assert args.input_store == '/custom/input.json' + assert args.backup == '/custom/backup' + assert args.file_strategy == 'backup' + assert args.non_interactive is True + assert args.output == 'console' + assert args.structures_path == '/custom/structures' + + def test_cli_args_override_all_env_vars(self): + """Test that CLI args override all env vars when provided.""" + env_vars = { + 'STRUCTKIT_GLOBAL_SYSTEM_PROMPT': 'Env prompt', + 'STRUCTKIT_INPUT_STORE': '/env/input.json', + 'STRUCTKIT_BACKUP_PATH': '/env/backup', + 'STRUCTKIT_FILE_STRATEGY': 'skip', + 'STRUCTKIT_NON_INTERACTIVE': 'false', + 'STRUCTKIT_OUTPUT_MODE': 'console', + } + + with patch.dict(os.environ, env_vars): + parser = argparse.ArgumentParser() + command = GenerateCommand(parser) + + args = parser.parse_args([ + '--global-system-prompt', 'CLI prompt', + '--input-store', '/cli/input.json', + '--backup', '/cli/backup', + '--file-strategy', 'rename', + '--non-interactive', + '--output', 'file', + 'structure.yaml', + 'base_path' + ]) + + assert args.global_system_prompt == 'CLI prompt' + assert args.input_store == '/cli/input.json' + assert args.backup == '/cli/backup' + assert args.file_strategy == 'rename' + assert args.non_interactive is True + assert args.output == 'file'