@anastops/cli 1.0.0 → 1.2.0

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 (74) hide show
  1. package/dist/commands/doctor.d.ts.map +1 -1
  2. package/dist/commands/doctor.js +8 -6
  3. package/dist/commands/doctor.js.map +1 -1
  4. package/dist/commands/init.d.ts +17 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +315 -17
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/ranger.d.ts +7 -2
  9. package/dist/commands/ranger.d.ts.map +1 -1
  10. package/dist/commands/ranger.js +99 -21
  11. package/dist/commands/ranger.js.map +1 -1
  12. package/dist/commands/uninstall.d.ts +16 -0
  13. package/dist/commands/uninstall.d.ts.map +1 -0
  14. package/dist/commands/uninstall.js +206 -0
  15. package/dist/commands/uninstall.js.map +1 -0
  16. package/dist/index.js +11 -0
  17. package/dist/index.js.map +1 -1
  18. package/package.json +8 -6
  19. package/ranger-tui/pyproject.toml +51 -0
  20. package/ranger-tui/ranger_tui/__init__.py +5 -0
  21. package/ranger-tui/ranger_tui/__main__.py +16 -0
  22. package/ranger-tui/ranger_tui/__pycache__/__init__.cpython-314.pyc +0 -0
  23. package/ranger-tui/ranger_tui/__pycache__/__main__.cpython-314.pyc +0 -0
  24. package/ranger-tui/ranger_tui/__pycache__/accessibility.cpython-314.pyc +0 -0
  25. package/ranger-tui/ranger_tui/__pycache__/app.cpython-314.pyc +0 -0
  26. package/ranger-tui/ranger_tui/__pycache__/config.cpython-314.pyc +0 -0
  27. package/ranger-tui/ranger_tui/__pycache__/theme.cpython-314.pyc +0 -0
  28. package/ranger-tui/ranger_tui/accessibility.py +499 -0
  29. package/ranger-tui/ranger_tui/actions/__init__.py +13 -0
  30. package/ranger-tui/ranger_tui/actions/agent_actions.py +74 -0
  31. package/ranger-tui/ranger_tui/actions/session_actions.py +110 -0
  32. package/ranger-tui/ranger_tui/actions/task_actions.py +107 -0
  33. package/ranger-tui/ranger_tui/app.py +93 -0
  34. package/ranger-tui/ranger_tui/assets/ranger_head.png +0 -0
  35. package/ranger-tui/ranger_tui/config.py +100 -0
  36. package/ranger-tui/ranger_tui/data/__init__.py +16 -0
  37. package/ranger-tui/ranger_tui/data/__pycache__/__init__.cpython-314.pyc +0 -0
  38. package/ranger-tui/ranger_tui/data/__pycache__/client.cpython-314.pyc +0 -0
  39. package/ranger-tui/ranger_tui/data/__pycache__/models.cpython-314.pyc +0 -0
  40. package/ranger-tui/ranger_tui/data/client.py +858 -0
  41. package/ranger-tui/ranger_tui/data/models.py +151 -0
  42. package/ranger-tui/ranger_tui/screens/__init__.py +16 -0
  43. package/ranger-tui/ranger_tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
  44. package/ranger-tui/ranger_tui/screens/__pycache__/dashboard.cpython-314.pyc +0 -0
  45. package/ranger-tui/ranger_tui/screens/__pycache__/modals.cpython-314.pyc +0 -0
  46. package/ranger-tui/ranger_tui/screens/__pycache__/session.cpython-314.pyc +0 -0
  47. package/ranger-tui/ranger_tui/screens/__pycache__/task.cpython-314.pyc +0 -0
  48. package/ranger-tui/ranger_tui/screens/command_palette.py +357 -0
  49. package/ranger-tui/ranger_tui/screens/dashboard.py +232 -0
  50. package/ranger-tui/ranger_tui/screens/help.py +103 -0
  51. package/ranger-tui/ranger_tui/screens/modals.py +95 -0
  52. package/ranger-tui/ranger_tui/screens/session.py +289 -0
  53. package/ranger-tui/ranger_tui/screens/task.py +187 -0
  54. package/ranger-tui/ranger_tui/styles/ranger.tcss +254 -0
  55. package/ranger-tui/ranger_tui/theme.py +93 -0
  56. package/ranger-tui/ranger_tui/widgets/__init__.py +23 -0
  57. package/ranger-tui/ranger_tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
  58. package/ranger-tui/ranger_tui/widgets/__pycache__/accessible.cpython-314.pyc +0 -0
  59. package/ranger-tui/ranger_tui/widgets/__pycache__/logo.cpython-314.pyc +0 -0
  60. package/ranger-tui/ranger_tui/widgets/__pycache__/logo_assets.cpython-314.pyc +0 -0
  61. package/ranger-tui/ranger_tui/widgets/__pycache__/ranger_image.cpython-314.pyc +0 -0
  62. package/ranger-tui/ranger_tui/widgets/__pycache__/sidebar.cpython-314.pyc +0 -0
  63. package/ranger-tui/ranger_tui/widgets/__pycache__/topbar.cpython-314.pyc +0 -0
  64. package/ranger-tui/ranger_tui/widgets/accessible.py +176 -0
  65. package/ranger-tui/ranger_tui/widgets/agents_table.py +151 -0
  66. package/ranger-tui/ranger_tui/widgets/header.py +141 -0
  67. package/ranger-tui/ranger_tui/widgets/logo.py +258 -0
  68. package/ranger-tui/ranger_tui/widgets/logo_assets.py +62 -0
  69. package/ranger-tui/ranger_tui/widgets/metrics_panel.py +121 -0
  70. package/ranger-tui/ranger_tui/widgets/ranger_image.py +91 -0
  71. package/ranger-tui/ranger_tui/widgets/sessions_table.py +191 -0
  72. package/ranger-tui/ranger_tui/widgets/sidebar.py +91 -0
  73. package/ranger-tui/ranger_tui/widgets/tasks_table.py +189 -0
  74. package/ranger-tui/ranger_tui/widgets/topbar.py +168 -0
