ic-code 1.0.0__py3-none-any.whl

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.
ic/commands/config.py ADDED
@@ -0,0 +1,699 @@
1
+ """
2
+ Configuration management CLI commands.
3
+
4
+ This module provides CLI commands for configuration management, migration,
5
+ and validation.
6
+ """
7
+
8
+ import argparse
9
+ import os
10
+ import sys
11
+ import yaml
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Dict, Any, Optional, List
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.table import Table
18
+ from rich.prompt import Confirm, Prompt
19
+ from rich.syntax import Syntax
20
+
21
+ from ..config.manager import ConfigManager
22
+ from ..config.security import SecurityManager
23
+ from ..config.migration import MigrationManager
24
+ from ..core.logging import ICLogger
25
+
26
+
27
+ class ConfigCommands:
28
+ """Configuration management commands."""
29
+
30
+ def __init__(self):
31
+ self.console = Console()
32
+ self.security_manager = SecurityManager()
33
+ self.config_manager = ConfigManager(security_manager=self.security_manager)
34
+ self.migration = MigrationManager()
35
+
36
+ def add_subparsers(self, parent_parser: argparse.ArgumentParser) -> None:
37
+ """
38
+ Add config subcommands to parent parser.
39
+
40
+ Args:
41
+ parent_parser: Parent argument parser
42
+ """
43
+ config_parser = parent_parser.add_parser(
44
+ "config",
45
+ help="Configuration management commands"
46
+ )
47
+ config_subparsers = config_parser.add_subparsers(
48
+ dest="config_command",
49
+ required=True,
50
+ help="Configuration management operations"
51
+ )
52
+
53
+ # ic config init
54
+ init_parser = config_subparsers.add_parser(
55
+ "init",
56
+ help="Initialize secure configuration setup"
57
+ )
58
+ init_parser.add_argument(
59
+ "--output", "-o",
60
+ default="ic.yaml",
61
+ help="Output configuration file path (default: ic.yaml)"
62
+ )
63
+ init_parser.add_argument(
64
+ "--template", "-t",
65
+ choices=["minimal", "full", "aws", "azure", "gcp", "multi-cloud"],
66
+ default="minimal",
67
+ help="Configuration template to use (default: minimal)"
68
+ )
69
+ init_parser.add_argument(
70
+ "--force", "-f",
71
+ action="store_true",
72
+ help="Overwrite existing configuration file"
73
+ )
74
+ init_parser.set_defaults(func=self.init_config)
75
+
76
+ # ic config migrate
77
+ migrate_parser = config_subparsers.add_parser(
78
+ "migrate",
79
+ help="Migrate from .env to YAML configuration"
80
+ )
81
+ migrate_parser.add_argument(
82
+ "--env-file",
83
+ default=".env",
84
+ help="Source .env file path (default: .env)"
85
+ )
86
+ migrate_parser.add_argument(
87
+ "--output", "-o",
88
+ default="ic.yaml",
89
+ help="Output YAML configuration file (default: ic.yaml)"
90
+ )
91
+ migrate_parser.add_argument(
92
+ "--backup", "-b",
93
+ action="store_true",
94
+ default=True,
95
+ help="Create backup of existing files (default: True)"
96
+ )
97
+ migrate_parser.add_argument(
98
+ "--dry-run", "-n",
99
+ action="store_true",
100
+ help="Show what would be migrated without making changes"
101
+ )
102
+ migrate_parser.set_defaults(func=self.migrate_config)
103
+
104
+ # ic config validate
105
+ validate_parser = config_subparsers.add_parser(
106
+ "validate",
107
+ help="Validate configuration files"
108
+ )
109
+ validate_parser.add_argument(
110
+ "config_file",
111
+ nargs="?",
112
+ help="Configuration file to validate (default: auto-detect)"
113
+ )
114
+ validate_parser.add_argument(
115
+ "--security", "-s",
116
+ action="store_true",
117
+ help="Include security validation"
118
+ )
119
+ validate_parser.add_argument(
120
+ "--verbose", "-v",
121
+ action="store_true",
122
+ help="Show detailed validation results"
123
+ )
124
+ validate_parser.set_defaults(func=self.validate_config)
125
+
126
+ # ic config show
127
+ show_parser = config_subparsers.add_parser(
128
+ "show",
129
+ help="Show current configuration"
130
+ )
131
+ show_parser.add_argument(
132
+ "--sources", "-s",
133
+ action="store_true",
134
+ help="Show configuration sources"
135
+ )
136
+ show_parser.add_argument(
137
+ "--mask-sensitive", "-m",
138
+ action="store_true",
139
+ default=True,
140
+ help="Mask sensitive data in output (default: True)"
141
+ )
142
+ show_parser.add_argument(
143
+ "--format", "-f",
144
+ choices=["yaml", "json", "table"],
145
+ default="yaml",
146
+ help="Output format (default: yaml)"
147
+ )
148
+ show_parser.add_argument(
149
+ "key_path",
150
+ nargs="?",
151
+ help="Specific configuration key to show (dot notation, e.g., aws.regions)"
152
+ )
153
+ show_parser.set_defaults(func=self.show_config)
154
+
155
+ # ic config set
156
+ set_parser = config_subparsers.add_parser(
157
+ "set",
158
+ help="Set configuration value"
159
+ )
160
+ set_parser.add_argument(
161
+ "key_path",
162
+ help="Configuration key to set (dot notation, e.g., aws.regions)"
163
+ )
164
+ set_parser.add_argument(
165
+ "value",
166
+ help="Value to set (JSON format for complex values)"
167
+ )
168
+ set_parser.add_argument(
169
+ "--config-file", "-c",
170
+ default="ic.yaml",
171
+ help="Configuration file to update (default: ic.yaml)"
172
+ )
173
+ set_parser.add_argument(
174
+ "--create",
175
+ action="store_true",
176
+ help="Create configuration file if it doesn't exist"
177
+ )
178
+ set_parser.set_defaults(func=self.set_config)
179
+
180
+ # ic config get
181
+ get_parser = config_subparsers.add_parser(
182
+ "get",
183
+ help="Get configuration value"
184
+ )
185
+ get_parser.add_argument(
186
+ "key_path",
187
+ help="Configuration key to get (dot notation, e.g., aws.regions)"
188
+ )
189
+ get_parser.add_argument(
190
+ "--default", "-d",
191
+ help="Default value if key not found"
192
+ )
193
+ get_parser.add_argument(
194
+ "--format", "-f",
195
+ choices=["raw", "json", "yaml"],
196
+ default="raw",
197
+ help="Output format (default: raw)"
198
+ )
199
+ get_parser.set_defaults(func=self.get_config)
200
+
201
+ def init_config(self, args) -> None:
202
+ """
203
+ Initialize secure configuration setup.
204
+
205
+ Args:
206
+ args: Command line arguments
207
+ """
208
+ output_path = Path(args.output)
209
+
210
+ # Check if file exists and not forcing
211
+ if output_path.exists() and not args.force:
212
+ if not Confirm.ask(f"Configuration file {output_path} already exists. Overwrite?"):
213
+ self.console.print("āŒ Configuration initialization cancelled.")
214
+ return
215
+
216
+ self.console.print(f"šŸš€ Initializing IC configuration with template: {args.template}")
217
+
218
+ # Get template configuration
219
+ template_config = self._get_template_config(args.template)
220
+
221
+ # Interactive configuration if not minimal
222
+ if args.template != "minimal":
223
+ template_config = self._interactive_config_setup(template_config, args.template)
224
+
225
+ try:
226
+ # Save configuration
227
+ self.config_manager.save_config(output_path, template_config)
228
+
229
+ # Create .env.example if it doesn't exist
230
+ env_example_path = Path(".env.example")
231
+ if not env_example_path.exists():
232
+ self._create_env_example(env_example_path, args.template)
233
+
234
+ # Update .gitignore
235
+ self._update_gitignore()
236
+
237
+ self.console.print(Panel(
238
+ f"āœ… Configuration initialized successfully!\n\n"
239
+ f"šŸ“ Configuration file: {output_path}\n"
240
+ f"šŸ“„ Environment example: .env.example\n"
241
+ f"šŸ”’ .gitignore updated for security\n\n"
242
+ f"Next steps:\n"
243
+ f"1. Review and customize {output_path}\n"
244
+ f"2. Set up environment variables (see .env.example)\n"
245
+ f"3. Run 'ic config validate' to verify setup",
246
+ title="Configuration Initialized",
247
+ border_style="green"
248
+ ))
249
+
250
+ except Exception as e:
251
+ self.console.print(f"āŒ Failed to initialize configuration: {e}")
252
+ sys.exit(1)
253
+
254
+ def migrate_config(self, args) -> None:
255
+ """
256
+ Migrate from .env to YAML configuration.
257
+
258
+ Args:
259
+ args: Command line arguments
260
+ """
261
+ env_file = Path(args.env_file)
262
+ output_file = Path(args.output)
263
+
264
+ if not env_file.exists():
265
+ self.console.print(f"āŒ Environment file {env_file} not found.")
266
+ sys.exit(1)
267
+
268
+ self.console.print(f"šŸ”„ Migrating configuration from {env_file} to {output_file}")
269
+
270
+ try:
271
+ # Perform migration
272
+ if args.dry_run:
273
+ self.console.print("šŸ” Dry run - showing what would be migrated:")
274
+ # TODO: Implement dry run preview
275
+ result = {"success": True, "dry_run": True}
276
+ else:
277
+ success = self.migration.migrate_env_to_yaml(str(env_file), force=True)
278
+ result = {"success": success, "output_file": str(output_file)}
279
+
280
+ if args.dry_run:
281
+ self.console.print("šŸ” Dry run - showing what would be migrated:")
282
+ self._display_migration_preview(result)
283
+ else:
284
+ self._display_migration_result(result)
285
+
286
+ except Exception as e:
287
+ self.console.print(f"āŒ Migration failed: {e}")
288
+ sys.exit(1)
289
+
290
+ def validate_config(self, args) -> None:
291
+ """
292
+ Validate configuration files.
293
+
294
+ Args:
295
+ args: Command line arguments
296
+ """
297
+ if args.config_file:
298
+ config_file = Path(args.config_file)
299
+ if not config_file.exists():
300
+ self.console.print(f"āŒ Configuration file {config_file} not found.")
301
+ sys.exit(1)
302
+ config_files = [config_file]
303
+ else:
304
+ # Auto-detect configuration files
305
+ config_files = self._find_config_files()
306
+
307
+ if not config_files:
308
+ self.console.print("āŒ No configuration files found.")
309
+ sys.exit(1)
310
+
311
+ self.console.print("šŸ” Validating configuration files...")
312
+
313
+ all_valid = True
314
+ for config_file in config_files:
315
+ self.console.print(f"\nšŸ“„ Validating {config_file}:")
316
+
317
+ try:
318
+ # Load and validate configuration
319
+ config_data = self.config_manager._load_config_file(config_file)
320
+ errors = self.config_manager.validate_config(config_data)
321
+
322
+ # Security validation if requested
323
+ security_warnings = []
324
+ if args.security:
325
+ security_warnings = self.security_manager.validate_config_security(config_data)
326
+
327
+ # Display results
328
+ if not errors and not security_warnings:
329
+ self.console.print(" āœ… Configuration is valid")
330
+ else:
331
+ all_valid = False
332
+
333
+ if errors:
334
+ self.console.print(" āŒ Validation errors:")
335
+ for error in errors:
336
+ self.console.print(f" • {error}")
337
+
338
+ if security_warnings:
339
+ self.console.print(" āš ļø Security warnings:")
340
+ for warning in security_warnings:
341
+ self.console.print(f" • {warning}")
342
+
343
+ if args.verbose:
344
+ self._display_config_summary(config_data)
345
+
346
+ except Exception as e:
347
+ all_valid = False
348
+ self.console.print(f" āŒ Failed to validate: {e}")
349
+
350
+ if all_valid:
351
+ self.console.print("\nāœ… All configuration files are valid!")
352
+ else:
353
+ self.console.print("\nāŒ Some configuration files have issues.")
354
+ sys.exit(1)
355
+
356
+ def show_config(self, args) -> None:
357
+ """
358
+ Show current configuration.
359
+
360
+ Args:
361
+ args: Command line arguments
362
+ """
363
+ try:
364
+ # Load configuration
365
+ config = self.config_manager.load_config()
366
+
367
+ # Mask sensitive data if requested
368
+ if args.mask_sensitive:
369
+ config = self.security_manager.mask_sensitive_data(config)
370
+
371
+ # Show specific key if requested
372
+ if args.key_path:
373
+ value = self.config_manager.get_config_value(args.key_path)
374
+ if value is None:
375
+ self.console.print(f"āŒ Configuration key '{args.key_path}' not found.")
376
+ sys.exit(1)
377
+ config = {args.key_path: value}
378
+
379
+ # Display configuration
380
+ if args.format == "json":
381
+ self.console.print(json.dumps(config, indent=2))
382
+ elif args.format == "yaml":
383
+ yaml_output = yaml.dump(config, default_flow_style=False, indent=2)
384
+ syntax = Syntax(yaml_output, "yaml", theme="monokai", line_numbers=True)
385
+ self.console.print(syntax)
386
+ elif args.format == "table":
387
+ self._display_config_table(config)
388
+
389
+ # Show sources if requested
390
+ if args.sources:
391
+ sources = self.config_manager.get_config_sources()
392
+ self.console.print(f"\nšŸ“‹ Configuration sources: {', '.join(sources)}")
393
+
394
+ except Exception as e:
395
+ self.console.print(f"āŒ Failed to show configuration: {e}")
396
+ sys.exit(1)
397
+
398
+ def set_config(self, args) -> None:
399
+ """
400
+ Set configuration value.
401
+
402
+ Args:
403
+ args: Command line arguments
404
+ """
405
+ config_file = Path(args.config_file)
406
+
407
+ # Create config file if requested and doesn't exist
408
+ if not config_file.exists():
409
+ if args.create:
410
+ config_data = self.config_manager._get_default_config()
411
+ else:
412
+ self.console.print(f"āŒ Configuration file {config_file} not found. Use --create to create it.")
413
+ sys.exit(1)
414
+ else:
415
+ config_data = self.config_manager._load_config_file(config_file)
416
+
417
+ # Parse value (try JSON first, then string)
418
+ try:
419
+ value = json.loads(args.value)
420
+ except json.JSONDecodeError:
421
+ value = args.value
422
+
423
+ # Set the value
424
+ keys = args.key_path.split('.')
425
+ current = config_data
426
+ for key in keys[:-1]:
427
+ if key not in current:
428
+ current[key] = {}
429
+ current = current[key]
430
+ current[keys[-1]] = value
431
+
432
+ try:
433
+ # Save configuration
434
+ self.config_manager.safe_update_config(config_file, config_data)
435
+ self.console.print(f"āœ… Configuration updated: {args.key_path} = {value}")
436
+
437
+ except Exception as e:
438
+ self.console.print(f"āŒ Failed to update configuration: {e}")
439
+ sys.exit(1)
440
+
441
+ def get_config(self, args) -> None:
442
+ """
443
+ Get configuration value.
444
+
445
+ Args:
446
+ args: Command line arguments
447
+ """
448
+ try:
449
+ # Load configuration
450
+ self.config_manager.load_config()
451
+
452
+ # Get value
453
+ value = self.config_manager.get_config_value(args.key_path, args.default)
454
+
455
+ if value is None:
456
+ self.console.print(f"āŒ Configuration key '{args.key_path}' not found.")
457
+ sys.exit(1)
458
+
459
+ # Format output
460
+ if args.format == "json":
461
+ self.console.print(json.dumps(value, indent=2))
462
+ elif args.format == "yaml":
463
+ yaml_output = yaml.dump({args.key_path: value}, default_flow_style=False)
464
+ self.console.print(yaml_output.strip())
465
+ else:
466
+ self.console.print(str(value))
467
+
468
+ except Exception as e:
469
+ self.console.print(f"āŒ Failed to get configuration: {e}")
470
+ sys.exit(1)
471
+
472
+ def _get_template_config(self, template: str) -> Dict[str, Any]:
473
+ """Get configuration template."""
474
+ base_config = self.config_manager._get_default_config()
475
+
476
+ if template == "minimal":
477
+ return {
478
+ "version": base_config["version"],
479
+ "logging": base_config["logging"],
480
+ "security": base_config["security"],
481
+ }
482
+ elif template == "aws":
483
+ return {
484
+ "version": base_config["version"],
485
+ "logging": base_config["logging"],
486
+ "aws": base_config["aws"],
487
+ "security": base_config["security"],
488
+ }
489
+ elif template == "azure":
490
+ return {
491
+ "version": base_config["version"],
492
+ "logging": base_config["logging"],
493
+ "azure": base_config["azure"],
494
+ "security": base_config["security"],
495
+ }
496
+ elif template == "gcp":
497
+ return {
498
+ "version": base_config["version"],
499
+ "logging": base_config["logging"],
500
+ "gcp": base_config["gcp"],
501
+ "security": base_config["security"],
502
+ }
503
+ elif template == "multi-cloud":
504
+ return base_config
505
+ else:
506
+ return base_config
507
+
508
+ def _interactive_config_setup(self, config: Dict[str, Any], template: str) -> Dict[str, Any]:
509
+ """Interactive configuration setup."""
510
+ self.console.print(f"\nšŸ”§ Interactive setup for {template} template:")
511
+
512
+ if template in ["aws", "multi-cloud"]:
513
+ accounts = Prompt.ask("AWS Account IDs (comma-separated)", default="")
514
+ if accounts:
515
+ config["aws"]["accounts"] = [acc.strip() for acc in accounts.split(",")]
516
+
517
+ regions = Prompt.ask("AWS Regions (comma-separated)", default="ap-northeast-2")
518
+ config["aws"]["regions"] = [reg.strip() for reg in regions.split(",")]
519
+
520
+ if template in ["azure", "multi-cloud"]:
521
+ subscription_id = Prompt.ask("Azure Subscription ID", default="")
522
+ if subscription_id:
523
+ config["azure"]["subscription_id"] = subscription_id
524
+
525
+ if template in ["gcp", "multi-cloud"]:
526
+ project_id = Prompt.ask("GCP Project ID", default="")
527
+ if project_id:
528
+ config["gcp"]["project_id"] = project_id
529
+
530
+ return config
531
+
532
+ def _create_env_example(self, env_example_path: Path, template: str) -> None:
533
+ """Create .env.example file."""
534
+ env_content = [
535
+ "# IC Configuration Environment Variables",
536
+ "# Copy this file to .env and fill in your actual values",
537
+ "# DO NOT commit .env to version control!",
538
+ "",
539
+ "# Logging Configuration",
540
+ "# IC_LOG_LEVEL=ERROR",
541
+ "# IC_LOG_FILE_LEVEL=INFO",
542
+ "",
543
+ ]
544
+
545
+ if template in ["aws", "multi-cloud"]:
546
+ env_content.extend([
547
+ "# AWS Configuration",
548
+ "# AWS_PROFILE=your-profile-name",
549
+ "# AWS_ACCOUNTS=123456789012,987654321098",
550
+ "# AWS_REGIONS=ap-northeast-2,us-east-1",
551
+ "# AWS_CROSS_ACCOUNT_ROLE=OrganizationAccountAccessRole",
552
+ "",
553
+ ])
554
+
555
+ if template in ["azure", "multi-cloud"]:
556
+ env_content.extend([
557
+ "# Azure Configuration",
558
+ "# AZURE_SUBSCRIPTION_ID=your-subscription-id",
559
+ "# AZURE_TENANT_ID=your-tenant-id",
560
+ "# AZURE_CLIENT_ID=your-client-id",
561
+ "# AZURE_CLIENT_SECRET=your-client-secret",
562
+ "",
563
+ ])
564
+
565
+ if template in ["gcp", "multi-cloud"]:
566
+ env_content.extend([
567
+ "# GCP Configuration",
568
+ "# GCP_PROJECT_ID=your-project-id",
569
+ "# GCP_SERVICE_ACCOUNT_KEY_PATH=/path/to/service-account.json",
570
+ "# GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json",
571
+ "",
572
+ ])
573
+
574
+ env_content.extend([
575
+ "# Optional: Slack Integration",
576
+ "# SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...",
577
+ "",
578
+ "# Optional: MCP GitHub Integration",
579
+ "# MCP_GITHUB_TOKEN=your-github-token",
580
+ ])
581
+
582
+ with open(env_example_path, 'w') as f:
583
+ f.write('\n'.join(env_content))
584
+
585
+ def _update_gitignore(self) -> None:
586
+ """Update .gitignore with security entries."""
587
+ gitignore_path = Path(".gitignore")
588
+ security_entries = self.security_manager.create_gitignore_entries()
589
+
590
+ existing_content = ""
591
+ if gitignore_path.exists():
592
+ with open(gitignore_path, 'r') as f:
593
+ existing_content = f.read()
594
+
595
+ # Add security entries if not already present
596
+ new_entries = []
597
+ for entry in security_entries:
598
+ if entry not in existing_content:
599
+ new_entries.append(entry)
600
+
601
+ if new_entries:
602
+ with open(gitignore_path, 'a') as f:
603
+ if existing_content and not existing_content.endswith('\n'):
604
+ f.write('\n')
605
+ f.write('\n'.join(new_entries) + '\n')
606
+
607
+ def _find_config_files(self) -> List[Path]:
608
+ """Find configuration files in common locations."""
609
+ config_files = []
610
+
611
+ # Check common config file locations
612
+ possible_paths = [
613
+ Path("ic.yaml"),
614
+ Path("ic.yml"),
615
+ Path(".ic/config.yaml"),
616
+ Path(".ic/config.yml"),
617
+ Path("config/config.yaml"),
618
+ Path("config/config.yml"),
619
+ Path.home() / ".ic" / "config.yaml",
620
+ ]
621
+
622
+ for path in possible_paths:
623
+ if path.exists():
624
+ config_files.append(path)
625
+
626
+ return config_files
627
+
628
+ def _display_migration_preview(self, result: Dict[str, Any]) -> None:
629
+ """Display migration preview."""
630
+ if result.get("config_data"):
631
+ self.console.print("šŸ“‹ Configuration that would be created:")
632
+ yaml_output = yaml.dump(result["config_data"], default_flow_style=False, indent=2)
633
+ syntax = Syntax(yaml_output, "yaml", theme="monokai")
634
+ self.console.print(syntax)
635
+
636
+ if result.get("warnings"):
637
+ self.console.print("\nāš ļø Warnings:")
638
+ for warning in result["warnings"]:
639
+ self.console.print(f" • {warning}")
640
+
641
+ def _display_migration_result(self, result: Dict[str, Any]) -> None:
642
+ """Display migration result."""
643
+ if result.get("success"):
644
+ self.console.print(Panel(
645
+ f"āœ… Migration completed successfully!\n\n"
646
+ f"šŸ“ Configuration file: {result.get('output_file', 'ic.yaml')}\n"
647
+ f"šŸ“„ Backup created: {result.get('backup_file', 'N/A')}\n\n"
648
+ f"Next steps:\n"
649
+ f"1. Review the generated configuration file\n"
650
+ f"2. Remove sensitive data from the config file\n"
651
+ f"3. Set up environment variables for secrets\n"
652
+ f"4. Run 'ic config validate' to verify setup",
653
+ title="Migration Complete",
654
+ border_style="green"
655
+ ))
656
+ else:
657
+ self.console.print(f"āŒ Migration failed: {result.get('error', 'Unknown error')}")
658
+
659
+ if result.get("warnings"):
660
+ self.console.print("\nāš ļø Warnings:")
661
+ for warning in result["warnings"]:
662
+ self.console.print(f" • {warning}")
663
+
664
+ def _display_config_summary(self, config_data: Dict[str, Any]) -> None:
665
+ """Display configuration summary."""
666
+ table = Table(title="Configuration Summary")
667
+ table.add_column("Section", style="cyan")
668
+ table.add_column("Keys", style="green")
669
+ table.add_column("Status", style="yellow")
670
+
671
+ for section, data in config_data.items():
672
+ if isinstance(data, dict):
673
+ keys = list(data.keys())
674
+ status = "āœ… Configured" if keys else "āš ļø Empty"
675
+ table.add_row(section, ", ".join(keys[:3]) + ("..." if len(keys) > 3 else ""), status)
676
+
677
+ self.console.print(table)
678
+
679
+ def _display_config_table(self, config: Dict[str, Any], prefix: str = "") -> None:
680
+ """Display configuration as table."""
681
+ table = Table(title="Configuration")
682
+ table.add_column("Key", style="cyan")
683
+ table.add_column("Value", style="green")
684
+ table.add_column("Type", style="yellow")
685
+
686
+ def add_rows(data: Dict[str, Any], current_prefix: str = ""):
687
+ for key, value in data.items():
688
+ full_key = f"{current_prefix}.{key}" if current_prefix else key
689
+
690
+ if isinstance(value, dict):
691
+ table.add_row(full_key, "[dict]", "object")
692
+ add_rows(value, full_key)
693
+ elif isinstance(value, list):
694
+ table.add_row(full_key, f"[{len(value)} items]", "array")
695
+ else:
696
+ table.add_row(full_key, str(value), type(value).__name__)
697
+
698
+ add_rows(config)
699
+ self.console.print(table)