polycoding 0.1.0__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.
cli/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """Central CLI tool for Polycode.
2
+
3
+ Entry point: `polycode` command.
4
+
5
+ Usage:
6
+ polycode server webhook # Start webhook server
7
+ polycode server socketio # Start Socket.IO server
8
+ polycode server flower # Start Flower monitoring
9
+ polycode worker start # Start Celery worker
10
+ polycode flow run ralph # Run a flow manually
11
+ polycode project sync # Sync issues to project
12
+ """
13
+
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.text import Text
17
+
18
+ console = Console()
19
+
20
+ __version__ = "0.1.0"
21
+
22
+
23
+ def print_banner():
24
+ """Print pretty CLI banner."""
25
+ banner = Text.assemble(
26
+ ("🚀 ", "bold cyan"),
27
+ ("polycode", "bold white"),
28
+ (f" v{__version__}", "dim"),
29
+ )
30
+ console.print(Panel(banner, border_style="cyan"))
31
+
32
+
33
+ def print_error(message: str) -> None:
34
+ """Print error message in red."""
35
+ console.print(f"[bold red]❌ {message}[/]")
36
+
37
+
38
+ def print_success(message: str) -> None:
39
+ """Print success message in green."""
40
+ console.print(f"[bold green]✓ {message}[/]")
41
+
42
+
43
+ def print_warning(message: str) -> None:
44
+ """Print warning message in yellow."""
45
+ console.print(f"[bold yellow]âš ī¸ {message}[/]")
46
+
47
+
48
+ def print_info(message: str) -> None:
49
+ """Print info message in blue."""
50
+ console.print(f"[bold blue]â„šī¸ {message}[/]")
51
+
52
+
53
+ __all__ = ["print_error", "print_success", "print_warning", "print_info", "console"]
cli/db.py ADDED
@@ -0,0 +1,67 @@
1
+ """Database management commands for Polycode CLI."""
2
+
3
+ import sys
4
+
5
+ import typer
6
+ from sqlalchemy import create_engine
7
+
8
+ from cli import print_error, print_info, print_success, print_warning
9
+ from cli.utils import get_logger
10
+ from persistence.postgres import Base
11
+
12
+ log = get_logger(__name__)
13
+ db_app = typer.Typer(help="Database management commands")
14
+
15
+
16
+ @db_app.command("init")
17
+ def db_init(
18
+ drop: bool = typer.Option(False, "--drop", help="Drop existing tables first"),
19
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
20
+ ) -> None:
21
+ """Initialize database tables.
22
+
23
+ Creates all tables defined in the persistence layer.
24
+ """
25
+ if verbose:
26
+ import cli.utils
27
+
28
+ cli.utils.setup_logging("DEBUG")
29
+
30
+ print_info("🔧 Initializing database...")
31
+
32
+ try:
33
+ from project_manager.config import settings
34
+
35
+ connection_string = settings.DATABASE_URL
36
+ engine = create_engine(connection_string)
37
+
38
+ if drop:
39
+ print_info("🗑 Dropping existing tables...")
40
+ Base.metadata.drop_all(engine)
41
+ print_success("Existing tables dropped")
42
+
43
+ print_info("📊 Creating tables...")
44
+ Base.metadata.create_all(engine)
45
+ print_success("Database initialized successfully")
46
+
47
+ except Exception as e:
48
+ print_error(f"Database initialization failed: {e}")
49
+ log.exception(f"Database init failed: {e}")
50
+ sys.exit(1)
51
+
52
+
53
+ @db_app.command("migrate")
54
+ def db_migrate(
55
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
56
+ ) -> None:
57
+ """Run database migrations.
58
+
59
+ Note: Migration support not yet implemented.
60
+ """
61
+ if verbose:
62
+ import cli.utils
63
+
64
+ cli.utils.setup_logging("DEBUG")
65
+
66
+ print_warning("âš ī¸ Migration support not yet implemented")
67
+ print_info("Use 'polycode db init --drop' to recreate tables")
cli/flow.py ADDED
@@ -0,0 +1,187 @@
1
+ """Flow execution commands for Polycode CLI."""
2
+
3
+ import json
4
+ import sys
5
+ from typing import Any
6
+
7
+ import typer
8
+ from alive_progress import alive_bar
9
+ from uuid import NAMESPACE_DNS, uuid5
10
+ from rich import box
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ from bootstrap import init_plugins
15
+ from cli import print_error, print_info, print_success
16
+ from cli.utils import get_logger
17
+ from flows.base import KickoffIssue, KickoffRepo
18
+ from project_manager.github import GitHubProjectManager
19
+ from project_manager.types import ProjectConfig, StatusMapping
20
+
21
+ log = get_logger(__name__)
22
+ console = Console()
23
+ flow_app = typer.Typer(help="Flow execution commands")
24
+
25
+
26
+ @flow_app.command("list")
27
+ def flow_list(
28
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
29
+ ) -> None:
30
+ """List all available flows."""
31
+ if verbose:
32
+ import cli.utils
33
+
34
+ cli.utils.setup_logging("DEBUG")
35
+
36
+ print_info("📋 Available flows:")
37
+
38
+ flows: dict[str, str] = {
39
+ "ralph": "Ralph - Feature development orchestrator with per-story commits",
40
+ }
41
+
42
+ table = Table(title="Available Flows", box=box.ROUNDED)
43
+ table.add_column("Name", style="cyan", header_style="bold")
44
+ table.add_column("Description", style="white")
45
+
46
+ for name, description in flows.items():
47
+ table.add_row(name, description)
48
+
49
+ console.print(table)
50
+ print_success(f"Found {len(flows)} flow(s)")
51
+
52
+
53
+ @flow_app.command("run")
54
+ def flow_run(
55
+ flow_name: str = typer.Argument(..., help="Flow name to execute"),
56
+ repo_owner: str = typer.Option(None, "--repo-owner", "-o", help="Repository owner (required)"),
57
+ repo_name: str = typer.Option(None, "--repo-name", "-r", help="Repository name (required)"),
58
+ issue_number: int = typer.Option(None, "--issue-number", "-i", help="Issue number to process (required)"),
59
+ project_id: str | None = typer.Option(None, "--project-id", "-p", help="GitHub project ID/number"),
60
+ provider: str = typer.Option("github", "--provider", help="Provider type (default: github)"),
61
+ token: str | None = typer.Option(None, "--token", "-t", help="GitHub token (or use GITHUB_TOKEN env var)"),
62
+ status_mapping: str | None = typer.Option(None, "--status-mapping", help="Custom status mapping as JSON"),
63
+ plugins: list[str] = typer.Option([], "--plugin", help="Load additional plugins (repeatable)"),
64
+ extra: str | None = typer.Option(None, "--extra", help="Extra configuration as JSON (for communications, etc.)"),
65
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
66
+ ) -> None:
67
+ """Run a flow manually against a repository.
68
+
69
+ Requires full project configuration including provider, repository details,
70
+ and optional extras for custom behavior.
71
+ """
72
+ if verbose:
73
+ import cli.utils
74
+
75
+ cli.utils.setup_logging("DEBUG")
76
+
77
+ if not repo_owner:
78
+ print_error("--repo-owner is required when specifying repo configuration")
79
+ sys.exit(1)
80
+
81
+ if not repo_name:
82
+ print_error("--repo-name is required when specifying repo configuration")
83
+ sys.exit(1)
84
+
85
+ if not issue_number:
86
+ print_error("--issue-number is required")
87
+ sys.exit(1)
88
+
89
+ print_info(f"🚀 Running flow '{flow_name}'")
90
+ print_info(f" Repository: {repo_owner}/{repo_name}")
91
+ print_info(f" Issue: #{issue_number}")
92
+
93
+ try:
94
+ init_plugins()
95
+
96
+ if status_mapping:
97
+ try:
98
+ mapping_data = json.loads(status_mapping)
99
+ status_map = StatusMapping.from_dict(mapping_data)
100
+ except json.JSONDecodeError as e:
101
+ print_error(f"Invalid status mapping JSON: {e}")
102
+ sys.exit(1)
103
+ else:
104
+ status_map = StatusMapping()
105
+
106
+ extra_data: dict[str, Any] = {}
107
+ if extra:
108
+ try:
109
+ extra_data = json.loads(extra)
110
+ except json.JSONDecodeError as e:
111
+ print_error(f"Invalid extra JSON: {e}")
112
+ sys.exit(1)
113
+
114
+ if plugins:
115
+ print_info(f" Plugins: {', '.join(plugins)}")
116
+
117
+ config = ProjectConfig(
118
+ provider=provider,
119
+ repo_owner=repo_owner,
120
+ repo_name=repo_name,
121
+ project_identifier=project_id,
122
+ token=token,
123
+ status_mapping=status_map,
124
+ extra=extra_data,
125
+ )
126
+
127
+ manager = GitHubProjectManager(config)
128
+ issue = manager.get_issue(issue_number)
129
+
130
+ if not issue:
131
+ print_error(f"Issue #{issue_number} not found")
132
+ sys.exit(1)
133
+
134
+ print_info(f" Title: {issue.title}")
135
+
136
+ from bootstrap import bootstrap
137
+
138
+ bootstrap(config={"modules": {p: {} for p in plugins}})
139
+
140
+ flow_identifier = f"{manager.config.repo_owner}/{manager.config.repo_name}/{issue_number}"
141
+
142
+ comments = manager.get_comments(issue_number)
143
+
144
+ kickoff_issue = KickoffIssue(
145
+ id=issue_number,
146
+ flow_id=uuid5(NAMESPACE_DNS, flow_identifier),
147
+ title=issue.title,
148
+ body=issue.body or "",
149
+ comments=[
150
+ {
151
+ "id": c.id,
152
+ "user": c.user.login if c.user else None,
153
+ "body": c.body,
154
+ "created_at": c.created_at.isoformat() if c.created_at else None,
155
+ }
156
+ for c in comments
157
+ ],
158
+ memory_prefix=f"{manager.config.repo_owner}/{manager.config.repo_name}",
159
+ repository=KickoffRepo(
160
+ owner=manager.config.repo_owner,
161
+ repository=manager.config.repo_name,
162
+ ),
163
+ project_config=config,
164
+ )
165
+ with alive_bar(
166
+ 0,
167
+ title=f"Running {flow_name}",
168
+ spinner="squares",
169
+ force_tty=True,
170
+ enrich_print=False,
171
+ enrich_offset=0,
172
+ monitor=False,
173
+ ) as bar:
174
+ bar.text = f"Processing issue #{issue_number}: {issue.title}"
175
+
176
+ from flows.ralph import ralph_kickoff
177
+
178
+ ralph_kickoff(kickoff_issue)
179
+
180
+ bar()
181
+
182
+ print_success(f"Flow '{flow_name}' completed for issue #{issue_number}")
183
+
184
+ except Exception as e:
185
+ print_error(f"Flow execution failed: {e}")
186
+ log.exception(f"Flow execution failed: {e}")
187
+ sys.exit(1)
cli/main.py ADDED
@@ -0,0 +1,44 @@
1
+ """Main CLI entry point using Typer."""
2
+
3
+ import typer
4
+
5
+ from . import console, db, flow, print_banner, project, server, worker
6
+ from .utils import setup_logging
7
+
8
+ app = typer.Typer(
9
+ name="polycode",
10
+ help="🚀 Polycode: Multi-agent software development automation",
11
+ add_completion=False,
12
+ no_args_is_help=True,
13
+ pretty_exceptions_enable=False,
14
+ )
15
+
16
+ app.add_typer(server.server_app, name="server", help="Server management commands")
17
+ app.add_typer(worker.worker_app, name="worker", help="Celery worker management commands")
18
+ app.add_typer(flow.flow_app, name="flow", help="Flow execution commands")
19
+ app.add_typer(project.project_app, name="project", help="GitHub project management commands")
20
+ app.add_typer(db.db_app, name="db", help="Database management commands")
21
+
22
+
23
+ @app.callback()
24
+ def main(
25
+ log_level: str = typer.Option(
26
+ "INFO",
27
+ "--log-level",
28
+ "-l",
29
+ help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
30
+ ),
31
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose (DEBUG) logging"),
32
+ ) -> None:
33
+ """Polycode CLI - Multi-agent software development automation."""
34
+ level = "DEBUG" if verbose else log_level
35
+ setup_logging(level)
36
+
37
+ if verbose or level.upper() == "DEBUG":
38
+ console.print("[dim]🔍 Verbose mode enabled[/]")
39
+
40
+ print_banner()
41
+
42
+
43
+ if __name__ == "__main__":
44
+ app()
cli/project.py ADDED
@@ -0,0 +1,166 @@
1
+ """Project management commands for Polycode CLI."""
2
+
3
+ import sys
4
+
5
+ import typer
6
+ from rich import box
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from cli import print_error, print_info, print_success
11
+ from cli.utils import get_logger
12
+
13
+ log = get_logger(__name__)
14
+ console = Console()
15
+
16
+ project_app = typer.Typer(help="GitHub project management commands")
17
+
18
+
19
+ def create_manager_from_env():
20
+ """Create project manager from environment variables.
21
+
22
+ Returns:
23
+ Configured project manager
24
+ """
25
+ import os
26
+
27
+ from project_manager.config import settings
28
+ from project_manager.types import ProjectConfig, StatusMapping
29
+
30
+ provider = settings.PROJECT_PROVIDER or "github"
31
+ repo_owner = settings.REPO_OWNER or os.getenv("REPO_OWNER")
32
+ repo_name = settings.REPO_NAME or os.getenv("REPO_NAME")
33
+ project_identifier = settings.PROJECT_IDENTIFIER or os.getenv("PROJECT_IDENTIFIER")
34
+
35
+ status_mapping = StatusMapping(
36
+ todo=os.getenv("STATUS_TODO", "Todo"),
37
+ ready=os.getenv("STATUS_READY", "Ready"),
38
+ in_progress=os.getenv("STATUS_IN_PROGRESS", "In progress"),
39
+ reviewing=os.getenv("STATUS_REVIEWING", "In review"),
40
+ done=os.getenv("STATUS_DONE", "Done"),
41
+ blocked=os.getenv("STATUS_BLOCKED", "Blocked"),
42
+ )
43
+
44
+ config = ProjectConfig(
45
+ provider=provider,
46
+ repo_owner=repo_owner or "",
47
+ repo_name=repo_name or "",
48
+ project_identifier=project_identifier,
49
+ status_mapping=status_mapping,
50
+ )
51
+
52
+ if provider == "github":
53
+ from project_manager.github import GitHubProjectManager
54
+
55
+ return GitHubProjectManager(config)
56
+ else:
57
+ raise ValueError(f"Unsupported provider: {provider}")
58
+
59
+
60
+ @project_app.command("sync")
61
+ def project_sync(
62
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
63
+ ) -> None:
64
+ """Sync all open issues to project."""
65
+ if verbose:
66
+ import cli.utils
67
+
68
+ cli.utils.setup_logging("DEBUG")
69
+
70
+ print_info("🔄 Syncing issues to project...")
71
+
72
+ try:
73
+ manager = create_manager_from_env()
74
+ added = manager.sync_issues_to_project()
75
+
76
+ if added > 0:
77
+ print_success(f"Added {added} issues to project")
78
+ else:
79
+ print_info("All issues already in project")
80
+
81
+ except Exception as e:
82
+ print_error(f"Sync failed: {e}")
83
+ log.exception(f"Sync failed: {e}")
84
+ sys.exit(1)
85
+
86
+
87
+ @project_app.command("list")
88
+ def project_list(
89
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
90
+ ) -> None:
91
+ """List all project items."""
92
+ if verbose:
93
+ import cli.utils
94
+
95
+ cli.utils.setup_logging("DEBUG")
96
+
97
+ print_info("📋 Listing project items...")
98
+
99
+ try:
100
+ manager = create_manager_from_env()
101
+ items = manager.get_project_items()
102
+
103
+ table = Table(title="Project Items", box=box.ROUNDED)
104
+ table.add_column("#", style="cyan", header_style="bold")
105
+ table.add_column("Status", style="green")
106
+ table.add_column("Title", style="white")
107
+
108
+ for item in items:
109
+ status = item.status or "No status"
110
+ table.add_row(f"#{item.issue_number}", f"[{status}]", item.title)
111
+
112
+ console.print(table)
113
+ print_success(f"Found {len(items)} item(s)")
114
+
115
+ except Exception as e:
116
+ print_error(f"List failed: {e}")
117
+ log.exception(f"List failed: {e}")
118
+ sys.exit(1)
119
+
120
+
121
+ @project_app.command("status")
122
+ def project_status(
123
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
124
+ ) -> None:
125
+ """Show current flow status from GitHub Project."""
126
+ if verbose:
127
+ import cli.utils
128
+
129
+ cli.utils.setup_logging("DEBUG")
130
+
131
+ print_info("🔍 Checking flow status...")
132
+
133
+ try:
134
+ manager = create_manager_from_env()
135
+
136
+ items = manager.get_project_items()
137
+
138
+ from project_manager.types import IssueStatus
139
+
140
+ ready_status = manager.config.status_mapping.to_provider_status(IssueStatus.READY)
141
+ in_progress_status = manager.config.status_mapping.to_provider_status(IssueStatus.IN_PROGRESS)
142
+
143
+ ready = [item for item in items if item.status == ready_status]
144
+ in_progress = [item for item in items if item.status == in_progress_status]
145
+
146
+ if in_progress:
147
+ item = in_progress[0]
148
+ console.print(f"[bold green]✓ Flow running:[/] Issue #{item.issue_number}")
149
+ console.print(f" Title: {item.title}")
150
+ console.print(f" Status: {item.status}")
151
+ else:
152
+ console.print("[yellow]✗ No flow currently running[/]")
153
+
154
+ console.print(f"\n[bold]Ready:[/] {len(ready)}, [bold]In progress:[/] {len(in_progress)}")
155
+
156
+ if ready:
157
+ console.print("\n[cyan]Next ready issues:[/]")
158
+ for item in ready[:5]:
159
+ console.print(f" #{item.issue_number}: {item.title}")
160
+
161
+ print_success("Status check complete")
162
+
163
+ except Exception as e:
164
+ print_error(f"Status check failed: {e}")
165
+ log.exception(f"Status check failed: {e}")
166
+ sys.exit(1)
cli/server.py ADDED
@@ -0,0 +1,127 @@
1
+ """Server commands for Polycode CLI."""
2
+
3
+ import sys
4
+
5
+ import typer
6
+ import uvicorn
7
+ from rich.console import Console
8
+
9
+ from cli import print_error, print_info
10
+ from cli.utils import get_logger
11
+ from github_app.app import app as github_app
12
+
13
+ log = get_logger(__name__)
14
+ console = Console()
15
+
16
+ server_app = typer.Typer(help="Server management commands")
17
+
18
+
19
+ @server_app.command("webhook")
20
+ def webhook_server(
21
+ host: str = typer.Option("0.0.0.0", "--host", "-h", help="Webhook server host"),
22
+ port: int = typer.Option(8000, "--port", "-p", help="Webhook server port"),
23
+ reload: bool = typer.Option(False, "--reload", help="Enable auto-reload (development)"),
24
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
25
+ ) -> None:
26
+ """Start webhook server for GitHub events.
27
+
28
+ Uses unified GitHub App webhook server.
29
+ For GitHub App webhooks (multi-repo), set GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY.
30
+ For legacy webhooks (single repo), set GITHUB_TOKEN and repo env vars.
31
+ """
32
+ if verbose:
33
+ import cli.utils
34
+
35
+ cli.utils.setup_logging("DEBUG")
36
+
37
+ print_info(f"🚀 Starting webhook server on {host}:{port}")
38
+
39
+ if reload:
40
+ print_info("🔁 Auto-reload enabled")
41
+
42
+ try:
43
+ uvicorn.run(
44
+ github_app,
45
+ host=host,
46
+ port=port,
47
+ reload=reload,
48
+ log_level="debug" if verbose else "warning",
49
+ )
50
+ except KeyboardInterrupt:
51
+ print_info("⏚ Server stopped by user")
52
+ sys.exit(0)
53
+ except Exception as e:
54
+ print_error(f"Failed to start server: {e}")
55
+ log.exception(f"Server startup failed: {e}")
56
+ sys.exit(1)
57
+
58
+
59
+ @server_app.command("socketio")
60
+ def socketio_server(
61
+ host: str = typer.Option("0.0.0.0", "--host", "-h", help="Socket.IO server host"),
62
+ port: int = typer.Option(8001, "--port", "-p", help="Socket.IO server port"),
63
+ reload: bool = typer.Option(False, "--reload", help="Enable auto-reload (development)"),
64
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
65
+ ) -> None:
66
+ """Start Socket.IO streaming server.
67
+
68
+ Bridges Redis pub/sub to Socket.IO clients for real-time flow updates.
69
+ """
70
+ if verbose:
71
+ import cli.utils
72
+
73
+ cli.utils.setup_logging("DEBUG")
74
+
75
+ print_info(f"🔌 Starting Socket.IO server on {host}:{port}")
76
+
77
+ if reload:
78
+ print_info("🔁 Auto-reload enabled")
79
+
80
+ try:
81
+ uvicorn.run(
82
+ "channels.stream.server_app:asgi_app",
83
+ host=host,
84
+ port=port,
85
+ reload=reload,
86
+ log_level="debug" if verbose else "warning",
87
+ )
88
+ except KeyboardInterrupt:
89
+ print_info("⏚ Server stopped by user")
90
+ sys.exit(0)
91
+ except Exception as e:
92
+ print_error(f"Failed to start Socket.IO server: {e}")
93
+ log.exception(f"Socket.IO server startup failed: {e}")
94
+ sys.exit(1)
95
+
96
+
97
+ @server_app.command("flower")
98
+ def flower_server(
99
+ host: str = typer.Option("0.0.0.0", "--host", "-h", help="Flower server host"),
100
+ port: int = typer.Option(5555, "--port", "-p", help="Flower server port"),
101
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
102
+ ) -> None:
103
+ """Start Flower monitoring UI for Celery workers.
104
+
105
+ Flower provides real-time monitoring of Celery tasks and workers.
106
+ """
107
+ if verbose:
108
+ import cli.utils
109
+
110
+ cli.utils.setup_logging("DEBUG")
111
+
112
+ print_info(f"🌸 Starting Flower monitoring on {host}:{port}")
113
+
114
+ try:
115
+ uvicorn.run(
116
+ "flower:app",
117
+ host=host,
118
+ port=port,
119
+ log_level="debug" if verbose else "warning",
120
+ )
121
+ except KeyboardInterrupt:
122
+ print_info("⏚ Flower stopped by user")
123
+ sys.exit(0)
124
+ except Exception as e:
125
+ print_error(f"Failed to start Flower: {e}")
126
+ log.exception(f"Flower startup failed: {e}")
127
+ sys.exit(1)