cinchdb 0.1.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.
Files changed (68) hide show
  1. cinchdb/__init__.py +7 -0
  2. cinchdb/__main__.py +6 -0
  3. cinchdb/api/__init__.py +5 -0
  4. cinchdb/api/app.py +76 -0
  5. cinchdb/api/auth.py +290 -0
  6. cinchdb/api/main.py +137 -0
  7. cinchdb/api/routers/__init__.py +25 -0
  8. cinchdb/api/routers/auth.py +135 -0
  9. cinchdb/api/routers/branches.py +368 -0
  10. cinchdb/api/routers/codegen.py +164 -0
  11. cinchdb/api/routers/columns.py +290 -0
  12. cinchdb/api/routers/data.py +479 -0
  13. cinchdb/api/routers/databases.py +177 -0
  14. cinchdb/api/routers/projects.py +133 -0
  15. cinchdb/api/routers/query.py +156 -0
  16. cinchdb/api/routers/tables.py +349 -0
  17. cinchdb/api/routers/tenants.py +216 -0
  18. cinchdb/api/routers/views.py +219 -0
  19. cinchdb/cli/__init__.py +0 -0
  20. cinchdb/cli/commands/__init__.py +1 -0
  21. cinchdb/cli/commands/branch.py +479 -0
  22. cinchdb/cli/commands/codegen.py +176 -0
  23. cinchdb/cli/commands/column.py +308 -0
  24. cinchdb/cli/commands/database.py +212 -0
  25. cinchdb/cli/commands/query.py +136 -0
  26. cinchdb/cli/commands/remote.py +144 -0
  27. cinchdb/cli/commands/table.py +289 -0
  28. cinchdb/cli/commands/tenant.py +173 -0
  29. cinchdb/cli/commands/view.py +189 -0
  30. cinchdb/cli/handlers/__init__.py +5 -0
  31. cinchdb/cli/handlers/codegen_handler.py +189 -0
  32. cinchdb/cli/main.py +137 -0
  33. cinchdb/cli/utils.py +182 -0
  34. cinchdb/config.py +177 -0
  35. cinchdb/core/__init__.py +5 -0
  36. cinchdb/core/connection.py +175 -0
  37. cinchdb/core/database.py +537 -0
  38. cinchdb/core/maintenance.py +73 -0
  39. cinchdb/core/path_utils.py +153 -0
  40. cinchdb/managers/__init__.py +26 -0
  41. cinchdb/managers/branch.py +167 -0
  42. cinchdb/managers/change_applier.py +414 -0
  43. cinchdb/managers/change_comparator.py +194 -0
  44. cinchdb/managers/change_tracker.py +182 -0
  45. cinchdb/managers/codegen.py +523 -0
  46. cinchdb/managers/column.py +579 -0
  47. cinchdb/managers/data.py +455 -0
  48. cinchdb/managers/merge_manager.py +429 -0
  49. cinchdb/managers/query.py +214 -0
  50. cinchdb/managers/table.py +383 -0
  51. cinchdb/managers/tenant.py +258 -0
  52. cinchdb/managers/view.py +252 -0
  53. cinchdb/models/__init__.py +27 -0
  54. cinchdb/models/base.py +44 -0
  55. cinchdb/models/branch.py +26 -0
  56. cinchdb/models/change.py +47 -0
  57. cinchdb/models/database.py +20 -0
  58. cinchdb/models/project.py +20 -0
  59. cinchdb/models/table.py +86 -0
  60. cinchdb/models/tenant.py +19 -0
  61. cinchdb/models/view.py +15 -0
  62. cinchdb/utils/__init__.py +15 -0
  63. cinchdb/utils/sql_validator.py +137 -0
  64. cinchdb-0.1.0.dist-info/METADATA +195 -0
  65. cinchdb-0.1.0.dist-info/RECORD +68 -0
  66. cinchdb-0.1.0.dist-info/WHEEL +4 -0
  67. cinchdb-0.1.0.dist-info/entry_points.txt +3 -0
  68. cinchdb-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,189 @@
