pulse-framework 0.1.62__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 (126) hide show
  1. pulse/__init__.py +1493 -0
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +1086 -0
  4. pulse/channel.py +607 -0
  5. pulse/cli/__init__.py +0 -0
  6. pulse/cli/cmd.py +575 -0
  7. pulse/cli/dependencies.py +181 -0
  8. pulse/cli/folder_lock.py +134 -0
  9. pulse/cli/helpers.py +271 -0
  10. pulse/cli/logging.py +102 -0
  11. pulse/cli/models.py +35 -0
  12. pulse/cli/packages.py +262 -0
  13. pulse/cli/processes.py +292 -0
  14. pulse/cli/secrets.py +39 -0
  15. pulse/cli/uvicorn_log_config.py +87 -0
  16. pulse/code_analysis.py +38 -0
  17. pulse/codegen/__init__.py +0 -0
  18. pulse/codegen/codegen.py +359 -0
  19. pulse/codegen/templates/__init__.py +0 -0
  20. pulse/codegen/templates/layout.py +106 -0
  21. pulse/codegen/templates/route.py +345 -0
  22. pulse/codegen/templates/routes_ts.py +42 -0
  23. pulse/codegen/utils.py +20 -0
  24. pulse/component.py +237 -0
  25. pulse/components/__init__.py +0 -0
  26. pulse/components/for_.py +83 -0
  27. pulse/components/if_.py +86 -0
  28. pulse/components/react_router.py +94 -0
  29. pulse/context.py +108 -0
  30. pulse/cookies.py +322 -0
  31. pulse/decorators.py +344 -0
  32. pulse/dom/__init__.py +0 -0
  33. pulse/dom/elements.py +1024 -0
  34. pulse/dom/events.py +445 -0
  35. pulse/dom/props.py +1250 -0
  36. pulse/dom/svg.py +0 -0
  37. pulse/dom/tags.py +328 -0
  38. pulse/dom/tags.pyi +480 -0
  39. pulse/env.py +178 -0
  40. pulse/form.py +538 -0
  41. pulse/helpers.py +541 -0
  42. pulse/hooks/__init__.py +0 -0
  43. pulse/hooks/core.py +452 -0
  44. pulse/hooks/effects.py +88 -0
  45. pulse/hooks/init.py +668 -0
  46. pulse/hooks/runtime.py +464 -0
  47. pulse/hooks/setup.py +254 -0
  48. pulse/hooks/stable.py +138 -0
  49. pulse/hooks/state.py +192 -0
  50. pulse/js/__init__.py +125 -0
  51. pulse/js/__init__.pyi +115 -0
  52. pulse/js/_types.py +299 -0
  53. pulse/js/array.py +339 -0
  54. pulse/js/console.py +50 -0
  55. pulse/js/date.py +119 -0
  56. pulse/js/document.py +145 -0
  57. pulse/js/error.py +140 -0
  58. pulse/js/json.py +66 -0
  59. pulse/js/map.py +97 -0
  60. pulse/js/math.py +69 -0
  61. pulse/js/navigator.py +79 -0
  62. pulse/js/number.py +57 -0
  63. pulse/js/obj.py +81 -0
  64. pulse/js/object.py +172 -0
  65. pulse/js/promise.py +172 -0
  66. pulse/js/pulse.py +115 -0
  67. pulse/js/react.py +495 -0
  68. pulse/js/regexp.py +57 -0
  69. pulse/js/set.py +124 -0
  70. pulse/js/string.py +38 -0
  71. pulse/js/weakmap.py +53 -0
  72. pulse/js/weakset.py +48 -0
  73. pulse/js/window.py +205 -0
  74. pulse/messages.py +202 -0
  75. pulse/middleware.py +471 -0
  76. pulse/plugin.py +96 -0
  77. pulse/proxy.py +242 -0
  78. pulse/py.typed +0 -0
  79. pulse/queries/__init__.py +0 -0
  80. pulse/queries/client.py +609 -0
  81. pulse/queries/common.py +101 -0
  82. pulse/queries/effect.py +55 -0
  83. pulse/queries/infinite_query.py +1418 -0
  84. pulse/queries/mutation.py +295 -0
  85. pulse/queries/protocol.py +136 -0
  86. pulse/queries/query.py +1314 -0
  87. pulse/queries/store.py +120 -0
  88. pulse/react_component.py +88 -0
  89. pulse/reactive.py +1208 -0
  90. pulse/reactive_extensions.py +1172 -0
  91. pulse/render_session.py +768 -0
  92. pulse/renderer.py +584 -0
  93. pulse/request.py +205 -0
  94. pulse/routing.py +598 -0
  95. pulse/serializer.py +279 -0
  96. pulse/state.py +556 -0
  97. pulse/test_helpers.py +15 -0
  98. pulse/transpiler/__init__.py +111 -0
  99. pulse/transpiler/assets.py +81 -0
  100. pulse/transpiler/builtins.py +1029 -0
  101. pulse/transpiler/dynamic_import.py +130 -0
  102. pulse/transpiler/emit_context.py +49 -0
  103. pulse/transpiler/errors.py +96 -0
  104. pulse/transpiler/function.py +611 -0
  105. pulse/transpiler/id.py +18 -0
  106. pulse/transpiler/imports.py +341 -0
  107. pulse/transpiler/js_module.py +336 -0
  108. pulse/transpiler/modules/__init__.py +33 -0
  109. pulse/transpiler/modules/asyncio.py +57 -0
  110. pulse/transpiler/modules/json.py +24 -0
  111. pulse/transpiler/modules/math.py +265 -0
  112. pulse/transpiler/modules/pulse/__init__.py +5 -0
  113. pulse/transpiler/modules/pulse/tags.py +250 -0
  114. pulse/transpiler/modules/typing.py +63 -0
  115. pulse/transpiler/nodes.py +1987 -0
  116. pulse/transpiler/py_module.py +135 -0
  117. pulse/transpiler/transpiler.py +1100 -0
  118. pulse/transpiler/vdom.py +256 -0
  119. pulse/types/__init__.py +0 -0
  120. pulse/types/event_handler.py +50 -0
  121. pulse/user_session.py +386 -0
  122. pulse/version.py +69 -0
  123. pulse_framework-0.1.62.dist-info/METADATA +198 -0
  124. pulse_framework-0.1.62.dist-info/RECORD +126 -0
  125. pulse_framework-0.1.62.dist-info/WHEEL +4 -0
  126. pulse_framework-0.1.62.dist-info/entry_points.txt +3 -0