@@ -0,0 +1,107 @@
1
+ """
2
+ Task action handlers.
3
+ """
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from ranger_tui.app import RangerApp
9
+ from ranger_tui.data import DataClient
10
+
11
+
12
+ class TaskActions:
13
+ """Task-related actions."""
14
+
15
+ def __init__(self, app: "RangerApp"):
16
+ self.app = app
17
+ self.client: "DataClient" = app.client
18
+
19
+ async def create(
20
+ self,
21
+ session_id: str,
22
+ task_type: str,
23
+ description: str,
24
+ prompt: str,
25
+ provider: str = "claude",
26
+ model: str = "claude-sonnet",
27
+ ) -> str | None:
28
+ """Create a new task."""
29
+ try:
30
+ task = await self.client.create_task(
31
+ session_id=session_id,
32
+ task_type=task_type,
33
+ description=description,
34
+ prompt=prompt,
35
+ provider=provider,
36
+ model=model,
37
+ )
38
+ self.app.notify(f"Created task: {task.id}", severity="information")
39
+ return task.id
40
+ except Exception as e:
41
+ self.app.notify(f"Failed to create task: {e}", severity="error")
42
+ return None
43
+
44
+ async def queue(self, task_id: str) -> bool:
45
+ """Queue a task for execution."""
46
+ try:
47
+ success = await self.client.queue_task(task_id)
48
+ if success:
49
+ self.app.notify(f"Queued task: {task_id}", severity="information")
50
+ else:
51
+ self.app.notify("Failed to queue task (may not be pending)", severity="error")
52
+ return success
53
+ except Exception as e:
54
+ self.app.notify(f"Error queuing task: {e}", severity="error")
55
+ return False
56
+
57
+ async def cancel(self, task_id: str) -> bool:
58
+ """Cancel a task."""
59
+ try:
60
+ success = await self.client.cancel_task(task_id)
61
+ if success:
62
+ self.app.notify(f"Cancelled task: {task_id}", severity="warning")
63
+ else:
64
+ self.app.notify("Failed to cancel task (may already be completed)", severity="error")
65
+ return success
66
+ except Exception as e:
67
+ self.app.notify(f"Error cancelling task: {e}", severity="error")
68
+ return False
69
+
70
+ async def retry(self, task_id: str) -> bool:
71
+ """Retry a failed task."""
72
+ try:
73
+ success = await self.client.retry_task(task_id)
74
+ if success:
75
+ self.app.notify(f"Retrying task: {task_id}", severity="information")
76
+ else:
77
+ self.app.notify("Failed to retry task (may not be failed)", severity="error")
78
+ return success
79
+ except Exception as e:
80
+ self.app.notify(f"Error retrying task: {e}", severity="error")
81
+ return False
82
+
83
+ async def complete(
84
+ self,
85
+ task_id: str,
86
+ content: str,
87
+ artifacts: list[str] | None = None,
88
+ ) -> bool:
89
+ """Mark a task as completed."""
90
+ try:
91
+ success = await self.client.complete_task(task_id, content, artifacts)
92
+ if success:
93
+ self.app.notify(f"Completed task: {task_id}", severity="information")
94
+ else:
95
+ self.app.notify("Failed to complete task", severity="error")
96
+ return success
97
+ except Exception as e:
98
+ self.app.notify(f"Error completing task: {e}", severity="error")
99
+ return False
100
+
101
+ async def get_status(self, task_id: str):
102
+ """Get task status."""
103
+ try:
104
+ return await self.client.get_task(task_id)
105
+ except Exception as e:
106
+ self.app.notify(f"Error getting task: {e}", severity="error")
107
+ return None
@@ -0,0 +1,93 @@
1
+ """
2
+ Main Ranger TUI Application.
3
+ """
4
+
5
+ from textual.app import App
6
+ from textual.binding import Binding
7
+ from textual.reactive import reactive
8
+
9
+ from ranger_tui.config import config
10
+ from ranger_tui.data import DataClient, get_client, Session
11
+ from ranger_tui.theme import RANGER_THEME
12
+ from ranger_tui.screens import DashboardScreen
13
+
14
+
15
+ class RangerApp(App):
16
+ """
17
+ Anastops Ranger TUI - Terminal dashboard for orchestration management.
18
+
19
+ Navigation:
20
+ - Arrow keys to move
21
+ - Enter to select/open
22
+ - Escape to go back
23
+ - q to quit
24
+ """
25
+
26
+ TITLE = "Ranger"
27
+ CSS_PATH = "styles/ranger.tcss"
28
+
29
+ BINDINGS = [
30
+ Binding("q", "quit", "Quit", show=True, priority=True),
31
+ Binding("escape", "back", "Back", show=True),
32
+ Binding("r", "refresh", "Refresh", show=True),
33
+ ]
34
+
35
+ # Reactive state
36
+ sessions: reactive[list[Session]] = reactive(list, init=False)
37
+ mongo_connected: reactive[bool] = reactive(False)
38
+ redis_connected: reactive[bool] = reactive(False)
39
+
40
+ def __init__(self):
41
+ super().__init__()
42
+ self.client: DataClient = get_client()
43
+
44
+ async def on_mount(self) -> None:
45
+ """Called when app is mounted."""
46
+ # Register and apply custom theme
47
+ self.register_theme(RANGER_THEME)
48
+ self.theme = "ranger"
49
+
50
+ # Connect to databases
51
+ await self._connect()
52
+
53
+ # Start periodic refresh
54
+ self.set_interval(config.refresh_interval, self._refresh_data)
55
+ await self._refresh_data()
56
+
57
+ # Push initial screen
58
+ await self.push_screen(DashboardScreen())
59
+
60
+ async def _connect(self) -> None:
61
+ """Connect to MongoDB and Redis."""
62
+ await self.client.connect()
63
+ health = await self.client.health_check()
64
+ self.mongo_connected = health.get("mongodb", False)
65
+ self.redis_connected = health.get("redis", False)
66
+
67
+ async def _refresh_data(self) -> None:
68
+ """Refresh data from databases."""
69
+ if not self.client.is_connected:
70
+ await self._connect()
71
+
72
+ if self.client.is_connected:
73
+ self.sessions = await self.client.get_sessions()
74
+
75
+ # Update connection status
76
+ health = await self.client.health_check()
77
+ self.mongo_connected = health.get("mongodb", False)
78
+ self.redis_connected = health.get("redis", False)
79
+
80
+ def action_quit(self) -> None:
81
+ """Quit the application."""
82
+ self.exit()
83
+
84
+ async def action_refresh(self) -> None:
85
+ """Manually refresh data."""
86
+ await self._refresh_data()
87
+ self.notify("Refreshed")
88
+
89
+ def action_back(self) -> None:
90
+ """Go back to previous screen (but not from dashboard)."""
91
+ # Don't pop if we're on the dashboard (root screen)
92
+ if len(self.screen_stack) > 2:
93
+ self.pop_screen()
@@ -0,0 +1,100 @@
1
+ """
2
+ Configuration management for Ranger TUI.
3
+ Automatically loads credentials from ~/.anastops/.env or docker/.env file.
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from dataclasses import dataclass
9
+ from dotenv import load_dotenv
10
+
11
+
12
+ def _find_env_file() -> Path | None:
13
+ """Find the .env file, checking ~/.anastops first, then docker/.env."""
14
+ # Priority 1: User's anastops home directory (created by anastops init)
15
+ anastops_home = Path.home() / ".anastops" / ".env"
16
+ if anastops_home.exists():
17
+ return anastops_home
18
+
19
+ # Priority 2: docker/.env in project directory tree
20
+ candidates = [
21
+ Path.cwd() / "docker" / ".env",
22
+ Path.cwd().parent / "docker" / ".env",
23
+ Path.cwd().parent.parent / "docker" / ".env",
24
+ Path(__file__).parent.parent.parent.parent.parent.parent / "docker" / ".env",
25
+ ]
26
+
27
+ for candidate in candidates:
28
+ if candidate.exists():
29
+ return candidate
30
+
31
+ return None
32
+
33
+
34
+ def _load_env_file() -> None:
35
+ """Load environment variables from .env file."""
36
+ env_file = _find_env_file()
37
+ if env_file:
38
+ load_dotenv(env_file)
39
+
40
+
41
+ # Load environment on module import
42
+ _load_env_file()
43
+
44
+
45
+ def _build_redis_url() -> str:
46
+ """Build Redis URL from environment variables."""
47
+ # Check for explicit REDIS_URL first
48
+ if redis_url := os.getenv("REDIS_URL"):
49
+ return redis_url
50
+
51
+ # Build from components
52
+ password = os.getenv("REDIS_PASSWORD", "")
53
+ host = os.getenv("REDIS_HOST", "localhost")
54
+ port = os.getenv("REDIS_PORT", "6380")
55
+
56
+ if password:
57
+ return f"redis://:{password}@{host}:{port}"
58
+ return f"redis://{host}:{port}"
59
+
60
+
61
+ def _build_mongodb_url() -> str:
62
+ """Build MongoDB URL from environment variables."""
63
+ # Check for explicit MONGODB_URL first
64
+ if mongodb_url := os.getenv("MONGODB_URL"):
65
+ return mongodb_url
66
+
67
+ # Build from components
68
+ username = os.getenv("MONGO_ROOT_USERNAME", "admin")
69
+ password = os.getenv("MONGO_ROOT_PASSWORD", "")
70
+ host = os.getenv("MONGO_HOST", "localhost")
71
+ port = os.getenv("MONGO_PORT", "27018")
72
+ database = os.getenv("MONGO_DATABASE", "anastops")
73
+
74
+ if password:
75
+ return f"mongodb://{username}:{password}@{host}:{port}/{database}?authSource=admin"
76
+ return f"mongodb://{host}:{port}/{database}"
77
+
78
+
79
+ @dataclass
80
+ class Config:
81
+ """Application configuration loaded from environment variables."""
82
+
83
+ mongodb_url: str
84
+ redis_url: str
85
+ refresh_interval: float
86
+ environment: str
87
+
88
+ @classmethod
89
+ def from_env(cls) -> "Config":
90
+ """Load configuration from environment variables."""
91
+ return cls(
92
+ mongodb_url=_build_mongodb_url(),
93
+ redis_url=_build_redis_url(),
94
+ refresh_interval=float(os.getenv("RANGER_REFRESH_INTERVAL", "2.0")),
95
+ environment=os.getenv("RANGER_ENVIRONMENT", "local"),
96
+ )
97
+
98
+
99
+ # Global config instance
100
+ config = Config.from_env()
@@ -0,0 +1,16 @@
1
+ """
2
+ Data layer for Ranger TUI.
3
+ """
4
+
5
+ from .client import DataClient, get_client
6
+ from .models import Session, Task, Agent, Artifact, SessionReport
7
+
8
+ __all__ = [
9
+ "DataClient",
10
+ "get_client",
11
+ "Session",
12
+ "Task",
13
+ "Agent",
14
+ "Artifact",
15
+ "SessionReport",
16
+ ]