Skip to content
Closed
2 changes: 1 addition & 1 deletion api/api/versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"version": "v1",
"status": "active",
"release_date": "2025-09-11T20:17:18.854289411+05:30",
"release_date": "2025-09-14T22:04:22.317136+05:30",
"end_of_life": "0001-01-01T00:00:00Z",
"changes": [
"Initial API version"
Expand Down
1 change: 1 addition & 0 deletions cli/app/commands/clone/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions cli/app/commands/install/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

76 changes: 69 additions & 7 deletions cli/app/commands/preflight/run.py
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.")

Comment on lines +41 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add timeouts and tighten errors for subprocess calls.

docker info and wsl -l -v can hang (bad contexts, stalled daemons). Add timeouts and handle TimeoutExpired. Also fix the “pin g” comment typo and prefer RuntimeError over bare Exception.

-        # pin g Docker daemon
+        # ping Docker daemon
         try:
             # quick daemon ping via 'docker info'
-            subprocess.run(["docker", "info"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+            subprocess.run(
+                ["docker", "info"],
+                check=True,
+                stdout=subprocess.DEVNULL,
+                stderr=subprocess.DEVNULL,
+                timeout=15,
+            )
             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.")
+        except subprocess.TimeoutExpired:
+            raise RuntimeError("Timed out checking Docker daemon (docker info). Verify Docker Desktop is responsive.")
+        except subprocess.CalledProcessError:
+            raise RuntimeError("Docker daemon is not running. Start Docker Desktop and retry.")
@@
-            try:
-                result = subprocess.run(
+            try:
+                result = subprocess.run(
                     ["wsl", "-l", "-v"],
                     check=False,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     text=True,
-                )
+                    timeout=15,
+                )
                 output = result.stdout or ""
@@
-            except Exception:
+            except subprocess.TimeoutExpired:
+                if self.logger:
+                    self.logger.warning("Timed out determining WSL version; proceeding.")
+            except Exception:
                 if self.logger:
                     self.logger.warning("Unable to verify WSL version. Proceeding.")

Also applies to: 53-59

🤖 Prompt for AI Agents
In cli/app/commands/preflight/run.py around lines 41 to 49 (and also apply same
changes to lines 53 to 59), the code pings Docker with subprocess.run(["docker",
"info"], ...) and currently can hang and raises a bare Exception; fix by
correcting the comment typo ("pin g" -> "ping"), add a reasonable timeout
argument (e.g., timeout=10), catch subprocess.TimeoutExpired and
subprocess.CalledProcessError separately, log or surface a clear error, and
raise RuntimeError with a descriptive message (including timeout vs non-zero
exit) instead of Exception; apply the same timeout/TimeoutExpired handling and
RuntimeError replacement to the wsl subprocess call in the other block.

# 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:
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 preflight check get OS checks too.

     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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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")
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")
🤖 Prompt for AI Agents
In cli/app/commands/preflight/run.py around lines 93 to 101, the config-driven
path that reads ports skips invoking the Windows-specific checks; update the
method so that after resolving ports (both when using user_config/defaults and
when falling back to YAML) it also calls the Windows checks routine (the
existing Windows-specific check method in this class) unconditionally — this
call is safe because the method is a no-op on non-Windows — so place the call
right after ports is set in both branches (or once after the if/else) to ensure
OS checks run for config-driven preflight.

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])
Empty file.
92 changes: 92 additions & 0 deletions cli/app/commands/setup/command.py
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"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect relative path for config file.

The default config file path uses a relative path that goes up one directory (../helpers/config.dev.yaml). This will be inconsistent depending on where the command is run from and may not exist in production.

-    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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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"),
🤖 Prompt for AI Agents
In cli/app/commands/setup/command.py around line 42, the default config path
"../helpers/config.dev.yaml" is a fragile relative path; replace it with a
reliably resolved absolute path. Import pathlib.Path (if not already), compute
the default like Path(__file__).resolve().parents[2] / "helpers" /
"config.dev.yaml" (or compute it inside the command) and use str(...) as the
default for the typer.Option, or alternatively accept None and resolve to that
absolute path at runtime; update the Option to use that resolved path instead of
the "../" relative string.

):
"""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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify timeout mechanism compatibility across platforms.

The TimeoutWrapper uses Unix signals (SIGALRM) which are not available on Windows. This will cause the timeout functionality to fail on Windows systems.

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 TimeoutWrapper class to handle Windows using threading.


🏁 Script executed:

#!/bin/bash
# Check if TimeoutWrapper handles Windows compatibility
cat cli/app/utils/timeout.py

Length of output: 750


TimeoutWrapper uses SIGALRM — incompatible with Windows; fix required

  • Confirmed: cli/app/utils/timeout.py registers signal.SIGALRM and calls signal.alarm() (SIGALRM unavailable on Windows).
  • Action: implement a cross-platform TimeoutWrapper (e.g., threading.Timer or concurrent.futures timeout) and update callers; short-term workaround: guard the caller (cli/app/commands/setup/command.py lines 79–80) with a platform check — example diff:
-        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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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()
🤖 Prompt for AI Agents
In cli/app/commands/setup/command.py around lines 79–80, the current use of
TimeoutWrapper relies on SIGALRM (not available on Windows); change the call to
be cross-platform by detecting the platform and using a thread-based timeout on
Windows (or any non-POSIX) while keeping the existing TimeoutWrapper on POSIX.
Concretely: import sys (or os), check platform (e.g.,
sys.platform.startswith("win") or os.name == "nt"); if Windows, run
dev_setup.run() inside a Thread/ThreadPoolExecutor and enforce the timeout via
thread.join(timeout) or future.result(timeout=...), otherwise keep the existing
with TimeoutWrapper(timeout): dev_setup.run(). Ensure necessary imports are
added and that exceptions/timeouts are handled the same way as the
TimeoutWrapper path.


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)
Loading
Loading