pulse/cli/cmd.py ADDED
@@ -0,0 +1,575 @@
1
+ """
2
+ Command-line interface for Pulse UI.
3
+ This module provides the CLI commands for running the server and generating routes.
4
+ """
5
+ # typer relies on function calls used as default values
6
+ # pyright: reportCallInDefaultInitializer=false
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import subprocess
12
+ import sys
13
+ from collections.abc import Sequence
14
+ from pathlib import Path
15
+ from typing import Callable, cast
16
+
17
+ import typer
18
+
19
+ from pulse.cli.dependencies import (
20
+ DependencyError,
21
+ DependencyPlan,
22
+ DependencyResolutionError,
23
+ check_web_dependencies,
24
+ prepare_web_dependencies,
25
+ )
26
+ from pulse.cli.folder_lock import FolderLock
27
+ from pulse.cli.helpers import load_app_from_target
28
+ from pulse.cli.logging import CLILogger
29
+ from pulse.cli.models import AppLoadResult, CommandSpec
30
+ from pulse.cli.processes import execute_commands
31
+ from pulse.cli.secrets import resolve_dev_secret
32
+ from pulse.cli.uvicorn_log_config import get_log_config
33
+ from pulse.env import (
34
+ ENV_PULSE_DISABLE_CODEGEN,
35
+ ENV_PULSE_HOST,
36
+ ENV_PULSE_PORT,
37
+ ENV_PULSE_REACT_SERVER_ADDRESS,
38
+ ENV_PULSE_SECRET,
39
+ PulseEnv,
40
+ env,
41
+ )
42
+ from pulse.helpers import find_available_port
43
+ from pulse.version import __version__ as PULSE_PY_VERSION
44
+
45
+ cli = typer.Typer(
46
+ name="pulse",
47
+ help="Pulse UI - Python to TypeScript bridge with server-side callbacks",
48
+ no_args_is_help=True,
49
+ )
50
+
51
+
52
+ @cli.command(
53
+ "run", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
54
+ )
55
+ def run(
56
+ ctx: typer.Context,
57
+ app_file: str = typer.Argument(
58
+ ...,
59
+ help=("App target: 'path/to/app.py[:var]' (default :app) or 'module.path:var'"),
60
+ ),
61
+ address: str = typer.Option(
62
+ "localhost",
63
+ "--address",
64
+ help="Host uvicorn binds to",
65
+ ),
66
+ port: int = typer.Option(8000, "--port", help="Port uvicorn binds to"),
67
+ # Env flags
68
+ dev: bool = typer.Option(False, "--dev", help="Run in development mode"),
69
+ prod: bool = typer.Option(False, "--prod", help="Run in production mode"),
70
+ plain: bool = typer.Option(
71
+ False, "--plain", help="Use plain output without colors or emojis"
72
+ ),
73
+ server_only: bool = typer.Option(False, "--server-only", "--backend-only"),
74
+ web_only: bool = typer.Option(False, "--web-only"),
75
+ react_server_address: str | None = typer.Option(
76
+ None,
77
+ "--react-server-address",
78
+ help="Full URL of React server (required for single-server + --server-only)",
79
+ ),
80
+ reload: bool | None = typer.Option(None, "--reload/--no-reload"),
81
+ find_port: bool = typer.Option(True, "--find-port/--no-find-port"),
82
+ verbose: bool = typer.Option(
83
+ False, "--verbose", help="Show all logs without filtering"
84
+ ),
85
+ ):
86
+ """Run the Pulse server and web development server together."""
87
+ extra_flags = list(ctx.args)
88
+
89
+ # Validate mode flags (dev is default if neither specified)
90
+ if dev and prod:
91
+ logger = CLILogger("dev", plain=plain)
92
+ logger.error("Please specify only one of --dev or --prod.")
93
+ raise typer.Exit(1)
94
+
95
+ # Set mode: prod if specified, otherwise dev (default)
96
+ mode: PulseEnv = "prod" if prod else "dev"
97
+ env.pulse_env = mode
98
+ logger = CLILogger(mode, plain=plain)
99
+
100
+ # Turn on reload in dev only
101
+ if reload is None:
102
+ reload = env.pulse_env == "dev"
103
+
104
+ if server_only and web_only:
105
+ logger.error("Cannot use --server-only and --web-only at the same time.")
106
+ raise typer.Exit(1)
107
+
108
+ if find_port:
109
+ port = find_available_port(port)
110
+
111
+ logger.print(f"Loading app from {app_file}")
112
+ app_ctx = load_app_from_target(app_file, logger)
113
+ _apply_app_context_to_env(app_ctx)
114
+ app_instance = app_ctx.app
115
+
116
+ is_single_server = app_instance.mode == "single-server"
117
+ if is_single_server:
118
+ logger.print("Single-server mode")
119
+
120
+ # In single-server + server-only mode, require explicit React server address
121
+ if is_single_server and server_only:
122
+ if not react_server_address:
123
+ logger.error(
124
+ "--react-server-address is required when using single-server mode with --server-only."
125
+ )
126
+ raise typer.Exit(1)
127
+ os.environ[ENV_PULSE_REACT_SERVER_ADDRESS] = react_server_address
128
+
129
+ web_root = app_instance.codegen.cfg.web_root
130
+ if not web_root.exists() and not server_only:
131
+ logger.error(f"Directory not found: {web_root.absolute()}")
132
+ raise typer.Exit(1)
133
+
134
+ dev_secret: str | None = None
135
+ if app_instance.env != "prod":
136
+ dev_secret = os.environ.get(ENV_PULSE_SECRET) or resolve_dev_secret(
137
+ web_root if web_root.exists() else app_ctx.app_file
138
+ )
139
+
140
+ if env.pulse_env == "dev" and not server_only:
141
+ try:
142
+ to_add = check_web_dependencies(
143
+ web_root,
144
+ pulse_version=PULSE_PY_VERSION,
145
+ )
146
+ except DependencyResolutionError as exc:
147
+ logger.error(str(exc))
148
+ raise typer.Exit(1) from None
149
+ except DependencyError as exc:
150
+ logger.error(str(exc))
151
+ raise typer.Exit(1) from None
152
+
153
+ if to_add:
154
+ try:
155
+ dep_plan = prepare_web_dependencies(
156
+ web_root,
157
+ pulse_version=PULSE_PY_VERSION,
158
+ )
159
+ if dep_plan:
160
+ _run_dependency_plan(logger, web_root, dep_plan)
161
+ except subprocess.CalledProcessError:
162
+ logger.error("Failed to install web dependencies with Bun.")
163
+ raise typer.Exit(1) from None
164
+
165
+ server_args = extra_flags if not web_only else []
166
+ web_args = extra_flags if web_only else []
167
+
168
+ commands: list[CommandSpec] = []
169
+
170
+ # Track readiness for announcement
171
+ server_ready = {"server": False, "web": False}
172
+ announced = False
173
+
174
+ def mark_web_ready() -> None:
175
+ server_ready["web"] = True
176
+ check_and_announce()
177
+
178
+ def mark_server_ready() -> None:
179
+ server_ready["server"] = True
180
+ check_and_announce()
181
+
182
+ def check_and_announce() -> None:
183
+ """Announce when all required servers are ready."""
184
+ nonlocal announced
185
+ if announced:
186
+ return
187
+
188
+ needs_server = not web_only
189
+ needs_web = not server_only
190
+
191
+ if needs_server and not server_ready["server"]:
192
+ return
193
+ if needs_web and not server_ready["web"]:
194
+ return
195
+
196
+ # All required servers are ready, show announcement
197
+ announced = True
198
+ protocol = "http" if address in ("127.0.0.1", "localhost") else "https"
199
+ server_url = f"{protocol}://{address}:{port}"
200
+ logger.write_ready_announcement(address, port, server_url)
201
+
202
+ # Build web command first (when needed) so we can set PULSE_REACT_SERVER_ADDRESS
203
+ # before building the uvicorn command, which needs that env var
204
+ if not server_only:
205
+ web_port = find_available_port(5173)
206
+ web_cmd = build_web_command(
207
+ web_root=web_root,
208
+ extra_args=web_args,
209
+ port=web_port,
210
+ mode=app_instance.env,
211
+ ready_pattern=r"localhost:\d+",
212
+ on_ready=mark_web_ready,
213
+ plain=plain,
214
+ )
215
+ commands.append(web_cmd)
216
+ # Set env var so app can read the React server address (only used in single-server mode)
217
+ env.react_server_address = f"http://localhost:{web_port}"
218
+
219
+ if not web_only:
220
+ server_cmd = build_uvicorn_command(
221
+ app_ctx=app_ctx,
222
+ address=address,
223
+ port=port,
224
+ reload_enabled=reload,
225
+ extra_args=server_args,
226
+ dev_secret=dev_secret,
227
+ server_only=server_only,
228
+ web_root=web_root,
229
+ verbose=verbose,
230
+ ready_pattern=r"Application startup complete",
231
+ on_ready=mark_server_ready,
232
+ plain=plain,
233
+ )
234
+ commands.append(server_cmd)
235
+
236
+ with FolderLock(web_root):
237
+ try:
238
+ exit_code = execute_commands(
239
+ commands,
240
+ tag_mode=logger.get_tag_mode(),
241
+ )
242
+ raise typer.Exit(exit_code)
243
+ except RuntimeError as exc:
244
+ logger.error(str(exc))
245
+ raise typer.Exit(1) from None
246
+
247
+
248
+ @cli.command("generate")
249
+ def generate(
250
+ app_file: str = typer.Argument(
251
+ ..., help="App target: 'path.py[:var]' (default :app) or 'module:var'"
252
+ ),
253
+ # Mode flags
254
+ dev: bool = typer.Option(False, "--dev", help="Generate in development mode"),
255
+ ci: bool = typer.Option(False, "--ci", help="Generate in CI mode"),
256
+ prod: bool = typer.Option(False, "--prod", help="Generate in production mode"),
257
+ plain: bool = typer.Option(
258
+ False, "--plain", help="Use plain output without colors or emojis"
259
+ ),
260
+ ):
261
+ """Generate TypeScript routes without starting the server."""
262
+ # Validate mode flags
263
+ mode_flags = [
264
+ name for flag, name in [(dev, "dev"), (ci, "ci"), (prod, "prod")] if flag
265
+ ]
266
+ if len(mode_flags) > 1:
267
+ logger = CLILogger("dev", plain=plain)
268
+ logger.error("Please specify only one of --dev, --ci, or --prod.")
269
+ raise typer.Exit(1)
270
+
271
+ # Set mode: use specified mode, otherwise dev (default)
272
+ mode: PulseEnv = cast(PulseEnv, mode_flags[0]) if mode_flags else "dev"
273
+ env.pulse_env = mode
274
+ logger = CLILogger(mode, plain=plain)
275
+
276
+ logger.print(f"Generating routes from {app_file}")
277
+ env.codegen_disabled = False
278
+ app_ctx = load_app_from_target(app_file, logger)
279
+ _apply_app_context_to_env(app_ctx)
280
+ app = app_ctx.app
281
+
282
+ # In CI or prod mode, server_address must be provided
283
+ if (ci or prod) and not app.server_address:
284
+ logger.error(
285
+ "server_address must be provided when generating in CI or production mode. "
286
+ + "Set it in your App constructor or via the PULSE_SERVER_ADDRESS environment variable."
287
+ )
288
+ raise typer.Exit(1)
289
+
290
+ addr = app.server_address or "http://localhost:8000"
291
+ try:
292
+ app.run_codegen(addr)
293
+ except Exception:
294
+ logger.error("Failed to generate routes")
295
+ logger.print_exception()
296
+ raise typer.Exit(1) from None
297
+
298
+ route_count = len(app.routes.flat_tree)
299
+ if route_count > 0:
300
+ logger.success(
301
+ f"Generated {route_count} route{'s' if route_count != 1 else ''}"
302
+ )
303
+ else:
304
+ logger.warning("No routes found")
305
+
306
+
307
+ @cli.command("check")
308
+ def check(
309
+ app_file: str = typer.Argument(
310
+ ..., help="App target: 'path.py[:var]' (default :app) or 'module:var'"
311
+ ),
312
+ fix: bool = typer.Option(
313
+ False, "--fix", help="Install missing or outdated dependencies"
314
+ ),
315
+ # Mode flags
316
+ dev: bool = typer.Option(False, "--dev", help="Run in development mode"),
317
+ ci: bool = typer.Option(False, "--ci", help="Run in CI mode"),
318
+ prod: bool = typer.Option(False, "--prod", help="Run in production mode"),
319
+ plain: bool = typer.Option(
320
+ False, "--plain", help="Use plain output without colors or emojis"
321
+ ),
322
+ ):
323
+ """Check if web project dependencies are in sync with Pulse app requirements."""
324
+ # Validate mode flags
325
+ mode_flags = [
326
+ name for flag, name in [(dev, "dev"), (ci, "ci"), (prod, "prod")] if flag
327
+ ]
328
+ if len(mode_flags) > 1:
329
+ logger = CLILogger("dev", plain=plain)
330
+ logger.error("Please specify only one of --dev, --ci, or --prod.")
331
+ raise typer.Exit(1)
332
+
333
+ # Set mode: use specified mode, otherwise dev (default)
334
+ mode: PulseEnv = cast(PulseEnv, mode_flags[0]) if mode_flags else "dev"
335
+ env.pulse_env = mode
336
+ logger = CLILogger(mode, plain=plain)
337
+
338
+ logger.print(f"Checking dependencies for {app_file}")
339
+ app_ctx = load_app_from_target(app_file, logger)
340
+ _apply_app_context_to_env(app_ctx)
341
+ app_instance = app_ctx.app
342
+
343
+ web_root = app_instance.codegen.cfg.web_root
344
+ if not web_root.exists():
345
+ logger.error(f"Directory not found: {web_root.absolute()}")
346
+ raise typer.Exit(1)
347
+
348
+ try:
349
+ to_add = check_web_dependencies(
350
+ web_root,
351
+ pulse_version=PULSE_PY_VERSION,
352
+ )
353
+ except DependencyResolutionError as exc:
354
+ logger.error(str(exc))
355
+ raise typer.Exit(1) from None
356
+ except DependencyError as exc:
357
+ logger.error(str(exc))
358
+ raise typer.Exit(1) from None
359
+
360
+ if not to_add:
361
+ logger.success("Dependencies in sync")
362
+ return
363
+
364
+ logger.print("Missing dependencies:")
365
+ for pkg in to_add:
366
+ logger.print(f" {pkg}")
367
+
368
+ if not fix:
369
+ logger.print("Run 'pulse check --fix' to install")
370
+ return
371
+
372
+ # Apply fix
373
+ try:
374
+ dep_plan = prepare_web_dependencies(
375
+ web_root,
376
+ pulse_version=PULSE_PY_VERSION,
377
+ )
378
+ if dep_plan:
379
+ _run_dependency_plan(logger, web_root, dep_plan)
380
+ logger.success("Dependencies synced")
381
+ except subprocess.CalledProcessError:
382
+ logger.error("Failed to install web dependencies with Bun.")
383
+ raise typer.Exit(1) from None
384
+
385
+
386
+ def build_uvicorn_command(
387
+ *,
388
+ app_ctx: AppLoadResult,
389
+ address: str,
390
+ port: int,
391
+ reload_enabled: bool,
392
+ extra_args: Sequence[str],
393
+ dev_secret: str | None,
394
+ server_only: bool,
395
+ web_root: Path,
396
+ verbose: bool = False,
397
+ ready_pattern: str | None = None,
398
+ on_ready: Callable[[], None] | None = None,
399
+ plain: bool = False,
400
+ ) -> CommandSpec:
401
+ cwd = app_ctx.server_cwd or app_ctx.app_dir or Path.cwd()
402
+ app_import = f"{app_ctx.module_name}:{app_ctx.app_var}.asgi_factory"
403
+ args: list[str] = [
404
+ sys.executable,
405
+ "-m",
406
+ "uvicorn",
407
+ app_import,
408
+ "--host",
409
+ address,
410
+ "--port",
411
+ str(port),
412
+ "--factory",
413
+ ]
414
+
415
+ if reload_enabled:
416
+ args.append("--reload")
417
+ args.extend(["--reload-include", "*.css"])
418
+ app_dir = app_ctx.app_dir or Path.cwd()
419
+ args.extend(["--reload-dir", str(app_dir)])
420
+ if web_root.exists():
421
+ args.extend(["--reload-dir", str(web_root)])
422
+ pulse_dir = str(app_ctx.app.codegen.cfg.pulse_dir)
423
+ pulse_app_dir = web_root / "app" / pulse_dir
424
+ rel_path = Path(os.path.relpath(pulse_app_dir, cwd))
425
+ if not rel_path.is_absolute():
426
+ args.extend(["--reload-exclude", str(rel_path)])
427
+ args.extend(["--reload-exclude", str(rel_path / "**")])
428
+
429
+ if app_ctx.app.env == "prod":
430
+ args.extend(production_flags())
431
+
432
+ if extra_args:
433
+ args.extend(extra_args)
434
+ if plain:
435
+ args.append("--no-use-colors")
436
+
437
+ command_env = os.environ.copy()
438
+ command_env.update(
439
+ {
440
+ "PYTHONUNBUFFERED": "1",
441
+ ENV_PULSE_HOST: address,
442
+ ENV_PULSE_PORT: str(port),
443
+ }
444
+ )
445
+ if plain:
446
+ command_env["NO_COLOR"] = "1"
447
+ command_env["FORCE_COLOR"] = "0"
448
+ else:
449
+ command_env["FORCE_COLOR"] = "1"
450
+ # Pass React server address to uvicorn process if set
451
+ if ENV_PULSE_REACT_SERVER_ADDRESS in os.environ:
452
+ command_env[ENV_PULSE_REACT_SERVER_ADDRESS] = os.environ[
453
+ ENV_PULSE_REACT_SERVER_ADDRESS
454
+ ]
455
+ if app_ctx.app.env == "prod" and server_only:
456
+ command_env[ENV_PULSE_DISABLE_CODEGEN] = "1"
457
+ if dev_secret:
458
+ command_env[ENV_PULSE_SECRET] = dev_secret
459
+
460
+ # Apply custom log config to filter noisy requests (dev/ci only)
461
+ if app_ctx.app.env != "prod" and not verbose:
462
+ import json
463
+ import tempfile
464
+
465
+ log_config = get_log_config()
466
+ log_config_file = Path(tempfile.gettempdir()) / "pulse_uvicorn_log_config.json"
467
+ log_config_file.write_text(json.dumps(log_config))
468
+ args.extend(["--log-config", str(log_config_file)])
469
+
470
+ return CommandSpec(
471
+ name="server",
472
+ args=args,
473
+ cwd=cwd,
474
+ env=command_env,
475
+ ready_pattern=ready_pattern,
476
+ on_ready=on_ready,
477
+ )
478
+
479
+
480
+ def build_web_command(
481
+ *,
482
+ web_root: Path,
483
+ extra_args: Sequence[str],
484
+ port: int | None = None,
485
+ mode: PulseEnv = "dev",
486
+ ready_pattern: str | None = None,
487
+ on_ready: Callable[[], None] | None = None,
488
+ plain: bool = False,
489
+ ) -> CommandSpec:
490
+ command_env = os.environ.copy()
491
+ if mode == "prod":
492
+ # Production: use built server
493
+ args = ["bun", "run", "start"]
494
+ else:
495
+ # Development: use dev server
496
+ args = ["bun", "run", "dev"]
497
+
498
+ if port is not None:
499
+ if mode == "prod":
500
+ # react-router-serve uses PORT environment variable
501
+ # Don't add --port flag for production
502
+ command_env["PORT"] = str(port)
503
+ else:
504
+ # react-router dev accepts --port flag
505
+ args.extend(["--port", str(port)])
506
+ if extra_args:
507
+ args.extend(extra_args)
508
+
509
+ command_env.update(
510
+ {
511
+ "PYTHONUNBUFFERED": "1",
512
+ }
513
+ )
514
+ if plain:
515
+ command_env["NO_COLOR"] = "1"
516
+ command_env["FORCE_COLOR"] = "0"
517
+ else:
518
+ command_env["FORCE_COLOR"] = "1"
519
+
520
+ return CommandSpec(
521
+ name="web",
522
+ args=args,
523
+ cwd=web_root,
524
+ env=command_env,
525
+ ready_pattern=ready_pattern,
526
+ on_ready=on_ready,
527
+ )
528
+
529
+
530
+ def _apply_app_context_to_env(app_ctx: AppLoadResult) -> None:
531
+ if app_ctx.app_file:
532
+ env.pulse_app_file = str(app_ctx.app_file)
533
+ if app_ctx.app_dir:
534
+ env.pulse_app_dir = str(app_ctx.app_dir)
535
+
536
+
537
+ def _run_dependency_plan(
538
+ logger: CLILogger, web_root: Path, plan: DependencyPlan
539
+ ) -> None:
540
+ if plan.to_add:
541
+ logger.print(f"Installing dependencies in {web_root}")
542
+ subprocess.run(plan.command, cwd=web_root, check=True)
543
+
544
+
545
+ def main():
546
+ """Main CLI entry point."""
547
+ try:
548
+ cli()
549
+ except SystemExit:
550
+ # Let typer.Exit and sys.exit propagate normally (no traceback)
551
+ raise
552
+ except Exception:
553
+ logger = CLILogger(env.pulse_env)
554
+ logger.print_exception()
555
+ sys.exit(1)
556
+
557
+
558
+ def production_flags():
559
+ # Prefer uvloop/http tools automatically if installed
560
+ flags: list[str] = []
561
+ try:
562
+ __import__("uvloop") # runtime check only
563
+ flags.extend(["--loop", "uvloop"])
564
+ except Exception:
565
+ pass
566
+ try:
567
+ __import__("httptools")
568
+ flags.extend(["--http", "httptools"])
569
+ except Exception:
570
+ pass
571
+ return flags
572
+
573
+
574
+ if __name__ == "__main__":
575
+ main()