pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__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 (144) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/commands/auth.py ADDED
@@ -0,0 +1,309 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import base64
5
+ import json
6
+ import os
7
+ import sys
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Optional, Dict, Any
11
+
12
+ import click
13
+ from rich.console import Console
14
+
15
+ # Internal imports
16
+ try:
17
+ from ..auth_service import (
18
+ get_auth_status,
19
+ logout as service_logout,
20
+ JWT_CACHE_FILE,
21
+ )
22
+ from ..get_jwt_token import (
23
+ get_jwt_token,
24
+ AuthError,
25
+ NetworkError,
26
+ TokenError,
27
+ UserCancelledError,
28
+ RateLimitError,
29
+ )
30
+ except ImportError:
31
+ pass
32
+
33
+ console = Console()
34
+
35
+ # Constants
36
+ PDD_ENV = os.environ.get("PDD_ENV", "local")
37
+
38
+
39
+ def _load_firebase_api_key() -> str:
40
+ """Load the Firebase API key from environment or .env files."""
41
+ # 1. Check direct env var
42
+ env_key = os.environ.get("NEXT_PUBLIC_FIREBASE_API_KEY")
43
+ if env_key:
44
+ return env_key
45
+
46
+ # 2. Check .env files in current directory
47
+ candidates = [Path(".env"), Path(".env.local")]
48
+
49
+ for candidate in candidates:
50
+ if candidate.exists():
51
+ try:
52
+ content = candidate.read_text(encoding="utf-8")
53
+ for line in content.splitlines():
54
+ if line.strip().startswith("NEXT_PUBLIC_FIREBASE_API_KEY="):
55
+ return line.split("=", 1)[1].strip().strip('"').strip("'")
56
+ except Exception:
57
+ continue
58
+
59
+ return ""
60
+
61
+
62
+ def _get_client_id() -> Optional[str]:
63
+ """Get the GitHub Client ID for the current environment."""
64
+ return os.environ.get(f"GITHUB_CLIENT_ID_{PDD_ENV.upper()}") or os.environ.get("GITHUB_CLIENT_ID")
65
+
66
+
67
+ def _decode_jwt_payload(token: str) -> Dict[str, Any]:
68
+ """Decode JWT payload without verification to extract claims."""
69
+ try:
70
+ # JWT is header.payload.signature
71
+ parts = token.split(".")
72
+ if len(parts) != 3:
73
+ return {}
74
+
75
+ payload = parts[1]
76
+ # Add padding if needed
77
+ padding = len(payload) % 4
78
+ if padding:
79
+ payload += "=" * (4 - padding)
80
+
81
+ decoded = base64.urlsafe_b64decode(payload)
82
+ return json.loads(decoded)
83
+ except Exception:
84
+ return {}
85
+
86
+
87
+ @click.group("auth")
88
+ def auth_group():
89
+ """Manage PDD Cloud authentication."""
90
+ pass
91
+
92
+
93
+ @auth_group.command("login")
94
+ @click.option(
95
+ "--browser/--no-browser",
96
+ default=None,
97
+ help="Control browser opening (auto-detect if not specified)"
98
+ )
99
+ def login(browser: Optional[bool]):
100
+ """Authenticate with PDD Cloud via GitHub."""
101
+
102
+ api_key = _load_firebase_api_key()
103
+ if not api_key:
104
+ console.print("[red]Error: NEXT_PUBLIC_FIREBASE_API_KEY not found.[/red]")
105
+ console.print("Please set it in your environment or .env file.")
106
+ sys.exit(1)
107
+
108
+ client_id = _get_client_id()
109
+ app_name = "PDD CLI"
110
+
111
+ async def run_login():
112
+ try:
113
+ # Import remote session detection
114
+ from ..core.remote_session import should_skip_browser
115
+
116
+ # Determine if browser should be skipped
117
+ skip_browser, reason = should_skip_browser(explicit_flag=browser)
118
+
119
+ if skip_browser:
120
+ console.print(f"[yellow]Note: {reason}[/yellow]")
121
+ console.print("[yellow]Please open the authentication URL manually in a browser.[/yellow]")
122
+
123
+ # Pass no_browser parameter to get_jwt_token
124
+ token = await get_jwt_token(
125
+ firebase_api_key=api_key,
126
+ github_client_id=client_id,
127
+ app_name=app_name,
128
+ no_browser=skip_browser
129
+ )
130
+
131
+ if not token:
132
+ console.print("[red]Authentication failed: No token received.[/red]")
133
+ sys.exit(1)
134
+
135
+ # Decode token to get expiration
136
+ payload = _decode_jwt_payload(token)
137
+ expires_at = payload.get("exp")
138
+
139
+ # Ensure cache directory exists
140
+ if not JWT_CACHE_FILE.parent.exists():
141
+ JWT_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
142
+
143
+ # Save token and expiration to cache
144
+ # We store id_token for retrieval and expires_at for auth_service checks
145
+ cache_data = {
146
+ "id_token": token,
147
+ "expires_at": expires_at
148
+ }
149
+
150
+ JWT_CACHE_FILE.write_text(json.dumps(cache_data))
151
+
152
+ console.print("[green]Successfully authenticated to PDD Cloud.[/green]")
153
+
154
+ except AuthError as e:
155
+ console.print(f"[red]Authentication failed: {e}[/red]")
156
+ sys.exit(1)
157
+ except NetworkError as e:
158
+ console.print(f"[red]Network error: {e}[/red]")
159
+ sys.exit(1)
160
+ except TokenError as e:
161
+ console.print(f"[red]Token error: {e}[/red]")
162
+ sys.exit(1)
163
+ except UserCancelledError:
164
+ console.print("[yellow]Authentication cancelled by user.[/yellow]")
165
+ sys.exit(1)
166
+ except RateLimitError as e:
167
+ console.print(f"[red]Rate limit exceeded: {e}[/red]")
168
+ sys.exit(1)
169
+ except Exception as e:
170
+ console.print(f"[red]An unexpected error occurred: {e}[/red]")
171
+ sys.exit(1)
172
+
173
+ asyncio.run(run_login())
174
+
175
+
176
+ @auth_group.command("status")
177
+ def status():
178
+ """Check current authentication status."""
179
+ auth_status = get_auth_status()
180
+
181
+ if not auth_status.get("authenticated"):
182
+ console.print("Not authenticated.")
183
+ return
184
+
185
+ username = "Unknown"
186
+
187
+ # If we have a cached token, try to extract user info
188
+ if auth_status.get("cached") and JWT_CACHE_FILE.exists():
189
+ try:
190
+ data = json.loads(JWT_CACHE_FILE.read_text())
191
+ token = data.get("id_token")
192
+ if token:
193
+ payload = _decode_jwt_payload(token)
194
+ # Try to find a meaningful identifier
195
+ username = payload.get("email") or payload.get("sub")
196
+
197
+ # Check for GitHub specific claims if available in Firebase token
198
+ firebase_claims = payload.get("firebase", {})
199
+ identities = firebase_claims.get("identities", {})
200
+ if "github.com" in identities:
201
+ # identities['github.com'] is a list of IDs, not usernames usually
202
+ pass
203
+ except Exception:
204
+ pass
205
+
206
+ console.print(f"Authenticated as: [bold green]{username}[/bold green]")
207
+ sys.exit(0)
208
+
209
+
210
+ @auth_group.command("logout")
211
+ def logout_cmd():
212
+ """Log out of PDD Cloud."""
213
+ success, error = service_logout()
214
+ if success:
215
+ console.print("Logged out of PDD Cloud.")
216
+ else:
217
+ console.print(f"[red]Failed to logout: {error}[/red]")
218
+ # We don't exit with 1 here as partial logout might have occurred
219
+ # and the user is effectively logged out locally anyway.
220
+
221
+
222
+ @auth_group.command("token")
223
+ @click.option("--format", "output_format", type=click.Choice(["raw", "json"]), default="raw", help="Output format.")
224
+ def token_cmd(output_format: str):
225
+ """Print the current authentication token."""
226
+
227
+ token_str = None
228
+ expires_at = None
229
+
230
+ # Attempt to read valid token from cache
231
+ if JWT_CACHE_FILE.exists():
232
+ try:
233
+ data = json.loads(JWT_CACHE_FILE.read_text())
234
+ cached_token = data.get("id_token")
235
+ cached_exp = data.get("expires_at")
236
+
237
+ # Simple expiry check
238
+ if cached_token and cached_exp and cached_exp > time.time():
239
+ token_str = cached_token
240
+ expires_at = cached_exp
241
+ except Exception:
242
+ pass
243
+
244
+ if not token_str:
245
+ # Removed err=True because rich.console.Console.print does not support it
246
+ console.print("[red]No valid token available. Please login.[/red]")
247
+ sys.exit(1)
248
+
249
+ if output_format == "json":
250
+ output = {
251
+ "token": token_str,
252
+ "expires_at": expires_at
253
+ }
254
+ console.print_json(data=output)
255
+ else:
256
+ console.print(token_str)
257
+
258
+
259
+ @auth_group.command("clear-cache")
260
+ def clear_cache():
261
+ """Clear the JWT token cache.
262
+
263
+ This is useful when:
264
+ - Switching between environments (staging vs production)
265
+ - Experiencing authentication issues
266
+ - JWT token audience mismatch errors
267
+
268
+ After clearing the cache, you'll need to re-authenticate
269
+ with 'pdd auth login' or source the appropriate environment
270
+ setup script (e.g., setup_staging_env.sh).
271
+ """
272
+ if not JWT_CACHE_FILE.exists():
273
+ console.print("[yellow]No JWT cache found at ~/.pdd/jwt_cache[/yellow]")
274
+ console.print("Nothing to clear.")
275
+ return
276
+
277
+ try:
278
+ # Try to read cache before deleting to show what was cached
279
+ cache_data = json.loads(JWT_CACHE_FILE.read_text())
280
+ token = cache_data.get("id_token") or cache_data.get("jwt")
281
+ if token:
282
+ payload = _decode_jwt_payload(token)
283
+ aud = payload.get("aud") or payload.get("firebase", {}).get("aud")
284
+ exp = payload.get("exp")
285
+
286
+ console.print("[dim]Cached token info:[/dim]")
287
+ if aud:
288
+ console.print(f" Audience: {aud}")
289
+ if exp:
290
+ if exp > time.time():
291
+ time_remaining = int((exp - time.time()) / 60)
292
+ console.print(f" Expires in: {time_remaining} minutes")
293
+ else:
294
+ console.print(" Status: [red]Expired[/red]")
295
+ except Exception:
296
+ # If we can't read the cache, that's fine - just proceed with deletion
297
+ pass
298
+
299
+ # Delete the cache file
300
+ try:
301
+ JWT_CACHE_FILE.unlink()
302
+ console.print("[green]✓[/green] JWT cache cleared successfully")
303
+ console.print()
304
+ console.print("[dim]To re-authenticate:[/dim]")
305
+ console.print(" - For production: [bold]pdd auth login[/bold]")
306
+ console.print(" - For staging: [bold]source setup_staging_env.sh[/bold]")
307
+ except OSError as e:
308
+ console.print(f"[red]Failed to clear cache: {e}[/red]")
309
+ sys.exit(1)
@@ -0,0 +1,290 @@
1
+ """
2
+ PDD Connect Command.
3
+
4
+ This module provides the `pdd connect` CLI command which launches a local
5
+ REST server to enable the web frontend to interact with PDD.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import os
12
+ import webbrowser
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ import click
17
+
18
+ # Handle optional dependencies - uvicorn may not be installed
19
+ try:
20
+ import uvicorn
21
+ except ImportError:
22
+ uvicorn = None
23
+
24
+ # Internal imports
25
+ # We wrap this in a try/except block to allow the module to be imported
26
+ # even if the server dependencies are not present (e.g. in partial environments)
27
+ try:
28
+ from ..server.app import create_app
29
+ except (ImportError, ValueError):
30
+ def create_app(*args, **kwargs):
31
+ raise ImportError("Could not import pdd.server.app.create_app. Ensure server dependencies are installed.")
32
+
33
+
34
+ @click.command("connect")
35
+ @click.option(
36
+ "--port",
37
+ default=9876,
38
+ help="Port to listen on",
39
+ show_default=True,
40
+ type=int,
41
+ )
42
+ @click.option(
43
+ "--host",
44
+ default="127.0.0.1",
45
+ help="Host to bind to",
46
+ show_default=True,
47
+ )
48
+ @click.option(
49
+ "--allow-remote",
50
+ is_flag=True,
51
+ help="Allow non-localhost connections",
52
+ )
53
+ @click.option(
54
+ "--token",
55
+ help="Bearer token for authentication",
56
+ default=None,
57
+ )
58
+ @click.option(
59
+ "--no-browser",
60
+ is_flag=True,
61
+ help="Don't open browser automatically",
62
+ )
63
+ @click.option(
64
+ "--frontend-url",
65
+ help="Custom frontend URL",
66
+ default=None,
67
+ )
68
+ @click.option(
69
+ "--local-only",
70
+ is_flag=True,
71
+ help="Skip cloud registration (local access only)",
72
+ )
73
+ @click.option(
74
+ "--session-name",
75
+ help="Custom session name for identification",
76
+ default=None,
77
+ )
78
+ @click.pass_context
79
+ def connect(
80
+ ctx: click.Context,
81
+ port: int,
82
+ host: str,
83
+ allow_remote: bool,
84
+ token: Optional[str],
85
+ no_browser: bool,
86
+ frontend_url: Optional[str],
87
+ local_only: bool,
88
+ session_name: Optional[str],
89
+ ) -> None:
90
+ """
91
+ Launch the local REST server for the PDD web frontend.
92
+
93
+ This command starts a FastAPI server that exposes the PDD functionality
94
+ via a REST API. It automatically opens the web interface in your default
95
+ browser unless --no-browser is specified.
96
+
97
+ For authenticated users, the session is automatically registered with
98
+ PDD Cloud for remote access. Use --local-only to skip cloud registration.
99
+ """
100
+ # Check uvicorn is available
101
+ if uvicorn is None:
102
+ click.echo(click.style("Error: 'uvicorn' is not installed. Please install it to use the connect command.", fg="red"))
103
+ ctx.exit(1)
104
+
105
+ # 1. Determine Project Root
106
+ # We assume the current working directory is the project root
107
+ project_root = Path.cwd()
108
+
109
+ # 2. Security Checks & Configuration
110
+ if allow_remote:
111
+ if not token:
112
+ click.echo(click.style(
113
+ "SECURITY WARNING: You are allowing remote connections without an authentication token.",
114
+ fg="red", bold=True
115
+ ))
116
+ click.echo("Anyone with access to your network could execute code on your machine.")
117
+ if not click.confirm("Do you want to proceed?"):
118
+ ctx.exit(1)
119
+
120
+ # If user explicitly asked for remote but left host as localhost,
121
+ # bind to all interfaces to actually allow remote connections.
122
+ if host == "127.0.0.1":
123
+ host = "0.0.0.0"
124
+ click.echo(click.style("Binding to 0.0.0.0 to allow remote connections.", fg="yellow"))
125
+ else:
126
+ # Warn if binding to non-localhost without explicit allow-remote
127
+ if host not in ("127.0.0.1", "localhost"):
128
+ click.echo(click.style(
129
+ f"Warning: Binding to {host} without --allow-remote flag. "
130
+ "External connections may be blocked or insecure.",
131
+ fg="yellow"
132
+ ))
133
+
134
+ # 3. Determine URLs
135
+ # The server URL is where the API lives
136
+ server_url = f"http://{host}:{port}"
137
+
138
+ # The frontend URL is what we open in the browser
139
+ # If binding to 0.0.0.0, we still use localhost for the local browser
140
+ browser_host = "localhost" if host == "0.0.0.0" else host
141
+ target_url = frontend_url if frontend_url else f"http://{browser_host}:{port}"
142
+
143
+ # 4. Configure CORS
144
+ # We need to allow the frontend to talk to the backend
145
+ allowed_origins = [
146
+ "http://localhost:3000",
147
+ "http://127.0.0.1:3000",
148
+ "http://localhost:5173",
149
+ "http://127.0.0.1:5173",
150
+ f"http://localhost:{port}",
151
+ f"http://127.0.0.1:{port}",
152
+ # PDD Cloud frontend
153
+ "https://pdd.dev",
154
+ "https://www.pdd.dev",
155
+ ]
156
+ if frontend_url:
157
+ allowed_origins.append(frontend_url)
158
+
159
+ # 4.5 Cloud Session Registration (automatic for authenticated users)
160
+ session_manager = None
161
+ cloud_url = None
162
+ if not local_only:
163
+ try:
164
+ from ..core.cloud import CloudConfig
165
+ from ..remote_session import (
166
+ RemoteSessionManager,
167
+ RemoteSessionError,
168
+ set_active_session_manager,
169
+ )
170
+
171
+ # Check if user is authenticated
172
+ jwt_token = CloudConfig.get_jwt_token(verbose=False)
173
+ if not jwt_token:
174
+ click.echo(click.style(
175
+ "Not authenticated. Running in local-only mode.",
176
+ dim=True
177
+ ))
178
+ click.echo(click.style(
179
+ "Run 'pdd login' to enable remote access via cloud.",
180
+ dim=True
181
+ ))
182
+ else:
183
+ click.echo("Registering session with PDD Cloud...")
184
+ session_manager = RemoteSessionManager(jwt_token, project_root)
185
+ try:
186
+ # Register with cloud - no public URL needed, cloud hosts everything
187
+ cloud_url = asyncio.run(session_manager.register(
188
+ session_name=session_name,
189
+ ))
190
+ # Heartbeat will be started by the app's lifespan manager
191
+ set_active_session_manager(session_manager)
192
+
193
+ click.echo(click.style(
194
+ "Session registered with PDD Cloud!", fg="green", bold=True
195
+ ))
196
+ # TODO: Re-enable when production /connect page is deployed
197
+ # click.echo(f" Access URL: {click.style(cloud_url, fg='cyan', underline=True)}")
198
+ # click.echo(click.style(
199
+ # " Share this URL to access your PDD session from any browser.",
200
+ # dim=True
201
+ # ))
202
+ except RemoteSessionError as e:
203
+ click.echo(click.style(
204
+ f"Warning: Failed to register with cloud: {e.message}",
205
+ fg="yellow"
206
+ ))
207
+ click.echo(click.style(
208
+ "Running in local-only mode.",
209
+ dim=True
210
+ ))
211
+ session_manager = None
212
+ except ImportError as e:
213
+ click.echo(click.style(
214
+ f"Running in local-only mode (cloud dependencies not available).",
215
+ dim=True
216
+ ))
217
+ else:
218
+ click.echo(click.style(
219
+ "Running in local-only mode (--local-only flag set).",
220
+ dim=True
221
+ ))
222
+
223
+ # 5. Initialize Server App
224
+ try:
225
+ # Pass token via environment variable if provided, as create_app might not take it directly
226
+ if token:
227
+ os.environ["PDD_ACCESS_TOKEN"] = token
228
+
229
+ app = create_app(project_root, allowed_origins=allowed_origins)
230
+ except Exception as e:
231
+ click.echo(click.style(f"Failed to initialize server: {e}", fg="red", bold=True))
232
+ ctx.exit(1)
233
+
234
+ # 6. Print Status Messages
235
+ click.echo(click.style(f"Starting PDD server on {server_url}", fg="green", bold=True))
236
+ click.echo(f"Project Root: {click.style(str(project_root), fg='blue')}")
237
+ click.echo(f"API Documentation: {click.style(f'{server_url}/docs', underline=True)}")
238
+ click.echo(f"Local Frontend: {click.style(target_url, underline=True)}")
239
+ # TODO: Re-enable when production /connect page is deployed
240
+ # if cloud_url:
241
+ # click.echo(f"Remote Access: {click.style(cloud_url, fg='cyan', underline=True)}")
242
+ click.echo(click.style("Press Ctrl+C to stop the server", dim=True))
243
+
244
+ # 7. Open Browser
245
+ if not no_browser:
246
+ # Import remote session detection
247
+ from ..core.remote_session import is_remote_session
248
+
249
+ is_remote, reason = is_remote_session()
250
+ if is_remote:
251
+ click.echo(click.style(f"Note: {reason}", fg="yellow"))
252
+ click.echo("Opening browser may not work in remote sessions. Use the URL above to connect manually.")
253
+
254
+ click.echo("Opening browser...")
255
+ try:
256
+ webbrowser.open(target_url)
257
+ except Exception as e:
258
+ click.echo(click.style(f"Could not open browser: {e}", fg="yellow"))
259
+ click.echo(f"Please open {target_url} manually in your browser.")
260
+
261
+ # 8. Run Server
262
+ try:
263
+ # Run uvicorn
264
+ # Disable access_log to avoid noisy polling logs - custom middleware handles important logging
265
+ uvicorn.run(
266
+ app,
267
+ host=host,
268
+ port=port,
269
+ log_level="warning", # Only show warnings and errors from uvicorn
270
+ access_log=False # Custom middleware handles request logging
271
+ )
272
+ except KeyboardInterrupt:
273
+ click.echo(click.style("\nServer stopping...", fg="yellow", bold=True))
274
+ except Exception as e:
275
+ click.echo(click.style(f"\nServer error: {e}", fg="red", bold=True))
276
+ ctx.exit(1)
277
+ finally:
278
+ # Clean up cloud session if registered
279
+ if session_manager is not None:
280
+ click.echo("Deregistering from PDD Cloud...")
281
+ try:
282
+ from ..remote_session import set_active_session_manager
283
+ asyncio.run(session_manager.stop_heartbeat())
284
+ asyncio.run(session_manager.deregister())
285
+ set_active_session_manager(None)
286
+ click.echo(click.style("Session deregistered.", fg="green"))
287
+ except Exception as e:
288
+ click.echo(click.style(f"Warning: Error during session cleanup: {e}", fg="yellow"))
289
+
290
+ click.echo(click.style("Goodbye!", fg="blue"))