onecoder 0.0.2__py3-none-any.whl

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 (73) hide show
  1. onecoder/agent.py +95 -0
  2. onecoder/agentic_tool_search/__init__.py +0 -0
  3. onecoder/agentic_tool_search/dynamic_tool_search.py +64 -0
  4. onecoder/agentic_tool_search/registry.py +33 -0
  5. onecoder/agents/__init__.py +7 -0
  6. onecoder/agents/documentation_agent.py +12 -0
  7. onecoder/agents/file_reader_agent.py +19 -0
  8. onecoder/agents/file_writer_agent.py +19 -0
  9. onecoder/agents/orchestrator_agent.py +51 -0
  10. onecoder/agents/refactoring_agent.py +12 -0
  11. onecoder/agents/research_agent.py +31 -0
  12. onecoder/agents/task_suggestion_agent.py +88 -0
  13. onecoder/alignment.py +236 -0
  14. onecoder/api.py +162 -0
  15. onecoder/api_client.py +112 -0
  16. onecoder/backends/base.py +22 -0
  17. onecoder/backends/local_tui.py +65 -0
  18. onecoder/blackboard.py +102 -0
  19. onecoder/cli.py +108 -0
  20. onecoder/commands/__init__.py +1 -0
  21. onecoder/commands/auth.py +78 -0
  22. onecoder/commands/ci.py +29 -0
  23. onecoder/commands/delegate.py +557 -0
  24. onecoder/commands/doctor.py +40 -0
  25. onecoder/commands/issue.py +136 -0
  26. onecoder/commands/logs.py +45 -0
  27. onecoder/commands/project.py +270 -0
  28. onecoder/commands/server.py +170 -0
  29. onecoder/config_manager.py +87 -0
  30. onecoder/constants.py +9 -0
  31. onecoder/diagnostics/__init__.py +2 -0
  32. onecoder/diagnostics/env_scan.py +207 -0
  33. onecoder/discovery.py +101 -0
  34. onecoder/distillation.py +236 -0
  35. onecoder/evaluation/__init__.py +1 -0
  36. onecoder/evaluation/ttu.py +176 -0
  37. onecoder/governance/__init__.py +0 -0
  38. onecoder/governance/probllm.py +91 -0
  39. onecoder/hooks.py +74 -0
  40. onecoder/ipc_auth.py +200 -0
  41. onecoder/issues.py +188 -0
  42. onecoder/jules_client.py +343 -0
  43. onecoder/knowledge.py +106 -0
  44. onecoder/llm.py +61 -0
  45. onecoder/logger.py +42 -0
  46. onecoder/metrics.py +129 -0
  47. onecoder/models/delegation.py +46 -0
  48. onecoder/onboarding.py +264 -0
  49. onecoder/review.py +233 -0
  50. onecoder/services/delegation_service.py +209 -0
  51. onecoder/services/validation_service.py +104 -0
  52. onecoder/sessions.py +186 -0
  53. onecoder/sprint_collector.py +165 -0
  54. onecoder/sync.py +167 -0
  55. onecoder/tmux.py +86 -0
  56. onecoder/tools/__init__.py +10 -0
  57. onecoder/tools/executor.py +53 -0
  58. onecoder/tools/external_tools.py +106 -0
  59. onecoder/tools/file_tools.py +77 -0
  60. onecoder/tools/interface.py +25 -0
  61. onecoder/tools/jules_tools.py +122 -0
  62. onecoder/tools/kit_tools.py +122 -0
  63. onecoder/tools/registry.py +32 -0
  64. onecoder/tui/__init__.py +5 -0
  65. onecoder/tui/app.py +263 -0
  66. onecoder/tui/commands.py +150 -0
  67. onecoder/tui/widgets.py +92 -0
  68. onecoder/worktree.py +186 -0
  69. onecoder-0.0.2.dist-info/METADATA +17 -0
  70. onecoder-0.0.2.dist-info/RECORD +73 -0
  71. onecoder-0.0.2.dist-info/WHEEL +5 -0
  72. onecoder-0.0.2.dist-info/entry_points.txt +2 -0
  73. onecoder-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,136 @@
