-
-
Notifications
You must be signed in to change notification settings - Fork 84
feat: one cmd development environment setup workflow in nixopus-cli #413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f1aa8eb
60d248b
c4e969f
7960c77
a761edb
736ad4c
feeaf1b
89edc89
0af43e8
4183793
c3dd19d
88f51f5
0b6a5b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,24 +1,84 @@ | ||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Dict, List | ||||||||||||||||||||||||||||||||||||||||
| import platform | ||||||||||||||||||||||||||||||||||||||||
| import shutil | ||||||||||||||||||||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||||||||||||||||||||
| from typing import List, Optional, Sequence | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| from app.utils.config import Config | ||||||||||||||||||||||||||||||||||||||||
| from app.utils.protocols import LoggerProtocol | ||||||||||||||||||||||||||||||||||||||||
| from app.utils.logger import Logger | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| from .messages import ports_unavailable | ||||||||||||||||||||||||||||||||||||||||
| from .port import PortConfig, PortService | ||||||||||||||||||||||||||||||||||||||||
| from .port import PortConfig, PortService, PortCheckResult | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| class PreflightRunner: | ||||||||||||||||||||||||||||||||||||||||
| """Centralized preflight check runner for port availability""" | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def __init__(self, logger: LoggerProtocol = None, verbose: bool = False): | ||||||||||||||||||||||||||||||||||||||||
| def __init__(self, logger: Optional[LoggerProtocol] = None, verbose: bool = False): | ||||||||||||||||||||||||||||||||||||||||
| self.logger = logger | ||||||||||||||||||||||||||||||||||||||||
| self.verbose = verbose | ||||||||||||||||||||||||||||||||||||||||
| self.config = Config() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def run_port_checks(self, ports: List[int], host: str = "localhost") -> List[Dict[str, Any]]: | ||||||||||||||||||||||||||||||||||||||||
| def _have(self, cmd: str) -> bool: | ||||||||||||||||||||||||||||||||||||||||
| return shutil.which(cmd) is not None | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def check_windows_environment(self) -> None: | ||||||||||||||||||||||||||||||||||||||||
| """On Windows hosts, verify Docker Desktop, WSL2 readiness. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| 1. Ensures docker CLI exists and Docker daemon is reachable. | ||||||||||||||||||||||||||||||||||||||||
| 2. Checks WSL presence and recommends WSL2 if not detected. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| if platform.system().lower() != "windows": | ||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if self.logger: | ||||||||||||||||||||||||||||||||||||||||
| self.logger.info("Running Windows preflight checks...") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # check Docker CLI | ||||||||||||||||||||||||||||||||||||||||
| if not self._have("docker"): | ||||||||||||||||||||||||||||||||||||||||
| raise Exception("Docker CLI not found on Windows. Please install Docker Desktop and ensure 'docker' is in PATH.") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # pin g Docker daemon | ||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| # quick daemon ping via 'docker info' | ||||||||||||||||||||||||||||||||||||||||
| subprocess.run(["docker", "info"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | ||||||||||||||||||||||||||||||||||||||||
| if self.logger: | ||||||||||||||||||||||||||||||||||||||||
| self.logger.success("Docker daemon is running.") | ||||||||||||||||||||||||||||||||||||||||
| except subprocess.CalledProcessError: | ||||||||||||||||||||||||||||||||||||||||
| raise Exception("Docker daemon is not running. Start Docker Desktop and retry.") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # WSL presence and version check (recommendation only) | ||||||||||||||||||||||||||||||||||||||||
| if self._have("wsl"): | ||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| result = subprocess.run( | ||||||||||||||||||||||||||||||||||||||||
| ["wsl", "-l", "-v"], | ||||||||||||||||||||||||||||||||||||||||
| check=False, | ||||||||||||||||||||||||||||||||||||||||
| stdout=subprocess.PIPE, | ||||||||||||||||||||||||||||||||||||||||
| stderr=subprocess.PIPE, | ||||||||||||||||||||||||||||||||||||||||
| text=True, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| output = result.stdout or "" | ||||||||||||||||||||||||||||||||||||||||
| # Look for any distro line containing version '2' | ||||||||||||||||||||||||||||||||||||||||
| has_wsl2 = any(" 2" in line or "\t2" in line for line in output.splitlines()) | ||||||||||||||||||||||||||||||||||||||||
| if has_wsl2: | ||||||||||||||||||||||||||||||||||||||||
| if self.logger: | ||||||||||||||||||||||||||||||||||||||||
| self.logger.success("WSL2 detected.") | ||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||
| if self.logger: | ||||||||||||||||||||||||||||||||||||||||
| self.logger.warning("WSL detected but no WSL2 distro found. Docker Desktop works best with WSL2.") | ||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||
| if self.logger: | ||||||||||||||||||||||||||||||||||||||||
| self.logger.warning("Unable to verify WSL version. Proceeding.") | ||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||
| if self.logger: | ||||||||||||||||||||||||||||||||||||||||
| self.logger.warning("WSL not found. Install WSL2 for the best Docker Desktop compatibility (optional).") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def run_port_checks(self, ports: List[int], host: str = "localhost") -> List[PortCheckResult]: | ||||||||||||||||||||||||||||||||||||||||
| """Run port availability checks and return results""" | ||||||||||||||||||||||||||||||||||||||||
| port_config = PortConfig(ports=ports, host=host, verbose=self.verbose) | ||||||||||||||||||||||||||||||||||||||||
| port_service = PortService(port_config, logger=self.logger) | ||||||||||||||||||||||||||||||||||||||||
| # Ensure a concrete logger instance is provided | ||||||||||||||||||||||||||||||||||||||||
| effective_logger = self.logger or Logger(verbose=self.verbose) | ||||||||||||||||||||||||||||||||||||||||
| port_service = PortService(port_config, logger=effective_logger) | ||||||||||||||||||||||||||||||||||||||||
| return port_service.check_ports() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def check_required_ports(self, ports: List[int], host: str = "localhost") -> None: | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,12 +91,14 @@ def check_required_ports(self, ports: List[int], host: str = "localhost") -> Non | |||||||||||||||||||||||||||||||||||||||
| raise Exception(error_msg) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def check_ports_from_config( | ||||||||||||||||||||||||||||||||||||||||
| self, config_key: str = "required_ports", user_config: dict = None, defaults: dict = None | ||||||||||||||||||||||||||||||||||||||||
| self, config_key: str = "required_ports", user_config: Optional[dict] = None, defaults: Optional[dict] = None | ||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||
| """Check ports using configuration values""" | ||||||||||||||||||||||||||||||||||||||||
| if user_config is not None and defaults is not None: | ||||||||||||||||||||||||||||||||||||||||
| ports = self.config.get_config_value(config_key, user_config, defaults) | ||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||
| ports = self.config.get_yaml_value("ports") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
93
to
101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Call Windows checks from config-driven path. Safe to always call; it no-ops on non-Windows. Ensures users running def check_ports_from_config(
self, config_key: str = "required_ports", user_config: Optional[dict] = None, defaults: Optional[dict] = None
) -> None:
"""Check ports using configuration values"""
+ # Run OS-specific checks (no-op on non-Windows)
+ self.check_windows_environment()
if user_config is not None and defaults is not None:
ports = self.config.get_config_value(config_key, user_config, defaults)
else:
ports = self.config.get_yaml_value("ports")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| self.check_required_ports(ports) | ||||||||||||||||||||||||||||||||||||||||
| if not isinstance(ports, list): | ||||||||||||||||||||||||||||||||||||||||
| raise Exception("Configured 'ports' must be a list of integers") | ||||||||||||||||||||||||||||||||||||||||
| self.check_required_ports([int(p) for p in ports]) | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,92 @@ | ||||||||||||||||||||
| import typer | ||||||||||||||||||||
| from app.utils.logger import Logger | ||||||||||||||||||||
| from app.utils.timeout import TimeoutWrapper | ||||||||||||||||||||
| from .dev import DevSetup, DevSetupConfig | ||||||||||||||||||||
| import traceback | ||||||||||||||||||||
|
|
||||||||||||||||||||
| setup_app = typer.Typer(help="Setup development and production environments") | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| @setup_app.command() | ||||||||||||||||||||
| def dev( | ||||||||||||||||||||
| # Port configurations from config.dev.yaml defaults | ||||||||||||||||||||
| api_port: int = typer.Option(8080, "--api-port", help="API server port"), | ||||||||||||||||||||
| view_port: int = typer.Option(7443, "--view-port", help="Frontend server port"), | ||||||||||||||||||||
| db_port: int = typer.Option(5432, "--db-port", help="Database port"), | ||||||||||||||||||||
| redis_port: int = typer.Option(6379, "--redis-port", help="Redis port"), | ||||||||||||||||||||
| # Repository and branch configuration | ||||||||||||||||||||
| branch: str = typer.Option("feat/develop", "--branch", "-b", help="Git branch to clone"), | ||||||||||||||||||||
| repo: str = typer.Option(None, "--repo", "-r", help="Custom repository URL"), | ||||||||||||||||||||
| workspace: str = typer.Option("./nixopus-dev", "--workspace", "-w", help="Target workspace directory"), | ||||||||||||||||||||
| # SSH configuration | ||||||||||||||||||||
| ssh_key_path: str = typer.Option("~/.ssh/id_ed25519_nixopus", "--ssh-key-path", help="SSH key location"), | ||||||||||||||||||||
| ssh_key_type: str = typer.Option("ed25519", "--ssh-key-type", help="SSH key type"), | ||||||||||||||||||||
| # Setup options | ||||||||||||||||||||
| skip_preflight: bool = typer.Option(False, "--skip-preflight", help="Skip preflight validation checks"), | ||||||||||||||||||||
| skip_conflict: bool = typer.Option(False, "--skip-conflict", help="Skip conflict detection"), | ||||||||||||||||||||
| skip_deps: bool = typer.Option(False, "--skip-deps", help="Skip dependency installation"), | ||||||||||||||||||||
| skip_docker: bool = typer.Option(False, "--skip-docker", help="Skip Docker-based database setup"), | ||||||||||||||||||||
| skip_ssh: bool = typer.Option(False, "--skip-ssh", help="Skip SSH key generation"), | ||||||||||||||||||||
| skip_admin: bool = typer.Option(False, "--skip-admin", help="Skip admin account creation"), | ||||||||||||||||||||
| # Admin credentials | ||||||||||||||||||||
| admin_email: str = typer.Option(None, "--admin-email", help="Admin email (defaults to $USER@example.com)"), | ||||||||||||||||||||
| admin_password: str = typer.Option("Nixopus123!", "--admin-password", help="Admin password"), | ||||||||||||||||||||
| # Control options | ||||||||||||||||||||
| force: bool = typer.Option(False, "--force", "-f", help="Force overwrite existing files"), | ||||||||||||||||||||
| dry_run: bool = typer.Option(False, "--dry-run", "-d", help="Show what would be done"), | ||||||||||||||||||||
| verbose: bool = typer.Option(False, "--verbose", "-v", help="Detailed output"), | ||||||||||||||||||||
| timeout: int = typer.Option(300, "--timeout", "-t", help="Operation timeout in seconds"), | ||||||||||||||||||||
| # Output configuration | ||||||||||||||||||||
| output: str = typer.Option("text", "--output", "-o", help="Output format (text/json)"), | ||||||||||||||||||||
| # Configuration override | ||||||||||||||||||||
| config_file: str = typer.Option("../helpers/config.dev.yaml", "--config-file", "-c", help="Configuration file path"), | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect relative path for config file. The default config file path uses a relative path that goes up one directory ( - config_file: str = typer.Option("../helpers/config.dev.yaml", "--config-file", "-c", help="Configuration file path"),
+ config_file: str = typer.Option("config.dev.yaml", "--config-file", "-c", help="Configuration file path"),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| ): | ||||||||||||||||||||
| """Setup complete development environment for Nixopus""" | ||||||||||||||||||||
| logger = Logger(verbose=verbose) | ||||||||||||||||||||
| try: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Create configuration object | ||||||||||||||||||||
| config = DevSetupConfig( | ||||||||||||||||||||
| api_port=api_port, | ||||||||||||||||||||
| view_port=view_port, | ||||||||||||||||||||
| db_port=db_port, | ||||||||||||||||||||
| redis_port=redis_port, | ||||||||||||||||||||
| branch=branch, | ||||||||||||||||||||
| repo=repo, | ||||||||||||||||||||
| workspace=workspace, | ||||||||||||||||||||
| ssh_key_path=ssh_key_path, | ||||||||||||||||||||
| ssh_key_type=ssh_key_type, | ||||||||||||||||||||
| skip_preflight=skip_preflight, | ||||||||||||||||||||
| skip_conflict=skip_conflict, | ||||||||||||||||||||
| skip_deps=skip_deps, | ||||||||||||||||||||
| skip_docker=skip_docker, | ||||||||||||||||||||
| skip_ssh=skip_ssh, | ||||||||||||||||||||
| skip_admin=skip_admin, | ||||||||||||||||||||
| admin_email=admin_email, | ||||||||||||||||||||
| admin_password=admin_password, | ||||||||||||||||||||
| force=force, | ||||||||||||||||||||
| dry_run=dry_run, | ||||||||||||||||||||
| verbose=verbose, | ||||||||||||||||||||
| timeout=timeout, | ||||||||||||||||||||
| output=output, | ||||||||||||||||||||
| config_file=config_file, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Initialize development setup orchestrator | ||||||||||||||||||||
| dev_setup = DevSetup(config=config, logger=logger) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Execute setup with timeout | ||||||||||||||||||||
| with TimeoutWrapper(timeout): | ||||||||||||||||||||
| dev_setup.run() | ||||||||||||||||||||
|
Comment on lines
+79
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify timeout mechanism compatibility across platforms. The Based on the timeout implementation, you should add platform-specific handling: - with TimeoutWrapper(timeout):
- dev_setup.run()
+ if platform.system().lower() == "windows":
+ # TimeoutWrapper uses SIGALRM which doesn't work on Windows
+ # Run without timeout on Windows or implement threading-based timeout
+ dev_setup.run()
+ else:
+ with TimeoutWrapper(timeout):
+ dev_setup.run()Or better yet, update the 🏁 Script executed: #!/bin/bash
# Check if TimeoutWrapper handles Windows compatibility
cat cli/app/utils/timeout.pyLength of output: 750 TimeoutWrapper uses SIGALRM — incompatible with Windows; fix required
- with TimeoutWrapper(timeout):
- dev_setup.run()
+ if platform.system().lower() == "windows":
+ # TimeoutWrapper uses SIGALRM which doesn't work on Windows
+ # Run without timeout on Windows or implement threading-based timeout
+ dev_setup.run()
+ else:
+ with TimeoutWrapper(timeout):
+ dev_setup.run()📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| logger.success("Development environment setup completed successfully!") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| except TimeoutError as e: | ||||||||||||||||||||
| logger.error(f"Setup timed out after {timeout} seconds: {e}") | ||||||||||||||||||||
| raise typer.Exit(1) | ||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||
| logger.error(f"Setup failed: {e}") | ||||||||||||||||||||
| if verbose: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| logger.error(traceback.format_exc()) | ||||||||||||||||||||
| raise typer.Exit(1) | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add timeouts and tighten errors for subprocess calls.
docker infoandwsl -l -vcan hang (bad contexts, stalled daemons). Add timeouts and handleTimeoutExpired. Also fix the “pin g” comment typo and preferRuntimeErrorover bareException.Also applies to: 53-59
🤖 Prompt for AI Agents