1
+ """View management commands for CinchDB CLI."""
2
+
3
+ import typer
4
+ from typing import Optional
5
+ from rich.console import Console
6
+ from rich.table import Table as RichTable
7
+
8
+ from cinchdb.managers.view import ViewModel
9
+ from cinchdb.managers.change_applier import ChangeApplier
10
+ from cinchdb.cli.utils import get_config_with_data, validate_required_arg
11
+
12
+ app = typer.Typer(help="View management commands", invoke_without_command=True)
13
+ console = Console()
14
+
15
+
16
+ @app.callback()
17
+ def callback(ctx: typer.Context):
18
+ """Show help when no subcommand is provided."""
19
+ if ctx.invoked_subcommand is None:
20
+ console.print(ctx.get_help())
21
+ raise typer.Exit(0)
22
+
23
+
24
+ @app.command(name="list")
25
+ def list_views():
26
+ """List all views in the current branch."""
27
+ config, config_data = get_config_with_data()
28
+ db_name = config_data.active_database
29
+ branch_name = config_data.active_branch
30
+
31
+ view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
32
+ views = view_mgr.list_views()
33
+
34
+ if not views:
35
+ console.print("[yellow]No views found[/yellow]")
36
+ return
37
+
38
+ # Create a table
39
+ table = RichTable(
40
+ title=f"Views db={db_name} branch={branch_name}", title_justify="left"
41
+ )
42
+ table.add_column("Name", style="cyan")
43
+ table.add_column("SQL Length", style="green")
44
+ table.add_column("Created", style="yellow")
45
+
46
+ for view in views:
47
+ sql_length = len(view.sql_statement) if view.sql_statement else 0
48
+ table.add_row(view.name, str(sql_length), "-")
49
+
50
+ console.print(table)
51
+
52
+
53
+ @app.command()
54
+ def create(
55
+ ctx: typer.Context,
56
+ name: Optional[str] = typer.Argument(None, help="Name of the view"),
57
+ sql: Optional[str] = typer.Argument(None, help="SQL query for the view"),
58
+ apply: bool = typer.Option(
59
+ True, "--apply/--no-apply", help="Apply changes to all tenants"
60
+ ),
61
+ ):
62
+ """Create a new view.
63
+
64
+ Examples:
65
+ cinch view create active_users "SELECT * FROM users WHERE is_active = 1"
66
+ cinch view create user_stats "SELECT age, COUNT(*) as count FROM users GROUP BY age"
67
+ """
68
+ name = validate_required_arg(name, "name", ctx)
69
+ sql = validate_required_arg(sql, "sql", ctx)
70
+ config, config_data = get_config_with_data()
71
+ db_name = config_data.active_database
72
+ branch_name = config_data.active_branch
73
+
74
+ try:
75
+ view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
76
+ view_mgr.create_view(name, sql)
77
+ console.print(f"[green]✅ Created view '{name}'[/green]")
78
+
79
+ if apply:
80
+ # Apply to all tenants
81
+ applier = ChangeApplier(config.project_dir, db_name, branch_name)
82
+ applied = applier.apply_all_unapplied()
83
+ if applied > 0:
84
+ console.print("[green]✅ Applied changes to all tenants[/green]")
85
+
86
+ except ValueError as e:
87
+ console.print(f"[red]❌ {e}[/red]")
88
+ raise typer.Exit(1)
89
+
90
+
91
+ @app.command()
92
+ def update(
93
+ ctx: typer.Context,
94
+ name: Optional[str] = typer.Argument(None, help="Name of the view to update"),
95
+ sql: Optional[str] = typer.Argument(None, help="New SQL query for the view"),
96
+ apply: bool = typer.Option(
97
+ True, "--apply/--no-apply", help="Apply changes to all tenants"
98
+ ),
99
+ ):
100
+ """Update an existing view's SQL."""
101
+ name = validate_required_arg(name, "name", ctx)
102
+ sql = validate_required_arg(sql, "sql", ctx)
103
+ config, config_data = get_config_with_data()
104
+ db_name = config_data.active_database
105
+ branch_name = config_data.active_branch
106
+
107
+ try:
108
+ view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
109
+ view_mgr.update_view(name, sql)
110
+ console.print(f"[green]✅ Updated view '{name}'[/green]")
111
+
112
+ if apply:
113
+ # Apply to all tenants
114
+ applier = ChangeApplier(config.project_dir, db_name, branch_name)
115
+ applied = applier.apply_all_unapplied()
116
+ if applied > 0:
117
+ console.print("[green]✅ Applied changes to all tenants[/green]")
118
+
119
+ except ValueError as e:
120
+ console.print(f"[red]❌ {e}[/red]")
121
+ raise typer.Exit(1)
122
+
123
+
124
+ @app.command()
125
+ def delete(
126
+ ctx: typer.Context,
127
+ name: Optional[str] = typer.Argument(None, help="Name of the view to delete"),
128
+ force: bool = typer.Option(
129
+ False, "--force", "-f", help="Force deletion without confirmation"
130
+ ),
131
+ apply: bool = typer.Option(
132
+ True, "--apply/--no-apply", help="Apply changes to all tenants"
133
+ ),
134
+ ):
135
+ """Delete a view."""
136
+ name = validate_required_arg(name, "name", ctx)
137
+ config, config_data = get_config_with_data()
138
+ db_name = config_data.active_database
139
+ branch_name = config_data.active_branch
140
+
141
+ # Confirmation
142
+ if not force:
143
+ confirm = typer.confirm(f"Are you sure you want to delete view '{name}'?")
144
+ if not confirm:
145
+ console.print("[yellow]Cancelled[/yellow]")
146
+ raise typer.Exit(0)
147
+
148
+ try:
149
+ view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
150
+ view_mgr.delete_view(name)
151
+ console.print(f"[green]✅ Deleted view '{name}'[/green]")
152
+
153
+ if apply:
154
+ # Apply to all tenants
155
+ applier = ChangeApplier(config.project_dir, db_name, branch_name)
156
+ applied = applier.apply_all_unapplied()
157
+ if applied > 0:
158
+ console.print("[green]✅ Applied changes to all tenants[/green]")
159
+
160
+ except ValueError as e:
161
+ console.print(f"[red]❌ {e}[/red]")
162
+ raise typer.Exit(1)
163
+
164
+
165
+ @app.command()
166
+ def info(
167
+ ctx: typer.Context, name: Optional[str] = typer.Argument(None, help="View name")
168
+ ):
169
+ """Show detailed information about a view."""
170
+ name = validate_required_arg(name, "name", ctx)
171
+ config, config_data = get_config_with_data()
172
+ db_name = config_data.active_database
173
+ branch_name = config_data.active_branch
174
+
175
+ try:
176
+ view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
177
+ view = view_mgr.get_view(name)
178
+
179
+ # Display info
180
+ console.print(f"\n[bold]View: {view.name}[/bold]")
181
+ console.print(f"Database: {db_name}")
182
+ console.print(f"Branch: {branch_name}")
183
+ console.print("Tenant: main")
184
+ console.print("\n[bold]SQL:[/bold]")
185
+ console.print(view.sql_statement)
186
+
187
+ except ValueError as e:
188
+ console.print(f"[red]❌ {e}[/red]")
189
+ raise typer.Exit(1)
@@ -0,0 +1,5 @@
1
+ """CLI handlers for unified local/remote operations."""
2
+
3
+ from .codegen_handler import CodegenHandler
4
+
5
+ __all__ = ["CodegenHandler"]
@@ -0,0 +1,189 @@
1
+ """Unified codegen handler for local and remote operations."""
2
+
3
+ import requests
4
+ from pathlib import Path
5
+ from typing import Dict, Any, Optional, List
6
+ from rich.console import Console
7
+
8
+ from ...managers import CodegenManager
9
+
10
+ console = Console()
11
+
12
+
13
+ class CodegenHandler:
14
+ """Handles both local and remote code generation operations."""
15
+
16
+ def __init__(
17
+ self,
18
+ config_data: Dict[str, Any],
19
+ api_url: Optional[str] = None,
20
+ api_key: Optional[str] = None,
21
+ force_local: bool = False,
22
+ ):
23
+ """Initialize the handler.
24
+
25
+ Args:
26
+ config_data: Configuration data from config.toml
27
+ api_url: Optional API URL for remote generation
28
+ api_key: Optional API key for remote generation
29
+ force_local: Force local generation even if API configured
30
+ """
31
+ self.config_data = config_data
32
+ self.force_local = force_local
33
+
34
+ # Determine if we should use remote API
35
+ self.api_url = api_url or config_data.get("api", {}).get("url")
36
+ self.api_key = api_key or config_data.get("api", {}).get("key")
37
+ self.is_remote = bool(self.api_url and self.api_key and not force_local)
38
+
39
+ def generate_models(
40
+ self,
41
+ language: str,
42
+ output_dir: Path,
43
+ database: str,
44
+ branch: str,
45
+ tenant: str = "main",
46
+ include_tables: bool = True,
47
+ include_views: bool = True,
48
+ project_root: Optional[Path] = None,
49
+ ) -> Dict[str, Any]:
50
+ """Generate models using local or remote approach.
51
+
52
+ Returns:
53
+ Dict with generation results in consistent format
54
+ """
55
+ if self.is_remote:
56
+ return self._generate_remote(
57
+ language=language,
58
+ output_dir=output_dir,
59
+ database=database,
60
+ branch=branch,
61
+ tenant=tenant,
62
+ include_tables=include_tables,
63
+ include_views=include_views,
64
+ )
65
+ else:
66
+ return self._generate_local(
67
+ language=language,
68
+ output_dir=output_dir,
69
+ database=database,
70
+ branch=branch,
71
+ tenant=tenant,
72
+ include_tables=include_tables,
73
+ include_views=include_views,
74
+ project_root=project_root,
75
+ )
76
+
77
+ def _generate_local(
78
+ self,
79
+ language: str,
80
+ output_dir: Path,
81
+ database: str,
82
+ branch: str,
83
+ tenant: str = "main",
84
+ include_tables: bool = True,
85
+ include_views: bool = True,
86
+ project_root: Optional[Path] = None,
87
+ ) -> Dict[str, Any]:
88
+ """Generate models locally using CodegenManager."""
89
+ if not project_root:
90
+ raise ValueError("project_root is required for local generation")
91
+
92
+ manager = CodegenManager(
93
+ project_root=project_root, database=database, branch=branch, tenant=tenant
94
+ )
95
+
96
+ return manager.generate_models(
97
+ language=language,
98
+ output_dir=output_dir,
99
+ include_tables=include_tables,
100
+ include_views=include_views,
101
+ )
102
+
103
+ def _generate_remote(
104
+ self,
105
+ language: str,
106
+ output_dir: Path,
107
+ database: str,
108
+ branch: str,
109
+ tenant: str = "main",
110
+ include_tables: bool = True,
111
+ include_views: bool = True,
112
+ ) -> Dict[str, Any]:
113
+ """Generate models remotely using API."""
114
+ try:
115
+ # Prepare request payload
116
+ payload = {
117
+ "language": language,
118
+ "include_tables": include_tables,
119
+ "include_views": include_views,
120
+ }
121
+
122
+ # Prepare query parameters - database and branch required, tenant not needed for codegen
123
+ params = {"database": database, "branch": branch}
124
+
125
+ # Make API request to generate files endpoint (returns JSON content)
126
+ response = requests.post(
127
+ f"{self.api_url}/api/v1/codegen/generate/files",
128
+ json=payload,
129
+ params=params,
130
+ headers={"Authorization": f"Bearer {self.api_key}"},
131
+ )
132
+ response.raise_for_status()
133
+
134
+ # Parse response
135
+ result = response.json()
136
+ files_data = result["files"]
137
+
138
+ # Create output directory
139
+ output_dir.mkdir(parents=True, exist_ok=True)
140
+
141
+ # Write files to local filesystem
142
+ files_generated = []
143
+ for file_info in files_data:
144
+ file_path = output_dir / file_info["filename"]
145
+ file_path.write_text(file_info["content"])
146
+ files_generated.append(file_info["filename"])
147
+
148
+ # Return consistent format matching local generation
149
+ return {
150
+ "files_generated": files_generated,
151
+ "tables_processed": result.get("tables_processed", []),
152
+ "views_processed": result.get("views_processed", []),
153
+ "output_dir": str(output_dir),
154
+ "language": language,
155
+ "remote": True,
156
+ }
157
+
158
+ except requests.RequestException as e:
159
+ raise RuntimeError(f"Remote codegen failed: {e}")
160
+ except KeyError as e:
161
+ raise RuntimeError(f"Invalid API response format: missing {e}")
162
+
163
+ def get_supported_languages(self, project_root: Optional[Path] = None) -> List[str]:
164
+ """Get supported languages from local or remote source."""
165
+ if self.is_remote:
166
+ try:
167
+ response = requests.get(
168
+ f"{self.api_url}/api/v1/codegen/languages",
169
+ headers={"Authorization": f"Bearer {self.api_key}"},
170
+ )
171
+ response.raise_for_status()
172
+ result = response.json()
173
+ return [lang["name"] for lang in result["languages"]]
174
+ except requests.RequestException:
175
+ # Fall back to local if remote fails
176
+ pass
177
+
178
+ # Use local manager for supported languages
179
+ if not project_root:
180
+ # Return hardcoded list if no project available
181
+ return ["python"]
182
+
183
+ manager = CodegenManager(
184
+ project_root=project_root,
185
+ database=self.config_data.get("active_database", "main"),
186
+ branch=self.config_data.get("active_branch", "main"),
187
+ tenant="main",
188
+ )
189
+ return manager.get_supported_languages()
cinchdb/cli/main.py ADDED
@@ -0,0 +1,137 @@
1
+ """Main CLI entry point for CinchDB."""
2
+
3
+ import typer
4
+ from typing import Optional
5
+ from pathlib import Path
6
+
7
+ # Import command groups
8
+ from cinchdb.cli.commands import (
9
+ database,
10
+ branch,
11
+ tenant,
12
+ table,
13
+ column,
14
+ view,
15
+ codegen,
16
+ remote,
17
+ )
18
+
19
+ app = typer.Typer(
20
+ name="cinch",
21
+ help="CinchDB - A Git-like SQLite database management system",
22
+ add_completion=False,
23
+ invoke_without_command=True,
24
+ )
25
+
26
+
27
+ @app.callback()
28
+ def main(ctx: typer.Context):
29
+ """
30
+ CinchDB - A Git-like SQLite database management system
31
+ """
32
+ if ctx.invoked_subcommand is None:
33
+ # No subcommand was invoked, show help
34
+ print(ctx.get_help())
35
+ raise typer.Exit(0)
36
+
37
+
38
+ # Add command groups
39
+ app.add_typer(database.app, name="db", help="Database management commands")
40
+ app.add_typer(branch.app, name="branch", help="Branch management commands")
41
+ app.add_typer(tenant.app, name="tenant", help="Tenant management commands")
42
+ app.add_typer(table.app, name="table", help="Table management commands")
43
+ app.add_typer(column.app, name="column", help="Column management commands")
44
+ app.add_typer(view.app, name="view", help="View management commands")
45
+ app.add_typer(codegen.app, name="codegen", help="Code generation commands")
46
+ app.add_typer(remote.app, name="remote", help="Remote instance management")
47
+
48
+
49
+ # Add query as direct command instead of subtyper
50
+ @app.command()
51
+ def query(
52
+ sql: str = typer.Argument(..., help="SQL query to execute"),
53
+ tenant: Optional[str] = typer.Option("main", "--tenant", "-t", help="Tenant name"),
54
+ format: Optional[str] = typer.Option(
55
+ "table", "--format", "-f", help="Output format (table, json, csv)"
56
+ ),
57
+ limit: Optional[int] = typer.Option(
58
+ None, "--limit", "-l", help="Limit number of rows"
59
+ ),
60
+ local: bool = typer.Option(False, "--local", "-L", help="Force local connection"),
61
+ remote: Optional[str] = typer.Option(
62
+ None, "--remote", "-r", help="Use specific remote alias"
63
+ ),
64
+ ):
65
+ """Execute a SQL query."""
66
+ from cinchdb.cli.commands.query import execute_query
67
+
68
+ execute_query(sql, tenant, format, limit, force_local=local, remote_alias=remote)
69
+
70
+
71
+ @app.command()
72
+ def init(
73
+ path: Optional[Path] = typer.Argument(
74
+ None, help="Directory to initialize project in (default: current directory)"
75
+ ),
76
+ ):
77
+ """Initialize a new CinchDB project."""
78
+ from cinchdb.config import Config
79
+
80
+ project_path = path or Path.cwd()
81
+
82
+ try:
83
+ config = Config(project_path)
84
+ config.init_project()
85
+ typer.secho(
86
+ f"✅ Initialized CinchDB project in {project_path}", fg=typer.colors.GREEN
87
+ )
88
+ except FileExistsError:
89
+ typer.secho(f"❌ Project already exists in {project_path}", fg=typer.colors.RED)
90
+ raise typer.Exit(1)
91
+
92
+
93
+ @app.command()
94
+ def version():
95
+ """Show CinchDB version."""
96
+ from cinchdb import __version__
97
+
98
+ typer.echo(f"CinchDB version {__version__}")
99
+
100
+
101
+ @app.command()
102
+ def status():
103
+ """Show CinchDB status including configuration and environment variables."""
104
+ from cinchdb.cli.utils import get_config_with_data, show_env_config
105
+ from rich.console import Console
106
+ from rich.table import Table as RichTable
107
+
108
+ console = Console()
109
+
110
+ # Show project configuration
111
+ try:
112
+ config, config_data = get_config_with_data()
113
+
114
+ console.print("\n[bold]CinchDB Status[/bold]")
115
+ console.print(f"Project: {config.project_dir}")
116
+ console.print(f"Active Database: {config_data.active_database}")
117
+ console.print(f"Active Branch: {config_data.active_branch}")
118
+
119
+ if config_data.active_remote:
120
+ console.print(f"Active Remote: {config_data.active_remote}")
121
+ if config_data.active_remote in config_data.remotes:
122
+ remote = config_data.remotes[config_data.active_remote]
123
+ console.print(f" URL: {remote.url}")
124
+ console.print(f" Key: ***{remote.key[-8:] if len(remote.key) > 8 else '*' * len(remote.key)}")
125
+ else:
126
+ console.print("Active Remote: [dim]None (local mode)[/dim]")
127
+
128
+ # Show environment variables
129
+ show_env_config()
130
+
131
+ except Exception as e:
132
+ console.print(f"[red]❌ Error: {e}[/red]")
133
+ raise typer.Exit(1)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ app()
cinchdb/cli/utils.py ADDED
@@ -0,0 +1,182 @@
1
+ """Utility functions for CLI commands."""
2
+
3
+ import os
4
+ import typer
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from rich.console import Console
8
+
9
+ from cinchdb.config import Config
10
+ from cinchdb.core.path_utils import get_project_root
11
+ from cinchdb.core.database import CinchDB
12
+
13
+ console = Console()
14
+
15
+
16
+ def get_config_with_data():
17
+ """Get config and load data from current directory.
18
+
19
+ Returns:
20
+ tuple: (config, config_data)
21
+ """
22
+ project_root = get_project_root(Path.cwd())
23
+ if not project_root:
24
+ console.print("[red]❌ Not in a CinchDB project directory[/red]")
25
+ raise typer.Exit(1)
26
+
27
+ config = Config(project_root)
28
+ try:
29
+ config_data = config.load()
30
+ except FileNotFoundError:
31
+ console.print("[red]❌ Config file not found. Run 'cinch init' first.[/red]")
32
+ raise typer.Exit(1)
33
+
34
+ return config, config_data
35
+
36
+
37
+ def get_config_dict():
38
+ """Get config data as dictionary, including API configuration if present.
39
+
40
+ Returns:
41
+ dict: Configuration data
42
+ """
43
+ config, config_data = get_config_with_data()
44
+
45
+ # Convert config_data to dict-like structure for handlers
46
+ config_dict = {
47
+ "active_database": getattr(config_data, "active_database", None),
48
+ "active_branch": getattr(config_data, "active_branch", "main"),
49
+ "project_root": config.project_dir,
50
+ }
51
+
52
+ # Add API configuration if present in raw config
53
+ if hasattr(config_data, "api") and config_data.api:
54
+ config_dict["api"] = {
55
+ "url": getattr(config_data.api, "url", None),
56
+ "key": getattr(config_data.api, "key", None),
57
+ }
58
+
59
+ return config_dict
60
+
61
+
62
+ def set_active_database(config: Config, database: str):
63
+ """Set the active database in config."""
64
+ config_data = config.load()
65
+ config_data.active_database = database
66
+ config.save(config_data)
67
+
68
+
69
+ def set_active_branch(config: Config, branch: str):
70
+ """Set the active branch in config."""
71
+ config_data = config.load()
72
+ config_data.active_branch = branch
73
+ config.save(config_data)
74
+
75
+
76
+ def validate_required_arg(
77
+ value: Optional[str], arg_name: str, ctx: typer.Context
78
+ ) -> str:
79
+ """Validate a required argument and show help if missing.
80
+
81
+ Args:
82
+ value: The argument value
83
+ arg_name: Name of the argument (for error message)
84
+ ctx: Typer context
85
+
86
+ Returns:
87
+ The validated value
88
+
89
+ Raises:
90
+ typer.Exit: If value is None
91
+ """
92
+ if value is None:
93
+ console.print(ctx.get_help())
94
+ console.print(f"\n[red]❌ Error: Missing argument '{arg_name.upper()}'.[/red]")
95
+ raise typer.Exit(1)
96
+ return value
97
+
98
+
99
+ def get_cinchdb_instance(
100
+ database: Optional[str] = None,
101
+ branch: Optional[str] = None,
102
+ tenant: str = "main",
103
+ force_local: bool = False,
104
+ remote_alias: Optional[str] = None,
105
+ ) -> CinchDB:
106
+ """Get a CinchDB instance configured for local or remote access.
107
+
108
+ Args:
109
+ database: Database name (uses active database if None)
110
+ branch: Branch name (uses active branch if None)
111
+ tenant: Tenant name (default: main)
112
+ force_local: Force local connection even if remote is configured
113
+ remote_alias: Use specific remote alias (overrides active remote)
114
+
115
+ Returns:
116
+ CinchDB instance
117
+
118
+ Raises:
119
+ typer.Exit: If configuration is invalid
120
+ """
121
+ config, config_data = get_config_with_data()
122
+
123
+ # Use provided or active database/branch
124
+ database = database or config_data.active_database
125
+ branch = branch or config_data.active_branch
126
+
127
+ # Determine if we should use remote connection
128
+ use_remote = False
129
+ remote_config = None
130
+
131
+ if not force_local:
132
+ if remote_alias:
133
+ # Use specific remote alias
134
+ if remote_alias not in config_data.remotes:
135
+ console.print(f"[red]❌ Remote '{remote_alias}' not found[/red]")
136
+ raise typer.Exit(1)
137
+ remote_config = config_data.remotes[remote_alias]
138
+ use_remote = True
139
+ elif config_data.active_remote:
140
+ # Use active remote
141
+ if config_data.active_remote not in config_data.remotes:
142
+ console.print(f"[red]❌ Active remote '{config_data.active_remote}' not found[/red]")
143
+ raise typer.Exit(1)
144
+ remote_config = config_data.remotes[config_data.active_remote]
145
+ use_remote = True
146
+
147
+ if use_remote and remote_config:
148
+ # Create remote connection
149
+ return CinchDB(
150
+ database=database,
151
+ branch=branch,
152
+ tenant=tenant,
153
+ api_url=remote_config.url,
154
+ api_key=remote_config.key,
155
+ )
156
+ else:
157
+ # Create local connection
158
+ return CinchDB(
159
+ database=database,
160
+ branch=branch,
161
+ tenant=tenant,
162
+ project_dir=config.project_dir,
163
+ )
164
+
165
+
166
+ def show_env_config():
167
+ """Display active environment variable configuration."""
168
+ env_vars = {
169
+ "CINCHDB_PROJECT_DIR": os.environ.get("CINCHDB_PROJECT_DIR"),
170
+ "CINCHDB_DATABASE": os.environ.get("CINCHDB_DATABASE"),
171
+ "CINCHDB_BRANCH": os.environ.get("CINCHDB_BRANCH"),
172
+ "CINCHDB_REMOTE_URL": os.environ.get("CINCHDB_REMOTE_URL"),
173
+ "CINCHDB_API_KEY": "***" if "CINCHDB_API_KEY" in os.environ else None,
174
+ }
175
+
176
+ active = {k: v for k, v in env_vars.items() if v}
177
+ if active:
178
+ console.print("\n[yellow]Active environment variables:[/yellow]")
179
+ for key, value in active.items():
180
+ console.print(f" {key}={value}")
181
+ else:
182
+ console.print("\n[dim]No CinchDB environment variables set[/dim]")