@anastops/cli 1.1.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/init.d.ts +15 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +161 -4
- 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 +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +1 -1
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- 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,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent 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 AgentActions:
|
|
13
|
+
"""Agent-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
|
+
role: str,
|
|
23
|
+
name: str | None = None,
|
|
24
|
+
provider: str = "claude",
|
|
25
|
+
model: str = "claude-sonnet",
|
|
26
|
+
) -> str | None:
|
|
27
|
+
"""Create a new agent."""
|
|
28
|
+
try:
|
|
29
|
+
agent = await self.client.create_agent(
|
|
30
|
+
session_id=session_id,
|
|
31
|
+
role=role,
|
|
32
|
+
name=name,
|
|
33
|
+
provider=provider,
|
|
34
|
+
model=model,
|
|
35
|
+
)
|
|
36
|
+
self.app.notify(f"Created agent: {agent.id}", severity="information")
|
|
37
|
+
return agent.id
|
|
38
|
+
except Exception as e:
|
|
39
|
+
self.app.notify(f"Failed to create agent: {e}", severity="error")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
async def deploy(self, agent_id: str, task_id: str) -> bool:
|
|
43
|
+
"""Deploy an agent to a task."""
|
|
44
|
+
try:
|
|
45
|
+
success = await self.client.deploy_agent(agent_id, task_id)
|
|
46
|
+
if success:
|
|
47
|
+
self.app.notify(f"Deployed agent {agent_id} to task {task_id}", severity="information")
|
|
48
|
+
else:
|
|
49
|
+
self.app.notify("Failed to deploy agent", severity="error")
|
|
50
|
+
return success
|
|
51
|
+
except Exception as e:
|
|
52
|
+
self.app.notify(f"Error deploying agent: {e}", severity="error")
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
async def retire(self, agent_id: str) -> bool:
|
|
56
|
+
"""Retire an agent."""
|
|
57
|
+
try:
|
|
58
|
+
success = await self.client.retire_agent(agent_id)
|
|
59
|
+
if success:
|
|
60
|
+
self.app.notify(f"Retired agent: {agent_id}", severity="information")
|
|
61
|
+
else:
|
|
62
|
+
self.app.notify("Failed to retire agent", severity="error")
|
|
63
|
+
return success
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.app.notify(f"Error retiring agent: {e}", severity="error")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
async def get_status(self, agent_id: str):
|
|
69
|
+
"""Get agent status."""
|
|
70
|
+
try:
|
|
71
|
+
return await self.client.get_agent(agent_id)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
self.app.notify(f"Error getting agent: {e}", severity="error")
|
|
74
|
+
return None
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session 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 SessionActions:
|
|
13
|
+
"""Session-related actions."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, app: "RangerApp"):
|
|
16
|
+
self.app = app
|
|
17
|
+
self.client: "DataClient" = app.client
|
|
18
|
+
|
|
19
|
+
async def spawn(self, objective: str) -> str | None:
|
|
20
|
+
"""Create a new session."""
|
|
21
|
+
try:
|
|
22
|
+
session = await self.client.create_session(objective)
|
|
23
|
+
self.app.notify(f"Created session: {session.id}", severity="information")
|
|
24
|
+
await self.app._refresh_data()
|
|
25
|
+
return session.id
|
|
26
|
+
except Exception as e:
|
|
27
|
+
self.app.notify(f"Failed to create session: {e}", severity="error")
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
async def archive(self, session_id: str) -> bool:
|
|
31
|
+
"""Archive a session."""
|
|
32
|
+
try:
|
|
33
|
+
success = await self.client.archive_session(session_id)
|
|
34
|
+
if success:
|
|
35
|
+
self.app.notify(f"Archived session: {session_id}", severity="information")
|
|
36
|
+
await self.app._refresh_data()
|
|
37
|
+
else:
|
|
38
|
+
self.app.notify("Failed to archive session", severity="error")
|
|
39
|
+
return success
|
|
40
|
+
except Exception as e:
|
|
41
|
+
self.app.notify(f"Error archiving session: {e}", severity="error")
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
async def purge(self, session_id: str) -> bool:
|
|
45
|
+
"""Permanently delete a session."""
|
|
46
|
+
try:
|
|
47
|
+
success = await self.client.purge_session(session_id)
|
|
48
|
+
if success:
|
|
49
|
+
self.app.notify(f"Purged session: {session_id}", severity="warning")
|
|
50
|
+
await self.app._refresh_data()
|
|
51
|
+
else:
|
|
52
|
+
self.app.notify("Failed to purge session", severity="error")
|
|
53
|
+
return success
|
|
54
|
+
except Exception as e:
|
|
55
|
+
self.app.notify(f"Error purging session: {e}", severity="error")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
async def purge_by_status(self, status: str) -> int:
|
|
59
|
+
"""Purge all sessions with a given status."""
|
|
60
|
+
try:
|
|
61
|
+
result = await self.client.purge_sessions_by_status(status)
|
|
62
|
+
count = result.get("deleted_count", 0)
|
|
63
|
+
if count > 0:
|
|
64
|
+
self.app.notify(f"Purged {count} {status} sessions", severity="warning")
|
|
65
|
+
await self.app._refresh_data()
|
|
66
|
+
else:
|
|
67
|
+
self.app.notify(f"No {status} sessions to purge", severity="information")
|
|
68
|
+
return count
|
|
69
|
+
except Exception as e:
|
|
70
|
+
self.app.notify(f"Error purging sessions: {e}", severity="error")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
async def fork(self, session_id: str, reason: str | None = None) -> str | None:
|
|
74
|
+
"""Fork a session."""
|
|
75
|
+
try:
|
|
76
|
+
forked = await self.client.fork_session(session_id, reason)
|
|
77
|
+
if forked:
|
|
78
|
+
self.app.notify(f"Forked to: {forked.id}", severity="information")
|
|
79
|
+
await self.app._refresh_data()
|
|
80
|
+
return forked.id
|
|
81
|
+
else:
|
|
82
|
+
self.app.notify("Failed to fork session", severity="error")
|
|
83
|
+
return None
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.app.notify(f"Error forking session: {e}", severity="error")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
async def get_report(self, session_id: str):
|
|
89
|
+
"""Get comprehensive session report."""
|
|
90
|
+
try:
|
|
91
|
+
return await self.client.get_session_report(session_id)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.app.notify(f"Error getting report: {e}", severity="error")
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
async def get_cost(self, session_id: str) -> dict:
|
|
97
|
+
"""Get session cost breakdown."""
|
|
98
|
+
try:
|
|
99
|
+
return await self.client.get_session_cost(session_id)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
self.app.notify(f"Error getting cost: {e}", severity="error")
|
|
102
|
+
return {}
|
|
103
|
+
|
|
104
|
+
async def get_metrics(self, session_id: str) -> dict:
|
|
105
|
+
"""Get session metrics."""
|
|
106
|
+
try:
|
|
107
|
+
return await self.client.get_session_metrics(session_id)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
self.app.notify(f"Error getting metrics: {e}", severity="error")
|
|
110
|
+
return {}
|
|
@@ -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
|