1
+ import click
2
+ import re
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from ..issues import IssueManager
6
+ from ..config_manager import config_manager
7
+ from ai_sprint.telemetry import FailureModeCapture
8
+
9
+ @click.group()
10
+ def issue():
11
+ """Manage platform governance issues."""
12
+ pass
13
+
14
+ @issue.command(name="create")
15
+ @click.option("--from-telemetry", is_flag=True, help="Create issue from latest telemetry failure")
16
+ @click.option("--title", help="Manual title for the issue")
17
+ def issue_create(from_telemetry, title):
18
+ """Create a new governance issue."""
19
+ from rich.console import Console
20
+ console = Console()
21
+
22
+ manager = IssueManager()
23
+
24
+ if from_telemetry:
25
+ capture = FailureModeCapture()
26
+ failures = capture.get_failures(limit=1)
27
+ if not failures:
28
+ console.print("[yellow]No recent failures found in telemetry.[/yellow]")
29
+ return
30
+
31
+ failure = failures[0]
32
+ issue_path = manager.create_from_telemetry(failure, title=title)
33
+ console.print(f"[bold green]✓ Issue created from telemetry:[/bold green] {issue_path.name}")
34
+ else:
35
+ # Future: manual issue creation wizard
36
+ console.print("[yellow]Manual issue creation not yet implemented. Use --from-telemetry.[/yellow]")
37
+
38
+ @issue.command(name="resolve")
39
+ @click.argument("issue_id")
40
+ @click.option("--message", "-m", help="Resolution message")
41
+ @click.option("--pr", help="Pull Request URL")
42
+ def issue_resolve(issue_id, message, pr):
43
+ """Mark an issue as resolved."""
44
+ from rich.console import Console
45
+ import subprocess
46
+
47
+ console = Console()
48
+ manager = IssueManager()
49
+
50
+ # 1. Get Resolution Metadata
51
+ resolution_meta = {
52
+ "user": config_manager.get_user(),
53
+ "resolved_at": datetime.now().isoformat(),
54
+ "pr_url": pr
55
+ }
56
+
57
+ # Get Current Commit
58
+ try:
59
+ commit = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
60
+ resolution_meta["commit_sha"] = commit
61
+ except Exception:
62
+ resolution_meta["commit_sha"] = "unknown"
63
+
64
+ # 2. Update Status
65
+ success = manager.update_status(issue_id, "resolved", resolution_meta)
66
+
67
+ if success:
68
+ console.print(f"[bold green]✓ Issue {issue_id} marked as resolved.[/bold green]")
69
+ console.print(msg="Resolution metadata captured.", style="dim")
70
+ else:
71
+ console.print(f"[bold red]Error:[/bold red] Issue {issue_id} not found.")
72
+
73
+ @issue.command(name="sync")
74
+ @click.option("--input", "-i", type=click.Path(exists=True), help="Input issue file/directory (optional)")
75
+ def issue_sync(input):
76
+ """Sync local governance issues to the OneCoder API."""
77
+ import asyncio
78
+ from ..sync import sync_issues, ProjectConfig
79
+ from ..api_client import get_api_client
80
+
81
+ # Setup
82
+ token = config_manager.get_token()
83
+ if not token:
84
+ click.secho("Error: Not logged in.", fg="red")
85
+ return
86
+
87
+ repo_root = Path.cwd() # Should use find_repo_root from sync.py really, importing locally
88
+ proj_config = ProjectConfig(repo_root)
89
+ project_id = proj_config.get_project_id()
90
+
91
+ if not project_id:
92
+ click.secho("Error: Project ID not found.", fg="red")
93
+ return
94
+
95
+ client = get_api_client(token)
96
+
97
+ # Run Sync
98
+ asyncio.run(sync_issues(client, project_id))
99
+
100
+ @issue.command(name="list")
101
+ def issue_list():
102
+ """List all local governance issues."""
103
+ from rich.console import Console
104
+ from rich.table import Table
105
+ console = Console()
106
+
107
+ manager = IssueManager()
108
+ issues_dir = manager.issues_dir
109
+ if not issues_dir.exists():
110
+ console.print("[yellow]No .issues directory found.[/yellow]")
111
+ return
112
+
113
+ table = Table(title="Local Governance Issues")
114
+ table.add_column("ID", style="cyan")
115
+ table.add_column("Title", style="white")
116
+ table.add_column("Status", style="magenta")
117
+
118
+ for item in sorted(issues_dir.iterdir()):
119
+ if item.is_file() and item.suffix == ".md" and item.name != "README.md":
120
+ match = re.match(r"^(\d{3})-(.+)\.md$", item.name)
121
+ if match:
122
+ issue_id = match.group(1)
123
+ issue_title = match.group(2).replace("-", " ").capitalize()
124
+
125
+ # Try to parse status from file
126
+ content = item.read_text()
127
+ if any(x in content for x in ["Resolved", "🟢 Resolved", "🟢 **Resolved**"]):
128
+ status = "[green]Resolved[/green]"
129
+ elif any(x in content for x in ["Open", "🔴 Open", "🔴 **Open**"]):
130
+ status = "[red]Open[/red]"
131
+ else:
132
+ status = "[yellow]Unknown[/yellow]"
133
+
134
+ table.add_row(issue_id, issue_title, status)
135
+
136
+ console.print(table)
@@ -0,0 +1,45 @@
1
+ import click
2
+ from pathlib import Path
3
+ import sys
4
+
5
+ @click.command()
6
+ @click.option("--n", default=50, help="Number of lines to show (default 50).")
7
+ @click.option("--follow", "-f", is_flag=True, help="Follow log output that updates.")
8
+ def logs(n, follow):
9
+ """View recent OneCoder logs."""
10
+ log_file = Path.home() / ".onecoder" / "logs" / "onecoder.log"
11
+
12
+ if not log_file.exists():
13
+ click.echo(f"No log file found at {log_file}")
14
+ return
15
+
16
+ click.secho(f"Reading logs from {log_file}...", dim=True)
17
+
18
+ try:
19
+ from collections import deque
20
+ with open(log_file, 'r') as f:
21
+ if follow:
22
+ # Print last n lines first
23
+ lines = deque(f, maxlen=n)
24
+ for line in lines:
25
+ sys.stdout.write(line)
26
+
27
+ # Follow loop
28
+ import time
29
+ while True:
30
+ line = f.readline()
31
+ if line:
32
+ sys.stdout.write(line)
33
+ else:
34
+ time.sleep(0.1)
35
+ else:
36
+ # Just print last n lines
37
+ lines = deque(f, maxlen=n)
38
+ for line in lines:
39
+ sys.stdout.write(line)
40
+
41
+ except KeyboardInterrupt:
42
+ pass
43
+ except Exception as e:
44
+ click.report_exception(e) # Keep traceback if needed
45
+ click.secho(f"Error reading logs: {e}", fg="red")
@@ -0,0 +1,270 @@
1
+ import click
2
+ import json
3
+ import asyncio
4
+ from pathlib import Path
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from rich.panel import Panel
8
+
9
+ from .auth import require_login
10
+ from ..onboarding import onboard_project
11
+ from ..knowledge import ProjectKnowledge
12
+ from ..distillation import SprintDistiller
13
+ from ..sync import sync_project_context
14
+ from ..alignment import AlignmentTracker
15
+
16
+ @click.command()
17
+ @click.option("--directory", default=".", help="Project directory")
18
+ @require_login
19
+ def init(directory):
20
+ """Onboards the current project into OneCoder."""
21
+ onboard_project(directory)
22
+
23
+ @click.command()
24
+ @click.option("--directory", default=".", help="Root directory to scan")
25
+ def status(directory):
26
+ """Shows the platform-wide status by aggregating all sub-project sprints."""
27
+ console = Console()
28
+ root_path = Path(directory).resolve()
29
+
30
+ console.print(f"[bold cyan]Scanning for OneCoder sub-projects in:[/bold cyan] {root_path}")
31
+
32
+ # Discovery logic
33
+ sprint_dirs = []
34
+ for p in root_path.rglob(".sprint"):
35
+ if "node_modules" in p.parts or ".git" in p.parts:
36
+ continue
37
+ sprint_dirs.append(p)
38
+
39
+ if not sprint_dirs:
40
+ console.print("[yellow]No .sprint directories found.[/yellow]")
41
+ return
42
+
43
+ table = Table(title="Platform-Wide Sprint Status")
44
+ table.add_column("Component", style="cyan")
45
+ table.add_column("Current Sprint", style="magenta")
46
+ table.add_column("Status", style="green")
47
+ table.add_column("Primary Goal", style="white")
48
+ table.add_column("Tasks (D/T)", justify="right")
49
+
50
+ for sd in sprint_dirs:
51
+ component_name = sd.parent.name
52
+ # Look for active sprint (usually the most recent subdirectory that isn't empty)
53
+ sprint_shards = [d for d in sd.iterdir() if d.is_dir()]
54
+ if not sprint_shards:
55
+ continue
56
+
57
+ # For now, let's take the latest/active one.
58
+ # In a real scenario, we might want to check sprint.json for status.
59
+ active_sprint = sorted(sprint_shards)[-1]
60
+ state_file = active_sprint / "sprint.json"
61
+
62
+ if state_file.exists():
63
+ try:
64
+ with open(state_file, "r") as f:
65
+ state = json.load(f)
66
+
67
+ sprint_id = state.get("sprintId", active_sprint.name)
68
+ status_obj = state.get("status", {})
69
+ status_str = f"{status_obj.get('state', 'unknown')} ({status_obj.get('phase', 'init')})"
70
+ goal = state.get("goals", {}).get("primary", "N/A")
71
+
72
+ tasks = state.get("tasks", [])
73
+ done_tasks = len([t for t in tasks if t.get("status") == "done"])
74
+ total_tasks = len(tasks)
75
+
76
+ table.add_row(
77
+ component_name,
78
+ sprint_id,
79
+ status_str,
80
+ goal or "N/A",
81
+ f"{done_tasks}/{total_tasks}"
82
+ )
83
+ except Exception as e:
84
+ table.add_row(component_name, active_sprint.name, "Error", str(e), "-")
85
+ else:
86
+ table.add_row(component_name, active_sprint.name, "No sprint.json", "-", "-")
87
+
88
+ console.print(table)
89
+
90
+ @click.command(name="suggest")
91
+ @click.option("--limit", default=3, help="Number of past sprints to analyze")
92
+ @click.option("--dry-run", is_flag=True, help="Don't call LLM, just show context")
93
+ def sprint_suggest(limit, dry_run):
94
+ """
95
+ Suggests next tasks based on sprint history and learnings.
96
+ """
97
+ from ..sprint_collector import SprintCollector
98
+ from ..agents import TaskSuggester
99
+
100
+ console = Console()
101
+ repo_root = Path.cwd()
102
+
103
+ # 1. Collect Context
104
+ with console.status("[bold green]Harvesting sprint context..."):
105
+ collector = SprintCollector(repo_root)
106
+ context = collector.get_recent_context(limit=limit)
107
+
108
+ if not context:
109
+ console.print("[yellow]No sprint context found. Run this from a repository with .sprint history.[/yellow]")
110
+ return
111
+
112
+ if dry_run:
113
+ console.print(Panel(json.dumps(context, indent=2), title="Captured Context (Dry Run)"))
114
+ return
115
+
116
+ # 2. Get Suggestions from LLM
117
+ with console.status("[bold purple]Analyzing patterns and generating suggestions..."):
118
+ suggester = TaskSuggester()
119
+ suggestions = suggester.suggest_next_tasks(context)
120
+
121
+ if not suggestions:
122
+ console.print("[yellow]No suggestions generated (or LLM unavailable).[/yellow]")
123
+ return
124
+
125
+ # 3. Present Suggestions
126
+ console.print("\n[bold]Suggested Next Tasks:[/bold]")
127
+ for i, task in enumerate(suggestions, 1):
128
+ type_style = {
129
+ "feature": "blue",
130
+ "fix": "red",
131
+ "chore": "dim",
132
+ "governance": "magenta"
133
+ }.get(task.get("type", "task"), "white")
134
+
135
+ console.print(f"\n{i}. [{type_style}]{task['title']}[/{type_style}]")
136
+ console.print(f" [dim]{task['rationale']}[/dim]")
137
+
138
+ # 4. Interactive Delegation (Optional)
139
+ if click.confirm("\nDelegate one of these tasks now?", default=False):
140
+ choice = click.prompt("Enter task number", type=int, default=1)
141
+ if 1 <= choice <= len(suggestions):
142
+ selected = suggestions[choice-1]
143
+ prompt = selected['title']
144
+ click.echo(f"\nrun: onecoder delegate \"{prompt}\"")
145
+
146
+ @click.command()
147
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON for machine consumption")
148
+ def knowledge(json_output):
149
+ """
150
+ Shows the platform-wide knowledge awareness.
151
+ Aggregates L2 (Durable Project Guidelines) and L1 (Active Sprint Context).
152
+ """
153
+ pk = ProjectKnowledge()
154
+
155
+ if json_output:
156
+ click.echo(json.dumps(pk.aggregate_knowledge(), indent=2))
157
+ else:
158
+ click.echo(pk.get_rag_ready_output())
159
+
160
+ @click.command()
161
+ @click.option("--sprint", "sprint_id", required=True, help="Sprint ID to distill learnings from")
162
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON for machine consumption")
163
+ def distill(sprint_id, json_output):
164
+ """
165
+ Distills learnings from a completed sprint and updates ANTIGRAVITY.md.
166
+ """
167
+ console = Console()
168
+ distiller = SprintDistiller()
169
+
170
+ if not json_output:
171
+ console.print(f"[bold green]Distilling learnings from {sprint_id}...[/bold green]")
172
+
173
+ result = distiller.distill_sprint(sprint_id)
174
+
175
+ if "error" in result:
176
+ if json_output:
177
+ click.echo(json.dumps(result))
178
+ else:
179
+ console.print(f"[bold red]Error:[/bold red] {result['error']}")
180
+ else:
181
+ if json_output:
182
+ click.echo(json.dumps(result))
183
+ else:
184
+ console.print(f"[bold green]Success![/bold green] Extracted {result['learnings_extracted']} learnings from {sprint_id}.")
185
+ if result["updated"]:
186
+ console.print(f"Updated awareness file: [cyan]{result['updated']}[/cyan]")
187
+
188
+ @click.command()
189
+ @click.option("--dry-run", is_flag=True, help="Show what would be synced without actually syncing")
190
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON for machine consumption")
191
+ def sync(dry_run, json_output):
192
+ """
193
+ Syncs the local project context (specifications, governance, learnings, sprints) to the API.
194
+ """
195
+ if json_output:
196
+ # We might want to pass this down to sync_project_context
197
+ pass
198
+
199
+ asyncio.run(sync_project_context())
200
+
201
+ @click.command()
202
+ @click.option("--sync", is_flag=True, help="Sync alignment data to API")
203
+ def alignment(sync):
204
+ """
205
+ Tracks project progress, aligns with roadmap, and captures PR/commit aspects.
206
+ """
207
+ console = Console()
208
+ tracker = AlignmentTracker()
209
+
210
+ with console.status("[bold green]Analyzing alignment..."):
211
+ # Log current state
212
+ log_msg = tracker.log_current_state()
213
+
214
+ # Analyze roadmap (Agentic Semantic Check)
215
+ alignment_data = tracker.check_roadmap_alignment_agentic()
216
+
217
+ # Fetch PRs
218
+ prs = tracker.fetch_recent_prs()
219
+
220
+ # Capture suggestions
221
+ suggestions = tracker.capture_suggestions()
222
+
223
+ # Agentic Summary (Pass the detailed alignment data)
224
+ summary = tracker.summarize_alignment_agentic(alignment_data, prs, suggestions)
225
+
226
+ console.print(Panel(f"[bold]{log_msg}[/bold]", title="Alignment Log", expand=False))
227
+
228
+ # Agentic Summary Panel
229
+ console.print(Panel(summary, title="🤖 Agentic Assessment", border_style="cyan"))
230
+
231
+ # Roadmap Table
232
+ roadmap_table = Table(title="Semantic Roadmap Alignment")
233
+ roadmap_table.add_column("Status", style="bold")
234
+ roadmap_table.add_column("Aligned Items", style="cyan")
235
+
236
+ status_style = "green" if alignment_data["status"] == "ALIGNED" else ("yellow" if alignment_data["status"] == "DRIFTING" else "red")
237
+
238
+ roadmap_table.add_row(
239
+ f"[{status_style}]{alignment_data['status']}[/]",
240
+ ", ".join(alignment_data["aligned_items"]) if alignment_data["aligned_items"] else "None"
241
+ )
242
+ console.print(roadmap_table)
243
+
244
+ # Drift Items
245
+ if alignment_data.get("drift_items"):
246
+ console.print("\n[bold yellow]Detected Drift (Work not in Roadmap):[/bold yellow]")
247
+ for item in alignment_data["drift_items"]:
248
+ console.print(f" • {item}")
249
+
250
+ # PRs Table
251
+ if prs:
252
+ pr_table = Table(title="Recent Merged PRs")
253
+ pr_table.add_column("PR #", style="dim")
254
+ pr_table.add_column("Title", style="white")
255
+ pr_table.add_column("Author", style="cyan")
256
+
257
+ for pr in prs:
258
+ pr_table.add_row(str(pr["number"]), pr["title"], pr["author"]["login"])
259
+ console.print(pr_table)
260
+
261
+ # Suggestions
262
+ if suggestions:
263
+ console.print("\n[bold]Suggestions for Next Sprint:[/bold]")
264
+ for s in suggestions[:5]:
265
+ console.print(f" • {s}")
266
+
267
+ if sync:
268
+ click.echo("\nSyncing alignment data to API...")
269
+ # TODO: Implement API sync once endpoints are ready
270
+ click.echo("Sync scheduled for next platform update.")
@@ -0,0 +1,170 @@
1
+ import click
2
+ import asyncio
3
+ import threading
4
+ import uvicorn
5
+ import webbrowser
6
+ import signal
7
+ import sys
8
+ import socket
9
+ from ..ipc_auth import IPCAuthServer, get_token_from_ipc
10
+
11
+ # Global flag for graceful shutdown
12
+ shutdown_event = asyncio.Event()
13
+
14
+ def check_port_available(port: int) -> bool:
15
+ """Check if a port is available."""
16
+ try:
17
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
18
+ s.bind(("127.0.0.1", port))
19
+ return True
20
+ except OSError:
21
+ return False
22
+
23
+ async def check_servers_running() -> bool:
24
+ """Check if OneCoder servers are running."""
25
+ try:
26
+ token = await get_token_from_ipc()
27
+ return token is not None
28
+ except:
29
+ return False
30
+
31
+ def run_api_server(port: int = 8000):
32
+ """Runs the FastAPI server."""
33
+ from ..api import app
34
+ uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")
35
+
36
+ async def run_servers_async(port: int = 8000):
37
+ """Run both API and IPC servers concurrently."""
38
+ # Start API server in a thread
39
+ api_thread = threading.Thread(target=run_api_server, args=(port,), daemon=True)
40
+ api_thread.start()
41
+
42
+ # Give API server time to start
43
+ await asyncio.sleep(1)
44
+
45
+ # Start IPC server in main async loop
46
+ ipc_server = IPCAuthServer()
47
+ try:
48
+ await ipc_server.start()
49
+ except asyncio.CancelledError:
50
+ click.echo("Shutting down gracefully...")
51
+
52
+ def signal_handler(signum, frame):
53
+ """Handle shutdown signals."""
54
+ click.echo("\nReceived shutdown signal. Cleaning up...")
55
+ sys.exit(0)
56
+
57
+ @click.command()
58
+ @click.option("--port", default=8000, help="API server port")
59
+ def serve(port):
60
+ """Starts the Agent API and IPC Auth servers."""
61
+ # Check if port is available
62
+ if not check_port_available(port):
63
+ click.echo(f"Error: Port {port} is already in use.")
64
+ click.echo(f"Check for running processes: lsof -i :{port}")
65
+ return
66
+
67
+ click.echo(f"Starting OneCoder servers on port {port}...")
68
+ click.echo("Press Ctrl+C to stop.")
69
+
70
+ # Set up signal handlers
71
+ signal.signal(signal.SIGINT, signal_handler)
72
+ signal.signal(signal.SIGTERM, signal_handler)
73
+
74
+ # Run servers
75
+ try:
76
+ asyncio.run(run_servers_async(port))
77
+ except KeyboardInterrupt:
78
+ click.echo("\nShutting down...")
79
+
80
+ @click.command()
81
+ @click.option("--auto-start", is_flag=True, help="Auto-start servers if not running")
82
+ def web(auto_start):
83
+ """Launches the secure Web UI."""
84
+
85
+ async def launch():
86
+ # Check if servers are running
87
+ servers_running = await check_servers_running()
88
+
89
+ if not servers_running:
90
+ if auto_start:
91
+ click.echo("Servers not running. Starting in background...")
92
+ # Start servers in background thread
93
+ server_thread = threading.Thread(
94
+ target=lambda: asyncio.run(run_servers_async()), daemon=True
95
+ )
96
+ server_thread.start()
97
+ # Wait for servers to be ready
98
+ await asyncio.sleep(2)
99
+
100
+ # Verify they started
101
+ if not await check_servers_running():
102
+ click.echo("Error: Failed to start servers automatically.")
103
+ return
104
+ else:
105
+ click.echo("Error: Servers not running.")
106
+ click.echo(
107
+ "Run 'onecoder serve' in another terminal or use --auto-start"
108
+ )
109
+ return
110
+
111
+ # Fetch token
112
+ token = await get_token_from_ipc()
113
+ if not token:
114
+ click.echo("Error: Could not fetch authentication token.")
115
+ return
116
+
117
+ # Launch browser
118
+ url = f"http://127.0.0.1:8000/?token={token}"
119
+ click.echo(f"Launching Web UI: {url}")
120
+ webbrowser.open(url)
121
+
122
+ if auto_start:
123
+ click.echo("\nServers running in background. Press Ctrl+C to stop.")
124
+ try:
125
+ # Keep running if we auto-started
126
+ await asyncio.Event().wait()
127
+ except KeyboardInterrupt:
128
+ click.echo("\nShutting down...")
129
+
130
+ asyncio.run(launch())
131
+
132
+ @click.command()
133
+ @click.option("--auto-start", is_flag=True, help="Auto-start servers if not running")
134
+ @click.option("--api-url", help="Override the API URL")
135
+ def tui(auto_start, api_url):
136
+ """Launches the modern Textual TUI."""
137
+
138
+ async def launch():
139
+ # Check if servers are running
140
+ servers_running = await check_servers_running()
141
+
142
+ if not servers_running:
143
+ if auto_start:
144
+ click.echo("Servers not running. Starting in background...")
145
+ # Start servers in background thread
146
+ server_thread = threading.Thread(
147
+ target=lambda: asyncio.run(run_servers_async()), daemon=True
148
+ )
149
+ server_thread.start()
150
+ # Wait for servers to be ready
151
+ await asyncio.sleep(2)
152
+
153
+ # Verify they started
154
+ if not await check_servers_running():
155
+ click.echo("Error: Failed to start servers automatically.")
156
+ return
157
+ else:
158
+ click.echo("Error: Servers not running.")
159
+ click.echo(
160
+ "Run 'onecoder serve' in another terminal or use --auto-start"
161
+ )
162
+ return
163
+
164
+ # Launch Textual TUI
165
+ from ..tui.app import OneCoderApp
166
+
167
+ app = OneCoderApp(api_url=api_url)
168
+ await app.run_async()
169
+
170
+ asyncio.run(launch())