pywire-cli 0.2.0__tar.gz

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.
@@ -0,0 +1,81 @@
1
+ # ─────────────────────── Python ──────────────────────
2
+ .venv/
3
+ venv/
4
+ env/
5
+ ENV/
6
+ __pycache__/
7
+ *.pyc
8
+ *.py[cod]
9
+ *$py.class
10
+ *.egg-info/
11
+ dist/
12
+ build/
13
+ .coverage
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .tox/
18
+ **/_version.py
19
+
20
+ # ─────────────────────── Rust ────────────────────────
21
+ target/
22
+
23
+ # ─────────────────────── Node ────────────────────────
24
+ node_modules/
25
+ coverage/
26
+ *.log
27
+ .astro/
28
+
29
+ # ─────────────────────── IDE ─────────────────────────
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+
35
+ # ─────────────────────── OS ──────────────────────────
36
+ .DS_Store
37
+ Thumbs.db
38
+
39
+ # ─────────────────────── Env ─────────────────────────
40
+ .env
41
+ .env.*
42
+ !.env.example
43
+
44
+ # ─────────────────────── pywire package ──────────────
45
+ .pywire/
46
+ scratch/
47
+ packages/pywire/src/pywire/*.so
48
+ packages/pywire/src/pywire/*.pyd
49
+ packages/pywire/src/pywire/client/dist/
50
+ # Un-ignore static dir — maturin must include it in WASM wheels
51
+ !packages/pywire/src/pywire/static/
52
+ !packages/pywire/src/pywire/static/**
53
+ # Re-ignore built JS bundles (generated by pnpm build)
54
+ packages/pywire/src/pywire/static/pywire.*.min.js
55
+ packages/pywire/emsdk/
56
+ emsdk/
57
+ packages/pywire/.pyodide-*
58
+
59
+ # ─────────────────────── vscode-pywire ───────────────
60
+ packages/vscode-pywire/out/
61
+ packages/vscode-pywire/bundled/
62
+ *.vsix
63
+
64
+ # ─────────────────────── tree-sitter-pywire ──────────
65
+ packages/tree-sitter-pywire/Cargo.lock
66
+ packages/tree-sitter-pywire/*.wasm
67
+
68
+ # ─────────────────────── Docs ────────────────────────
69
+ docs/public/sw.js
70
+ docs/public/pywire-worker.js
71
+
72
+ # ─────────────────────── Certs ───────────────────────
73
+ localhost.pem
74
+ localhost-key.pem
75
+
76
+ # ─────────────────────── Other ───────────────────────
77
+ chatbot.db
78
+ .gemini/
79
+ .claude/worktrees/
80
+ gha-creds-*.json
81
+ .agent/settings.local.json
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0](https://github.com/pywire/pywire/compare/pywire-cli-v0.1.0...pywire-cli-v0.2.0) (2026-04-22)
4
+
5
+
6
+ ### Features
7
+
8
+ * split pywire CLI, add pywire check static analysis, oneliner scaffolder ([#156](https://github.com/pywire/pywire/issues/156)) ([d2b8a9d](https://github.com/pywire/pywire/commit/d2b8a9dff0871ff1d9b4cabc64a855842c48fbb6))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **prettier-plugin-pywire:** bump for consistency ([33fc161](https://github.com/pywire/pywire/commit/33fc1613c5a70dfc4e8b00d1da3d99f2dc849925))
14
+ * **pywire-language-server:** bump for release ([88aa449](https://github.com/pywire/pywire/commit/88aa449a1a55ece6145f1f21bc7b1ca807e394ca))
15
+ * **pywire-language-server:** fix PyPI publish by using explicit build output dir ([8dec973](https://github.com/pywire/pywire/commit/8dec97382276ce0f139cc5abd89b0bd54806bc7d))
16
+ * **pywire-language-server:** trigger release PR update ([77491d0](https://github.com/pywire/pywire/commit/77491d02b5f51957b83b1ae3a320cbab53fb56a8))
17
+ * **tree-sitter-pywire:** bump for consistency ([42b40bd](https://github.com/pywire/pywire/commit/42b40bd3ec2711445865c772573b52774857bbc0))
18
+ * **vscode-pywire:** fix CI publish with --no-dependencies flag ([1ef1d72](https://github.com/pywire/pywire/commit/1ef1d72018f4c02bf29a923dd33e4f96bdf813ac))
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: pywire-cli
3
+ Version: 0.2.0
4
+ Summary: Command-line tools for PyWire projects: dev, build, deploy, check
5
+ Project-URL: Homepage, https://pywire.dev
6
+ Project-URL: Documentation, https://pywire.dev/docs
7
+ Project-URL: Repository, https://github.com/pywire/pywire
8
+ Project-URL: Issues, https://github.com/pywire/pywire/issues
9
+ Author-email: Reece Holmdahl <reece@pywire.dev>
10
+ License-Expression: MIT
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: jinja2>=3.1.0
21
+ Requires-Dist: pywire-templates
22
+ Requires-Dist: pywire[build]
23
+ Requires-Dist: rich-click>=1.9.6
24
+ Requires-Dist: textual>=7.4.0
25
+ Requires-Dist: uvicorn[standard]>=0.27.0
26
+ Requires-Dist: watchfiles>=0.21.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
29
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
30
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
32
+ Requires-Dist: ty; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # pywire-cli
36
+
37
+ Command-line tools for [PyWire](https://pywire.dev) projects: `dev`, `run`, `build`, `deploy`, `check`.
38
+
39
+ Normally installed via the `pywire[cli]` extra:
40
+
41
+ ```sh
42
+ uv add pywire[cli]
43
+ # or
44
+ pip install pywire[cli]
45
+ ```
46
+
47
+ This installs `pywire-cli` alongside the core framework and makes the `pywire` command available.
48
+
49
+ ## Commands
50
+
51
+ - `pywire dev` — run the development server with hot reload
52
+ - `pywire run` — run the production server
53
+ - `pywire build` — compile the project for production
54
+ - `pywire deploy` — generate deployment configs (Docker, Render, Fly, Railway, Cloudflare)
55
+ - `pywire check` — run static analysis on the project (non-serializable wires, reactivity errors, redundant patterns)
56
+ - `pywire config` — read/write PyWire settings
@@ -0,0 +1,22 @@
1
+ # pywire-cli
2
+
3
+ Command-line tools for [PyWire](https://pywire.dev) projects: `dev`, `run`, `build`, `deploy`, `check`.
4
+
5
+ Normally installed via the `pywire[cli]` extra:
6
+
7
+ ```sh
8
+ uv add pywire[cli]
9
+ # or
10
+ pip install pywire[cli]
11
+ ```
12
+
13
+ This installs `pywire-cli` alongside the core framework and makes the `pywire` command available.
14
+
15
+ ## Commands
16
+
17
+ - `pywire dev` — run the development server with hot reload
18
+ - `pywire run` — run the production server
19
+ - `pywire build` — compile the project for production
20
+ - `pywire deploy` — generate deployment configs (Docker, Render, Fly, Railway, Cloudflare)
21
+ - `pywire check` — run static analysis on the project (non-serializable wires, reactivity errors, redundant patterns)
22
+ - `pywire config` — read/write PyWire settings
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pywire-cli"
7
+ version = "0.2.0"
8
+ description = "Command-line tools for PyWire projects: dev, build, deploy, check"
9
+ authors = [
10
+ { name = "Reece Holmdahl", email = "reece@pywire.dev" },
11
+ ]
12
+ readme = "README.md"
13
+ requires-python = ">=3.11"
14
+ license = "MIT"
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Build Tools",
24
+ ]
25
+ dependencies = [
26
+ "pywire[build]",
27
+ "pywire-templates",
28
+ "jinja2>=3.1.0",
29
+ "uvicorn[standard]>=0.27.0",
30
+ "watchfiles>=0.21.0",
31
+ "textual>=7.4.0",
32
+ "rich-click>=1.9.6",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=7.4.0",
38
+ "pytest-asyncio>=0.21.0",
39
+ "pytest-cov>=4.1.0",
40
+ "ruff>=0.1.0",
41
+ "ty",
42
+ ]
43
+
44
+ [project.scripts]
45
+ pywire = "pywire_cli.main:cli"
46
+
47
+ [project.urls]
48
+ Homepage = "https://pywire.dev"
49
+ Documentation = "https://pywire.dev/docs"
50
+ Repository = "https://github.com/pywire/pywire"
51
+ Issues = "https://github.com/pywire/pywire/issues"
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["src/pywire_cli"]
55
+
56
+ [tool.ty.environment]
57
+ python-version = "3.11"
58
+
59
+ [tool.ty.analysis]
60
+ allowed-unresolved-imports = [
61
+ "pywire_parser",
62
+ "pywire_parser.**",
63
+ "pywire_templates",
64
+ "pywire_templates.**",
65
+ ]
66
+
67
+ [[tool.ty.overrides]]
68
+ include = ["tests/**"]
69
+ rules = { possibly-missing-attribute = "ignore", invalid-argument-type = "ignore", unresolved-attribute = "ignore", unresolved-import = "ignore" }
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ["tests"]
73
+ addopts = "--cov=pywire_cli"
@@ -0,0 +1,16 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ echo "🔍 Running checks for pywire-cli..."
5
+
6
+ echo "-> ruff format"
7
+ uv run ruff format --check src/ tests/
8
+
9
+ echo "-> ruff lint"
10
+ uv run ruff check src/ tests/
11
+
12
+ echo "-> ty"
13
+ uv run ty check src/
14
+
15
+ echo "-> pytest"
16
+ uv run --package pywire-cli pytest tests/
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ echo "🧪 Running tests for pywire-cli..."
5
+
6
+ uv run --package pywire-cli pytest tests/ "$@"
@@ -0,0 +1,3 @@
1
+ """PyWire CLI — dev, build, deploy, and check tools for PyWire projects."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,117 @@
1
+ """`pywire check` — static analysis for a PyWire project.
2
+
3
+ Thin CLI layer over :mod:`pywire_parser.analysis`. Supports rich output by
4
+ default and a ruff-style ``--plain`` format for CI / greppable logs. Exits
5
+ 1 on any ERROR-severity diagnostic.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Iterable, List, Optional, Sequence
13
+
14
+ from pywire_parser.analysis import Diagnostic, Severity, analyze_files
15
+ from pywire_parser.analysis.registry import all_rule_codes
16
+ from pywire_parser.parser import PyWireParser
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class CheckSummary:
21
+ errors: int
22
+ warnings: int
23
+ infos: int
24
+ exit_code: int
25
+
26
+ @property
27
+ def total(self) -> int:
28
+ return self.errors + self.warnings + self.infos
29
+
30
+
31
+ def collect_diagnostics(
32
+ pages_dir: Path,
33
+ *,
34
+ rule_codes: Optional[Sequence[str]] = None,
35
+ ) -> List[Diagnostic]:
36
+ """Parse every ``.wire`` under ``pages_dir`` and run the analysis engine."""
37
+ parser = PyWireParser()
38
+ items = []
39
+ if not pages_dir.exists():
40
+ return []
41
+ for wire_file in sorted(pages_dir.rglob("*.wire")):
42
+ try:
43
+ parsed = parser.parse_file(wire_file)
44
+ except Exception as e: # noqa: BLE001 — surface parse errors as diags
45
+ items.append(_parse_error_diag(wire_file, e))
46
+ continue
47
+ items.append((parsed, wire_file))
48
+ # Split: tuples go to engine; parse-error diagnostics are already Diagnostic
49
+ parsed_items = [x for x in items if isinstance(x, tuple)]
50
+ parse_error_diags = [x for x in items if not isinstance(x, tuple)]
51
+ engine_diags = analyze_files(parsed_items, codes=rule_codes)
52
+ return parse_error_diags + engine_diags
53
+
54
+
55
+ def _parse_error_diag(path: Path, err: Exception) -> Diagnostic:
56
+ from pywire_parser.analysis.diagnostics import Span
57
+
58
+ return Diagnostic(
59
+ code="PW000",
60
+ severity=Severity.ERROR,
61
+ message=f"parse error: {err}",
62
+ file_path=str(path),
63
+ span=Span(line=1, column=0),
64
+ )
65
+
66
+
67
+ def format_plain(diags: Iterable[Diagnostic]) -> str:
68
+ lines = []
69
+ for d in diags:
70
+ lines.append(
71
+ f"{d.file_path}:{d.line}:{d.column}: {d.severity.value} [{d.code}] {d.message}"
72
+ )
73
+ return "\n".join(lines)
74
+
75
+
76
+ def format_rich(diags: Iterable[Diagnostic], console) -> None:
77
+ """Render diagnostics with rich markup. ``console`` is a rich Console."""
78
+ current_file: Optional[str] = None
79
+ sev_style = {
80
+ Severity.ERROR: "bold red",
81
+ Severity.WARNING: "yellow",
82
+ Severity.INFO: "cyan",
83
+ }
84
+ for d in diags:
85
+ if d.file_path != current_file:
86
+ console.print(f"\n[bold]{d.file_path}[/]")
87
+ current_file = d.file_path
88
+ style = sev_style[d.severity]
89
+ console.print(
90
+ f" [dim]{d.line}:{d.column}[/] "
91
+ f"[{style}]{d.severity.value}[/] "
92
+ f"[bold]{d.code}[/] {d.message}"
93
+ )
94
+ if d.hint:
95
+ console.print(f" [dim]→ {d.hint}[/]")
96
+
97
+
98
+ def summarize(diags: Iterable[Diagnostic], *, strict: bool = False) -> CheckSummary:
99
+ errors = sum(1 for d in diags if d.severity == Severity.ERROR)
100
+ warnings = sum(1 for d in diags if d.severity == Severity.WARNING)
101
+ infos = sum(1 for d in diags if d.severity == Severity.INFO)
102
+ exit_code = 0
103
+ if errors > 0:
104
+ exit_code = 1
105
+ elif strict and (warnings > 0 or infos > 0):
106
+ exit_code = 1
107
+ return CheckSummary(errors, warnings, infos, exit_code)
108
+
109
+
110
+ __all__ = [
111
+ "CheckSummary",
112
+ "collect_diagnostics",
113
+ "format_plain",
114
+ "format_rich",
115
+ "summarize",
116
+ "all_rule_codes",
117
+ ]
@@ -0,0 +1,144 @@
1
+ """Persistent CLI settings stored in .pywire/settings.toml."""
2
+
3
+ import tomllib
4
+ from pathlib import Path
5
+ from typing import Any, Optional
6
+
7
+ try:
8
+ import rich_click as click
9
+ from rich.console import Console
10
+ except ImportError:
11
+ import sys
12
+
13
+ print(
14
+ "Error: pywire CLI requires additional dependencies.\n"
15
+ "Install them with: uv add pywire[cli] (or: pip install pywire[cli])",
16
+ file=sys.stderr,
17
+ )
18
+ sys.exit(1)
19
+
20
+ console = Console()
21
+
22
+ SETTINGS_DIR = Path(".pywire")
23
+ SETTINGS_FILE = SETTINGS_DIR / "settings.toml"
24
+
25
+ # Valid setting keys and their accepted values
26
+ VALID_SETTINGS: dict[str, dict[str, Any]] = {
27
+ "tui": {
28
+ "type": "bool",
29
+ "on_values": {"on", "true", "1", "yes"},
30
+ "off_values": {"off", "false", "0", "no"},
31
+ "description": "Enable TUI dashboard for dev server",
32
+ },
33
+ "port": {
34
+ "type": "int",
35
+ "description": "Default port for dev server (default: 3000)",
36
+ },
37
+ }
38
+
39
+
40
+ def _read_settings() -> dict[str, Any]:
41
+ """Read settings from .pywire/settings.toml, returning empty dict if missing."""
42
+ if not SETTINGS_FILE.exists():
43
+ return {}
44
+ with open(SETTINGS_FILE, "rb") as f:
45
+ data = tomllib.load(f)
46
+ return data.get("cli", {})
47
+
48
+
49
+ def _write_settings(settings: dict[str, Any]) -> None:
50
+ """Write settings to .pywire/settings.toml using simple TOML formatting."""
51
+ SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
52
+ lines = ["[cli]"]
53
+ for key, value in sorted(settings.items()):
54
+ if isinstance(value, bool):
55
+ lines.append(f"{key} = {'true' if value else 'false'}")
56
+ elif isinstance(value, str):
57
+ lines.append(f'{key} = "{value}"')
58
+ else:
59
+ lines.append(f"{key} = {value}")
60
+ SETTINGS_FILE.write_text("\n".join(lines) + "\n")
61
+
62
+
63
+ def get_setting(key: str) -> Optional[Any]:
64
+ """Get a single setting value, or None if not set."""
65
+ return _read_settings().get(key)
66
+
67
+
68
+ def _parse_bool_value(key: str, value: str) -> bool:
69
+ """Parse a string value into a boolean for the given setting key."""
70
+ meta = VALID_SETTINGS[key]
71
+ lower = value.lower()
72
+ if lower in meta["on_values"]:
73
+ return True
74
+ if lower in meta["off_values"]:
75
+ return False
76
+ on = ", ".join(sorted(meta["on_values"]))
77
+ off = ", ".join(sorted(meta["off_values"]))
78
+ raise click.BadParameter(
79
+ f"Invalid value '{value}' for '{key}'. Use one of: {on} (enable) or {off} (disable)."
80
+ )
81
+
82
+
83
+ @click.command(name="config")
84
+ @click.argument("key", required=False)
85
+ @click.argument("value", required=False)
86
+ def config_command(key: Optional[str], value: Optional[str]) -> None:
87
+ """View or update persistent CLI settings (.pywire/settings.toml).
88
+
89
+ \b
90
+ Examples:
91
+ pywire config Show all settings
92
+ pywire config tui Show TUI setting
93
+ pywire config tui on Enable TUI by default
94
+ pywire config tui off Disable TUI by default
95
+ """
96
+ if key is None:
97
+ # Show all settings
98
+ settings = _read_settings()
99
+ if not settings:
100
+ console.print("[dim]No settings configured yet.[/]")
101
+ console.print(
102
+ "[dim]Available settings:[/] "
103
+ + ", ".join(f"[cyan]{k}[/]" for k in sorted(VALID_SETTINGS))
104
+ )
105
+ return
106
+ for k, v in sorted(settings.items()):
107
+ console.print(f"[cyan]{k}[/] = [bold]{v}[/]")
108
+ return
109
+
110
+ if key not in VALID_SETTINGS:
111
+ valid = ", ".join(sorted(VALID_SETTINGS))
112
+ raise click.BadParameter(
113
+ f"Unknown setting '{key}'. Valid settings: {valid}",
114
+ param_hint="KEY",
115
+ )
116
+
117
+ if value is None:
118
+ # Show single setting
119
+ current = get_setting(key)
120
+ if current is None:
121
+ console.print(f"[cyan]{key}[/] = [dim]<not set>[/]")
122
+ else:
123
+ console.print(f"[cyan]{key}[/] = [bold]{current}[/]")
124
+ return
125
+
126
+ # Set value
127
+ meta = VALID_SETTINGS[key]
128
+ if meta["type"] == "bool":
129
+ parsed = _parse_bool_value(key, value)
130
+ elif meta["type"] == "int":
131
+ try:
132
+ parsed = int(value)
133
+ except ValueError:
134
+ raise click.BadParameter(
135
+ f"Invalid value '{value}' for '{key}'. Must be an integer.",
136
+ param_hint="VALUE",
137
+ )
138
+ else:
139
+ parsed = value
140
+
141
+ settings = _read_settings()
142
+ settings[key] = parsed
143
+ _write_settings(settings)
144
+ console.print(f"[cyan]{key}[/] = [bold]{parsed}[/]")
@@ -0,0 +1,90 @@
1
+ """Deployment configuration generators for PyWire apps.
2
+
3
+ Templates live in the ``pywire-templates`` package so ``create-pywire-app``
4
+ and ``pywire deploy`` stay in sync. This module is a thin rendering layer
5
+ that feeds the right context variables to each template.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from pywire_templates import render_deploy_template
13
+
14
+
15
+ def _parse_app_string(app_string: str) -> tuple[str, str]:
16
+ """Parse ``'src.main:app'`` into ``('src.main', 'app')``."""
17
+ if ":" in app_string:
18
+ app_module, app_attr = app_string.rsplit(":", 1)
19
+ else:
20
+ app_module, app_attr = app_string, "app"
21
+ return app_module, app_attr
22
+
23
+
24
+ def generate_dockerfile(project_root: Path, workers: int = 1) -> str:
25
+ """Generate Dockerfile content for a PyWire project."""
26
+ return render_deploy_template("Dockerfile.j2", workers=workers)
27
+
28
+
29
+ def generate_render_yaml(
30
+ project_root: Path, project_name: str, redis: bool = False
31
+ ) -> str:
32
+ """Generate render.yaml content for a PyWire project."""
33
+ return render_deploy_template(
34
+ "render.yaml.j2",
35
+ project_name=project_name,
36
+ redis_enabled=redis,
37
+ )
38
+
39
+
40
+ def generate_fly_toml(project_root: Path, project_name: str) -> str:
41
+ """Generate fly.toml content for a PyWire project."""
42
+ return render_deploy_template("fly.toml.j2", project_name=project_name)
43
+
44
+
45
+ def generate_railway_json(project_root: Path) -> str:
46
+ """Generate railway.json content for a PyWire project."""
47
+ return render_deploy_template("railway.json.j2")
48
+
49
+
50
+ def generate_wrangler_toml(project_root: Path, project_name: str) -> str:
51
+ """Generate wrangler.toml content for Cloudflare Workers with Durable Objects."""
52
+ return render_deploy_template("wrangler.toml.j2", project_name=project_name)
53
+
54
+
55
+ def generate_cf_entry(project_root: Path, app_string: str = "main:app") -> str:
56
+ """Generate entry.py for Cloudflare Workers with Durable Object routing."""
57
+ app_module, app_attr = _parse_app_string(app_string)
58
+ return render_deploy_template(
59
+ "entry.py.j2", app_module=app_module, app_attr=app_attr
60
+ )
61
+
62
+
63
+ def generate_cf_durable_object(project_root: Path, app_string: str = "main:app") -> str:
64
+ """Generate pywire_do.py — the Durable Object class for PyWire sessions."""
65
+ app_module, app_attr = _parse_app_string(app_string)
66
+ return render_deploy_template(
67
+ "pywire_do.py.j2", app_module=app_module, app_attr=app_attr
68
+ )
69
+
70
+
71
+ def validate_deploy_config(platform: str, project_root: Path) -> list[str]:
72
+ """Check what's missing for deployment on the given platform.
73
+
74
+ Returns a list of warning/error messages. An empty list means
75
+ everything looks good.
76
+ """
77
+ issues: list[str] = []
78
+
79
+ if not (project_root / "pyproject.toml").exists():
80
+ issues.append("Missing pyproject.toml — required for dependency installation.")
81
+
82
+ if (
83
+ platform in ("docker", "fly", "render", "railway")
84
+ and not (project_root / "uv.lock").exists()
85
+ ):
86
+ issues.append(
87
+ "Missing uv.lock — run 'uv lock' to generate a lock file for reproducible builds."
88
+ )
89
+
90
+ return issues