@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.
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +8 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +17 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +315 -17
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/ranger.d.ts +7 -2
- package/dist/commands/ranger.d.ts.map +1 -1
- package/dist/commands/ranger.js +99 -21
- package/dist/commands/ranger.js.map +1 -1
- package/dist/commands/uninstall.d.ts +16 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +206 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
- package/ranger-tui/pyproject.toml +51 -0
- package/ranger-tui/ranger_tui/__init__.py +5 -0
- package/ranger-tui/ranger_tui/__main__.py +16 -0
- package/ranger-tui/ranger_tui/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/__main__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/accessibility.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/app.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/config.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/theme.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/accessibility.py +499 -0
- package/ranger-tui/ranger_tui/actions/__init__.py +13 -0
- package/ranger-tui/ranger_tui/actions/agent_actions.py +74 -0
- package/ranger-tui/ranger_tui/actions/session_actions.py +110 -0
- package/ranger-tui/ranger_tui/actions/task_actions.py +107 -0
- package/ranger-tui/ranger_tui/app.py +93 -0
- package/ranger-tui/ranger_tui/assets/ranger_head.png +0 -0
- package/ranger-tui/ranger_tui/config.py +100 -0
- package/ranger-tui/ranger_tui/data/__init__.py +16 -0
- package/ranger-tui/ranger_tui/data/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/data/__pycache__/client.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/data/__pycache__/models.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/data/client.py +858 -0
- package/ranger-tui/ranger_tui/data/models.py +151 -0
- package/ranger-tui/ranger_tui/screens/__init__.py +16 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/dashboard.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/modals.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/session.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/task.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/command_palette.py +357 -0
- package/ranger-tui/ranger_tui/screens/dashboard.py +232 -0
- package/ranger-tui/ranger_tui/screens/help.py +103 -0
- package/ranger-tui/ranger_tui/screens/modals.py +95 -0
- package/ranger-tui/ranger_tui/screens/session.py +289 -0
- package/ranger-tui/ranger_tui/screens/task.py +187 -0
- package/ranger-tui/ranger_tui/styles/ranger.tcss +254 -0
- package/ranger-tui/ranger_tui/theme.py +93 -0
- package/ranger-tui/ranger_tui/widgets/__init__.py +23 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/accessible.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/logo.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/logo_assets.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/ranger_image.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/sidebar.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/topbar.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/accessible.py +176 -0
- package/ranger-tui/ranger_tui/widgets/agents_table.py +151 -0
- package/ranger-tui/ranger_tui/widgets/header.py +141 -0
- package/ranger-tui/ranger_tui/widgets/logo.py +258 -0
- package/ranger-tui/ranger_tui/widgets/logo_assets.py +62 -0
- package/ranger-tui/ranger_tui/widgets/metrics_panel.py +121 -0
- package/ranger-tui/ranger_tui/widgets/ranger_image.py +91 -0
- package/ranger-tui/ranger_tui/widgets/sessions_table.py +191 -0
- package/ranger-tui/ranger_tui/widgets/sidebar.py +91 -0
- package/ranger-tui/ranger_tui/widgets/tasks_table.py +189 -0
- 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()
|
|
Binary file
|
|
@@ -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
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|