caspian-native 0.0.1__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.
Files changed (28) hide show
  1. caspian_native-0.0.1/LICENSE +22 -0
  2. caspian_native-0.0.1/PKG-INFO +158 -0
  3. caspian_native-0.0.1/README.md +134 -0
  4. caspian_native-0.0.1/pyproject.toml +49 -0
  5. caspian_native-0.0.1/setup.cfg +4 -0
  6. caspian_native-0.0.1/src/caspian_native/__init__.py +8 -0
  7. caspian_native-0.0.1/src/caspian_native/__main__.py +5 -0
  8. caspian_native-0.0.1/src/caspian_native/backend.py +118 -0
  9. caspian_native-0.0.1/src/caspian_native/cli.py +119 -0
  10. caspian_native-0.0.1/src/caspian_native/config.py +165 -0
  11. caspian_native-0.0.1/src/caspian_native/doctor.py +57 -0
  12. caspian_native-0.0.1/src/caspian_native/errors.py +3 -0
  13. caspian_native-0.0.1/src/caspian_native/project.py +48 -0
  14. caspian_native-0.0.1/src/caspian_native/runner.py +168 -0
  15. caspian_native-0.0.1/src/caspian_native/scaffold.py +471 -0
  16. caspian_native-0.0.1/src/caspian_native/schema.py +54 -0
  17. caspian_native-0.0.1/src/caspian_native/security.py +81 -0
  18. caspian_native-0.0.1/src/caspian_native.egg-info/PKG-INFO +158 -0
  19. caspian_native-0.0.1/src/caspian_native.egg-info/SOURCES.txt +26 -0
  20. caspian_native-0.0.1/src/caspian_native.egg-info/dependency_links.txt +1 -0
  21. caspian_native-0.0.1/src/caspian_native.egg-info/entry_points.txt +2 -0
  22. caspian_native-0.0.1/src/caspian_native.egg-info/requires.txt +8 -0
  23. caspian_native-0.0.1/src/caspian_native.egg-info/top_level.txt +1 -0
  24. caspian_native-0.0.1/tests/test_config.py +41 -0
  25. caspian_native-0.0.1/tests/test_project.py +30 -0
  26. caspian_native-0.0.1/tests/test_runner.py +50 -0
  27. caspian_native-0.0.1/tests/test_scaffold.py +82 -0
  28. caspian_native-0.0.1/tests/test_security.py +46 -0
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Caspian Native contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: caspian-native
3
+ Version: 0.0.1
4
+ Summary: Optional Tauri packaging for Caspian applications
5
+ Author: Caspian Native contributors
6
+ License-Expression: MIT
7
+ Keywords: caspian,pulsepoint,tauri,desktop,webview
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Environment :: Win32 (MS Windows)
10
+ Classifier: Framework :: FastAPI
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Topic :: Software Development :: Build Tools
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: build
18
+ Requires-Dist: pyinstaller>=6; extra == "build"
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=8; extra == "test"
21
+ Requires-Dist: ruff>=0.8; extra == "test"
22
+ Requires-Dist: pyright>=1.1; extra == "test"
23
+ Dynamic: license-file
24
+
25
+ # caspian-native
26
+
27
+ Optional Tauri packaging for applications built with the Caspian framework and
28
+ PulsePoint. Version `0.0.1` packages the existing application in a system
29
+ WebView; it does not translate HTML into operating-system widgets.
30
+
31
+ ## Support in 0.0.1
32
+
33
+ | Target | Backend | Status |
34
+ | --- | --- | --- |
35
+ | Windows | Embedded Python sidecar | Initial support |
36
+ | Windows | Remote Caspian server | Initial support |
37
+ | Android | Remote Caspian server | Initial support |
38
+ | Android | Embedded Python | Not supported yet |
39
+
40
+ Android embedded Python is deliberately refused. A Caspian application may
41
+ depend on native Python wheels such as `asyncpg`, `cryptography`, or Pillow;
42
+ shipping an APK without proving every wheel for every ABI would produce a
43
+ package that builds unreliably or fails on launch.
44
+
45
+ ## Install for development
46
+
47
+ ```text
48
+ python -m pip install -e ".[test,build]"
49
+ ```
50
+
51
+ Tauri's CLI is installed separately so it can match the version selected by
52
+ the generated application's `native/Cargo.toml`:
53
+
54
+ ```text
55
+ cargo install tauri-cli --version "^2" --locked
56
+ ```
57
+
58
+ ## Add native packaging to a Caspian project
59
+
60
+ From a directory containing `caspian.config.json`:
61
+
62
+ ```text
63
+ caspian-native init --identifier com.example.myapp --windows
64
+ caspian-native doctor --target windows
65
+ caspian-native backend build
66
+ caspian-native dev --target windows
67
+ caspian-native build --target windows
68
+ ```
69
+
70
+ For an Android thin client backed by a deployed Caspian server:
71
+
72
+ ```text
73
+ caspian-native init \
74
+ --identifier com.example.myapp \
75
+ --windows --android \
76
+ --remote-url https://app.example.com
77
+ caspian-native doctor --target android
78
+ caspian-native build --target android
79
+ ```
80
+
81
+ The generated files live under `native/`. Re-running `init` never overwrites a
82
+ changed generated file unless `--force` is supplied.
83
+
84
+ ## Configuration
85
+
86
+ `caspian.native.json` is the source of truth:
87
+
88
+ ```json
89
+ {
90
+ "$schema": "./caspian.native.schema.json",
91
+ "schema": 1,
92
+ "productName": "My App",
93
+ "identifier": "com.example.myapp",
94
+ "version": "0.0.1",
95
+ "targets": ["windows"],
96
+ "window": {
97
+ "title": "My App",
98
+ "width": 1200,
99
+ "height": 800,
100
+ "resizable": true
101
+ },
102
+ "backend": {
103
+ "desktopMode": "embedded",
104
+ "remoteUrl": null
105
+ },
106
+ "security": {
107
+ "loopbackToken": true
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Runtime model
113
+
114
+ Embedded desktop mode builds `main.py`, the Caspian runtime, project modules,
115
+ route index, and `public/` into a one-file sidecar. The sidecar:
116
+
117
+ 1. restores a per-installation `AUTH_SECRET` from application data;
118
+ 2. imports the project's `main.app` in production mode;
119
+ 3. binds `127.0.0.1` on an operating-system-assigned port;
120
+ 4. prints the protected launch URL to the Tauri host;
121
+ 5. serves the normal Caspian ASGI application with Uvicorn.
122
+
123
+ The WebView receives a per-launch token once. The native middleware exchanges
124
+ it for an `HttpOnly; SameSite=Strict` cookie and refuses documents, assets,
125
+ RPCs, uploads, streams, and WebSocket handshakes without that cookie.
126
+
127
+ Caspian's production session cookie normally has the `Secure` flag. A packaged
128
+ loopback server intentionally uses `http://127.0.0.1`, so the adapter changes
129
+ only that middleware option before Starlette constructs the stack. The app
130
+ otherwise remains in production mode.
131
+
132
+ ## Native bridge
133
+
134
+ Pages may capability-check `window.caspianNative`:
135
+
136
+ ```javascript
137
+ const native = window.caspianNative;
138
+ if (native?.has("open-external")) {
139
+ await native.invoke("open_external", { url: "https://example.com" });
140
+ }
141
+ ```
142
+
143
+ The bridge allowlist is intentionally short: platform, application version,
144
+ application data directory, external HTTP/HTTPS/mail links, and a user-driven
145
+ file picker. An XSS can reach every bridge command, so commands must remain
146
+ safe even when the calling page is compromised.
147
+
148
+ ## Current limitations
149
+
150
+ - The first embedded-backend implementation uses PyInstaller and has only been
151
+ designed for Windows packaging.
152
+ - Dynamic route discovery requires project Python sources and
153
+ `settings/files-list.json` to be bundled.
154
+ - Secrets from `.env` are never embedded. Remote database credentials and
155
+ third-party credentials must be provisioned outside the package.
156
+ - Android remote mode requires connectivity and a deployed Caspian server.
157
+ - Installer signing and Android store signing are not automated in `0.0.1`.
158
+
@@ -0,0 +1,134 @@
1
+ # caspian-native
2
+
3
+ Optional Tauri packaging for applications built with the Caspian framework and
4
+ PulsePoint. Version `0.0.1` packages the existing application in a system
5
+ WebView; it does not translate HTML into operating-system widgets.
6
+
7
+ ## Support in 0.0.1
8
+
9
+ | Target | Backend | Status |
10
+ | --- | --- | --- |
11
+ | Windows | Embedded Python sidecar | Initial support |
12
+ | Windows | Remote Caspian server | Initial support |
13
+ | Android | Remote Caspian server | Initial support |
14
+ | Android | Embedded Python | Not supported yet |
15
+
16
+ Android embedded Python is deliberately refused. A Caspian application may
17
+ depend on native Python wheels such as `asyncpg`, `cryptography`, or Pillow;
18
+ shipping an APK without proving every wheel for every ABI would produce a
19
+ package that builds unreliably or fails on launch.
20
+
21
+ ## Install for development
22
+
23
+ ```text
24
+ python -m pip install -e ".[test,build]"
25
+ ```
26
+
27
+ Tauri's CLI is installed separately so it can match the version selected by
28
+ the generated application's `native/Cargo.toml`:
29
+
30
+ ```text
31
+ cargo install tauri-cli --version "^2" --locked
32
+ ```
33
+
34
+ ## Add native packaging to a Caspian project
35
+
36
+ From a directory containing `caspian.config.json`:
37
+
38
+ ```text
39
+ caspian-native init --identifier com.example.myapp --windows
40
+ caspian-native doctor --target windows
41
+ caspian-native backend build
42
+ caspian-native dev --target windows
43
+ caspian-native build --target windows
44
+ ```
45
+
46
+ For an Android thin client backed by a deployed Caspian server:
47
+
48
+ ```text
49
+ caspian-native init \
50
+ --identifier com.example.myapp \
51
+ --windows --android \
52
+ --remote-url https://app.example.com
53
+ caspian-native doctor --target android
54
+ caspian-native build --target android
55
+ ```
56
+
57
+ The generated files live under `native/`. Re-running `init` never overwrites a
58
+ changed generated file unless `--force` is supplied.
59
+
60
+ ## Configuration
61
+
62
+ `caspian.native.json` is the source of truth:
63
+
64
+ ```json
65
+ {
66
+ "$schema": "./caspian.native.schema.json",
67
+ "schema": 1,
68
+ "productName": "My App",
69
+ "identifier": "com.example.myapp",
70
+ "version": "0.0.1",
71
+ "targets": ["windows"],
72
+ "window": {
73
+ "title": "My App",
74
+ "width": 1200,
75
+ "height": 800,
76
+ "resizable": true
77
+ },
78
+ "backend": {
79
+ "desktopMode": "embedded",
80
+ "remoteUrl": null
81
+ },
82
+ "security": {
83
+ "loopbackToken": true
84
+ }
85
+ }
86
+ ```
87
+
88
+ ## Runtime model
89
+
90
+ Embedded desktop mode builds `main.py`, the Caspian runtime, project modules,
91
+ route index, and `public/` into a one-file sidecar. The sidecar:
92
+
93
+ 1. restores a per-installation `AUTH_SECRET` from application data;
94
+ 2. imports the project's `main.app` in production mode;
95
+ 3. binds `127.0.0.1` on an operating-system-assigned port;
96
+ 4. prints the protected launch URL to the Tauri host;
97
+ 5. serves the normal Caspian ASGI application with Uvicorn.
98
+
99
+ The WebView receives a per-launch token once. The native middleware exchanges
100
+ it for an `HttpOnly; SameSite=Strict` cookie and refuses documents, assets,
101
+ RPCs, uploads, streams, and WebSocket handshakes without that cookie.
102
+
103
+ Caspian's production session cookie normally has the `Secure` flag. A packaged
104
+ loopback server intentionally uses `http://127.0.0.1`, so the adapter changes
105
+ only that middleware option before Starlette constructs the stack. The app
106
+ otherwise remains in production mode.
107
+
108
+ ## Native bridge
109
+
110
+ Pages may capability-check `window.caspianNative`:
111
+
112
+ ```javascript
113
+ const native = window.caspianNative;
114
+ if (native?.has("open-external")) {
115
+ await native.invoke("open_external", { url: "https://example.com" });
116
+ }
117
+ ```
118
+
119
+ The bridge allowlist is intentionally short: platform, application version,
120
+ application data directory, external HTTP/HTTPS/mail links, and a user-driven
121
+ file picker. An XSS can reach every bridge command, so commands must remain
122
+ safe even when the calling page is compromised.
123
+
124
+ ## Current limitations
125
+
126
+ - The first embedded-backend implementation uses PyInstaller and has only been
127
+ designed for Windows packaging.
128
+ - Dynamic route discovery requires project Python sources and
129
+ `settings/files-list.json` to be bundled.
130
+ - Secrets from `.env` are never embedded. Remote database credentials and
131
+ third-party credentials must be provisioned outside the package.
132
+ - Android remote mode requires connectivity and a deployed Caspian server.
133
+ - Installer signing and Android store signing are not automated in `0.0.1`.
134
+
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "caspian-native"
7
+ version = "0.0.1"
8
+ description = "Optional Tauri packaging for Caspian applications"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "Caspian Native contributors" }]
13
+ keywords = ["caspian", "pulsepoint", "tauri", "desktop", "webview"]
14
+ classifiers = [
15
+ "Development Status :: 2 - Pre-Alpha",
16
+ "Environment :: Win32 (MS Windows)",
17
+ "Framework :: FastAPI",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Software Development :: Build Tools",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.optional-dependencies]
25
+ build = ["pyinstaller>=6"]
26
+ test = ["pytest>=8", "ruff>=0.8", "pyright>=1.1"]
27
+
28
+ [project.scripts]
29
+ caspian-native = "caspian_native.cli:main"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
36
+ addopts = "-q"
37
+
38
+ [tool.ruff]
39
+ line-length = 100
40
+ target-version = "py311"
41
+
42
+ [tool.ruff.lint]
43
+ select = ["E", "F", "I", "UP", "B"]
44
+ ignore = ["E501"]
45
+
46
+ [tool.pyright]
47
+ include = ["src", "tests"]
48
+ typeCheckingMode = "basic"
49
+ pythonVersion = "3.11"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """Native WebView packaging for Caspian applications."""
2
+
3
+ from .config import NativeConfig
4
+
5
+ __all__ = ["NativeConfig", "__version__"]
6
+
7
+ __version__ = "0.0.1"
8
+
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
5
+
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from .errors import NativeError
10
+ from .project import CaspianProject
11
+
12
+
13
+ def rust_host_triple() -> str:
14
+ result = subprocess.run(
15
+ ["rustc", "-vV"], capture_output=True, text=True, check=False, encoding="utf-8"
16
+ )
17
+ if result.returncode:
18
+ raise NativeError("rustc is required to name the Tauri sidecar target")
19
+ for line in result.stdout.splitlines():
20
+ if line.startswith("host: "):
21
+ return line.removeprefix("host: ").strip()
22
+ raise NativeError("rustc did not report a host target triple")
23
+
24
+
25
+ def build_backend(project: CaspianProject) -> Path:
26
+ uv = shutil.which("uv")
27
+ if uv is None and shutil.which("pyinstaller") is None:
28
+ raise NativeError(
29
+ "embedded backend builds require uv (the normal Caspian environment manager) "
30
+ "or PyInstaller in the current Python environment"
31
+ )
32
+ public = project.root / "public"
33
+ files_list = project.root / "settings" / "files-list.json"
34
+ if not public.is_dir():
35
+ raise NativeError("the Caspian project has no public directory; run its build first")
36
+ if not files_list.is_file():
37
+ raise NativeError("settings/files-list.json is missing; run the Caspian project build first")
38
+
39
+ work = project.native_dir / ".backend-build"
40
+ dist = work / "dist"
41
+ spec = work / "spec"
42
+ binaries = project.native_dir / "binaries"
43
+ work.mkdir(parents=True, exist_ok=True)
44
+ binaries.mkdir(parents=True, exist_ok=True)
45
+ launcher = work / "launcher.py"
46
+ launcher.write_text(
47
+ "from caspian_native.runner import main\n\nif __name__ == '__main__':\n main()\n",
48
+ encoding="utf-8",
49
+ )
50
+
51
+ add_data = [
52
+ (project.root / "main.py", "app"),
53
+ (project.root / "src", "app/src"),
54
+ (public, "app/public"),
55
+ (project.root / "caspian.config.json", "app"),
56
+ (files_list, "app/settings"),
57
+ ]
58
+ component_map = project.root / "settings" / "component-map.json"
59
+ if component_map.is_file():
60
+ add_data.append((component_map, "app/settings"))
61
+
62
+ separator = ";" if os.name == "nt" else ":"
63
+ if uv:
64
+ command = [
65
+ uv,
66
+ "run",
67
+ "--project",
68
+ str(project.root),
69
+ "--with",
70
+ "pyinstaller>=6",
71
+ "python",
72
+ "-m",
73
+ "PyInstaller",
74
+ ]
75
+ else:
76
+ command = [sys.executable, "-m", "PyInstaller"]
77
+ command.extend(
78
+ [
79
+ "--noconfirm",
80
+ "--clean",
81
+ "--onefile",
82
+ "--name",
83
+ "caspian-server",
84
+ "--distpath",
85
+ str(dist),
86
+ "--workpath",
87
+ str(work / "work"),
88
+ "--specpath",
89
+ str(spec),
90
+ "--paths",
91
+ str(project.root),
92
+ "--paths",
93
+ str(Path(__file__).resolve().parents[1]),
94
+ "--hidden-import",
95
+ "main",
96
+ "--collect-submodules",
97
+ "src",
98
+ "--collect-submodules",
99
+ "caspian_native",
100
+ "--collect-all",
101
+ "casp",
102
+ ]
103
+ )
104
+ for source, destination in add_data:
105
+ command.extend(["--add-data", f"{source}{separator}{destination}"])
106
+ command.append(str(launcher))
107
+
108
+ result = subprocess.run(command, cwd=project.root, check=False)
109
+ if result.returncode:
110
+ raise NativeError("PyInstaller failed to build the Caspian backend")
111
+
112
+ suffix = ".exe" if os.name == "nt" else ""
113
+ built = dist / f"caspian-server{suffix}"
114
+ if not built.is_file():
115
+ raise NativeError(f"PyInstaller completed but did not produce {built}")
116
+ target = binaries / f"caspian-server-{rust_host_triple()}{suffix}"
117
+ shutil.copy2(built, target)
118
+ return target
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import __version__
9
+ from .backend import build_backend
10
+ from .config import BackendConfig, NativeConfig, SecurityConfig, WindowConfig
11
+ from .doctor import inspect
12
+ from .errors import NativeError
13
+ from .project import CaspianProject
14
+ from .scaffold import write_generated
15
+
16
+
17
+ def parser() -> argparse.ArgumentParser:
18
+ root = argparse.ArgumentParser(prog="caspian-native")
19
+ root.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
20
+ root.add_argument("--project", type=Path, help="Caspian project root")
21
+ commands = root.add_subparsers(dest="command", required=True)
22
+
23
+ init = commands.add_parser("init", help="create or refresh the native Tauri shell")
24
+ init.add_argument("--identifier", required=True)
25
+ init.add_argument("--product-name")
26
+ init.add_argument("--app-version", default="0.0.1")
27
+ init.add_argument("--windows", action="store_true")
28
+ init.add_argument("--android", action="store_true")
29
+ init.add_argument("--remote-url")
30
+ init.add_argument("--desktop-mode", choices=("embedded", "remote"), default="embedded")
31
+ init.add_argument("--force", action="store_true")
32
+
33
+ doctor = commands.add_parser("doctor", help="check native packaging prerequisites")
34
+ doctor.add_argument("--target", choices=("windows", "android"), default="windows")
35
+
36
+ backend = commands.add_parser("backend", help="build the embedded Caspian sidecar")
37
+ backend.add_argument("action", choices=("build",))
38
+
39
+ dev = commands.add_parser("dev", help="run the generated Tauri application")
40
+ dev.add_argument("--target", choices=("windows", "android"), default="windows")
41
+
42
+ build = commands.add_parser("build", help="build a native installer or Android package")
43
+ build.add_argument("--target", choices=("windows", "android"), default="windows")
44
+ build.add_argument("--debug", action="store_true")
45
+ return root
46
+
47
+
48
+ def main(argv: list[str] | None = None) -> None:
49
+ args = parser().parse_args(argv)
50
+ try:
51
+ project = (
52
+ CaspianProject.from_root(args.project)
53
+ if args.project
54
+ else CaspianProject.discover()
55
+ )
56
+ if args.command == "init":
57
+ _init(project, args)
58
+ elif args.command == "doctor":
59
+ _doctor(project, args.target)
60
+ elif args.command == "backend":
61
+ print(build_backend(project))
62
+ elif args.command in {"dev", "build"}:
63
+ _tauri(project, args.command, args.target, getattr(args, "debug", False))
64
+ except NativeError as exc:
65
+ print(f"error: {exc}", file=sys.stderr)
66
+ raise SystemExit(2) from exc
67
+
68
+
69
+ def _init(project: CaspianProject, args: argparse.Namespace) -> None:
70
+ targets = [target for target, enabled in (("windows", args.windows), ("android", args.android)) if enabled]
71
+ if not targets:
72
+ targets = ["windows"]
73
+ product = args.product_name or project.name.replace("-", " ").title()
74
+ config = NativeConfig(
75
+ product_name=product,
76
+ identifier=args.identifier,
77
+ version=args.app_version,
78
+ targets=targets,
79
+ window=WindowConfig(title=product),
80
+ backend=BackendConfig(desktop_mode=args.desktop_mode, remote_url=args.remote_url),
81
+ security=SecurityConfig(loopback_token=True),
82
+ )
83
+ config.validate()
84
+ for path, outcome in write_generated(project, config, force=args.force):
85
+ print(f"{outcome:9} {path}")
86
+
87
+
88
+ def _doctor(project: CaspianProject, target: str) -> None:
89
+ config = NativeConfig.load(project.native_config)
90
+ checks = inspect(project, config, target)
91
+ for check in checks:
92
+ print(f"{'OK' if check.ok else 'MISSING':7} {check.name}: {check.detail}")
93
+ if not all(check.ok for check in checks):
94
+ raise NativeError(f"{target} prerequisites are incomplete")
95
+
96
+
97
+ def _tauri(project: CaspianProject, action: str, target: str, debug: bool) -> None:
98
+ config = NativeConfig.load(project.native_config)
99
+ if target not in config.targets:
100
+ raise NativeError(f"target {target} is not enabled in caspian.native.json")
101
+ if target == "android" and not config.backend.remote_url:
102
+ raise NativeError("Android requires backend.remoteUrl in caspian-native 0.0.1")
103
+ if target == "windows" and config.backend.desktop_mode == "embedded":
104
+ build_backend(project)
105
+
106
+ command = ["cargo", "tauri"]
107
+ if target == "android":
108
+ command.extend(["android", action])
109
+ else:
110
+ command.append(action)
111
+ if debug and action == "build":
112
+ command.append("--debug")
113
+ result = subprocess.run(command, cwd=project.native_dir, check=False)
114
+ if result.returncode:
115
+ raise NativeError(f"Tauri {action} failed for {target}")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()