aurasprout 0.1.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 (53) hide show
  1. aurasprout-0.1.1/LICENSE +21 -0
  2. aurasprout-0.1.1/PKG-INFO +74 -0
  3. aurasprout-0.1.1/README.md +61 -0
  4. aurasprout-0.1.1/pyproject.toml +31 -0
  5. aurasprout-0.1.1/setup.cfg +4 -0
  6. aurasprout-0.1.1/src/aurasprout/__init__.py +4 -0
  7. aurasprout-0.1.1/src/aurasprout/__main__.py +8 -0
  8. aurasprout-0.1.1/src/aurasprout/catalog/__init__.py +2 -0
  9. aurasprout-0.1.1/src/aurasprout/catalog/crypto.py +101 -0
  10. aurasprout-0.1.1/src/aurasprout/catalog/html_generator.py +202 -0
  11. aurasprout-0.1.1/src/aurasprout/catalog/manager.py +422 -0
  12. aurasprout-0.1.1/src/aurasprout/catalog/models.py +640 -0
  13. aurasprout-0.1.1/src/aurasprout/catalog/packager.py +124 -0
  14. aurasprout-0.1.1/src/aurasprout/catalog/sources.py +122 -0
  15. aurasprout-0.1.1/src/aurasprout/cli.py +219 -0
  16. aurasprout-0.1.1/src/aurasprout/cli_catalog.py +305 -0
  17. aurasprout-0.1.1/src/aurasprout/cli_volunteer.py +122 -0
  18. aurasprout-0.1.1/src/aurasprout/contracts/__init__.py +2 -0
  19. aurasprout-0.1.1/src/aurasprout/contracts/paths.py +225 -0
  20. aurasprout-0.1.1/src/aurasprout/coordination/__init__.py +10 -0
  21. aurasprout-0.1.1/src/aurasprout/coordination/batches.py +240 -0
  22. aurasprout-0.1.1/src/aurasprout/coordination/leases.py +577 -0
  23. aurasprout-0.1.1/src/aurasprout/installer/__init__.py +2 -0
  24. aurasprout-0.1.1/src/aurasprout/installer/autostart.py +473 -0
  25. aurasprout-0.1.1/src/aurasprout/installer/engine.py +924 -0
  26. aurasprout-0.1.1/src/aurasprout/installer/registry.py +203 -0
  27. aurasprout-0.1.1/src/aurasprout/installer/self_install.py +739 -0
  28. aurasprout-0.1.1/src/aurasprout/ipc/__init__.py +9 -0
  29. aurasprout-0.1.1/src/aurasprout/ipc/server.py +705 -0
  30. aurasprout-0.1.1/src/aurasprout/storage/__init__.py +23 -0
  31. aurasprout-0.1.1/src/aurasprout/storage/cell_state.py +424 -0
  32. aurasprout-0.1.1/src/aurasprout/storage/events.py +278 -0
  33. aurasprout-0.1.1/src/aurasprout/storage/mutex.py +244 -0
  34. aurasprout-0.1.1/src/aurasprout/supervisor/__init__.py +9 -0
  35. aurasprout-0.1.1/src/aurasprout/supervisor/runner.py +335 -0
  36. aurasprout-0.1.1/src/aurasprout/ui/__init__.py +11 -0
  37. aurasprout-0.1.1/src/aurasprout/ui/desktop.py +874 -0
  38. aurasprout-0.1.1/src/aurasprout/ui/server.py +788 -0
  39. aurasprout-0.1.1/src/aurasprout/ui/static/app.js +326 -0
  40. aurasprout-0.1.1/src/aurasprout/ui/static/aurasprout.ico +0 -0
  41. aurasprout-0.1.1/src/aurasprout/ui/static/aurasprout.png +0 -0
  42. aurasprout-0.1.1/src/aurasprout/ui/static/index.html +135 -0
  43. aurasprout-0.1.1/src/aurasprout.egg-info/PKG-INFO +74 -0
  44. aurasprout-0.1.1/src/aurasprout.egg-info/SOURCES.txt +51 -0
  45. aurasprout-0.1.1/src/aurasprout.egg-info/dependency_links.txt +1 -0
  46. aurasprout-0.1.1/src/aurasprout.egg-info/entry_points.txt +2 -0
  47. aurasprout-0.1.1/src/aurasprout.egg-info/requires.txt +4 -0
  48. aurasprout-0.1.1/src/aurasprout.egg-info/top_level.txt +1 -0
  49. aurasprout-0.1.1/tests/test_catalog_and_installer.py +337 -0
  50. aurasprout-0.1.1/tests/test_coordination_and_deconfliction.py +168 -0
  51. aurasprout-0.1.1/tests/test_http_catalog_and_self_install.py +333 -0
  52. aurasprout-0.1.1/tests/test_ipc_bridge_and_events.py +157 -0
  53. aurasprout-0.1.1/tests/test_ui_openapi_sync.py +152 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AuraCoreDynamics
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.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: aurasprout
3
+ Version: 0.1.1
4
+ Summary: User-space zero-privilege AuraGrid cell coordination, application catalog, installer, and supervision layer.
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: auraseed>=1.1.0
9
+ Requires-Dist: auragrid>=4.3.0
10
+ Requires-Dist: pydantic>=2.0.0
11
+ Requires-Dist: httpx>=0.25.0
12
+ Dynamic: license-file
13
+
14
+ # AuraSprout
15
+
16
+ Zero-privilege, user-space application distribution, local supervision, and shared-filesystem coordination layer for **AuraCore**.
17
+
18
+ ## Architecture Boundaries
19
+
20
+ - **`auragrid-api`**: Canonical OpenAPI (`ipc-bridge.openapi.json`, `aurasprout-management.openapi.json`) and JSON Schemas (`app.manifest.schema.json`, `catalog.schema.json`, `lease-collaboration.schema.json`).
21
+ - **`auragrid-py`**: Typed Python SDK consumed by `LocalOnly`, `CellSingleton`, and `Distributed` MAS workloads.
22
+ - **`auraseed`**: Strictly Python discovery & sovereign virtual environment (`mvenv`) management (`aurasprout` composes `auraseed.gui.MvenvManagerPanel`).
23
+ - **`aurasprout`**: Application cataloging & packaging (`.apkg`), HTTP catalog hosting/push, user-space installation/updates/rollback (`active_version.json` + preserved `app_data/`), cleanroom standalone self-install/cleanup, SMB-safe SQLite event bus (`PRAGMA journal_mode = TRUNCATE`), atomic directory mutex & fenced leases/batches (`SproutMutex`), Tier 2 loopback IPC bridge (`X-AuraGrid-IPC-Token`), Tier 3 loopback Management Web UI (`aurasprout_session` + `X-CSRF-Token`), and Tkinter Desktop Hub.
24
+
25
+ ## Quick Start
26
+
27
+ Both `aurasprout` (installed console script / standalone launcher) and `python -m aurasprout` expose the full CLI (supporting global `--cell-root` and `--user-root` flags):
28
+
29
+ ```powershell
30
+ # Install AuraSprout standalone into %LOCALAPPDATA%\AuraSprout\standalone
31
+ python -m aurasprout self-install --add-to-path --shortcut --json
32
+
33
+ # Package & publish an app to a static zero-CDN catalog (supports --wheelhouse, --format zip|tar.gz)
34
+ aurasprout package .\my_app --out .\dist --publish .\catalog --channel stable --json
35
+
36
+ # Push an .apkg or source directory to a running AuraSprout HTTP server
37
+ aurasprout catalog push .\dist\my-app-1.0.0.apkg --server http://127.0.0.1:8765 --token-file ui_daemon.json --json
38
+
39
+ # Install locally from directory, .apkg, or HTTP/FS catalog (user-space only, zero admin privileges)
40
+ aurasprout install my-app --from-catalog http://127.0.0.1:8765/catalog --channel stable --shortcut --json
41
+
42
+ # Manage catalog entries & attached sources
43
+ aurasprout catalog list --installed --json
44
+ aurasprout catalog sources --json
45
+ aurasprout catalog add-source https://example.internal/catalog --default
46
+ aurasprout catalog remove-source https://example.internal/catalog
47
+ aurasprout catalog delete my-app --json
48
+
49
+ # Rollback, uninstall, or clean up user state completely
50
+ aurasprout rollback my-app --json
51
+ aurasprout uninstall my-app --remove-mvenv --json
52
+ aurasprout self-uninstall --yes --json
53
+
54
+ # Inspect or opt in to cell volunteering (opt-in default: false) & resolve conflicts
55
+ aurasprout volunteer status --json
56
+ aurasprout volunteer enable my-service --mode CellSingleton --json
57
+ aurasprout volunteer disable my-service --json
58
+ aurasprout conflicts list --json
59
+ aurasprout conflicts resolve <conflictId> TakeOverStaleLock --json
60
+
61
+ # Start Tier 3 loopback Management Web UI (plus /catalog hosting), Tier 2 IPC bridge, or Desktop Hub
62
+ aurasprout ui --port 8765 --token-file ui_daemon.json --json
63
+ aurasprout ipc --port 8766
64
+ aurasprout desktop
65
+ ```
66
+
67
+ ## HTTP Endpoints & Storage
68
+
69
+ - **Tier 3 Management UI & Hosted Catalog (`aurasprout ui`)**:
70
+ - Public: `GET /`, `GET /api/health`, `GET /api/v1/health`, `GET /catalog/`, `GET /catalog/index.html`, `GET /catalog/catalog.json`, `GET /catalog/apps/<path>`, `GET /catalog/channels/<path>`.
71
+ - Auth & CSRF (`POST /api/v1/auth/session`): `/api/v1/apps`, `/api/v1/apps/install`, `/api/v1/apps/{appId}`, `/api/v1/apps/{appId}/launch`, `/api/v1/catalog`, `/api/v1/catalog/sources`, `POST /api/v1/catalog/publish` (binary/multipart/base64/path), `/api/v1/catalog/apps/{appId}/download`, `/api/v1/volunteer*`, `/api/v1/conflicts*`, `/api/v1/runtimes`.
72
+ - **Tier 2 IPC Bridge (`aurasprout ipc`)**: Loopback `X-AuraGrid-IPC-Token` endpoints under `/health`, `/ready`, `/cell/config*`, `/cell/members*`, `/cell/governance*`, `/cell/events*`, `/cell/lease*`, `/cell/collaboration*`, `/cell/registry*`, `/cell/storage*`, `/cell/tattle`, and `/cell/chaos*`.
73
+ - **User Data Roots**: Resolved via `AURASPROUT_HOME` -> `%LOCALAPPDATA%\AuraSprout` (Windows), `~/Library/Application Support/aurasprout` (macOS), or `$XDG_DATA_HOME/aurasprout` / `~/.local/share/aurasprout` (Linux).
74
+ - **Environment Variables**: `AURASPROUT_HOME`, `AURASPROUT_CELL_ROOT`, `AURAGRID_CELL_ROOT`, `AURASPROUT_STANDALONE_DIR`, `AURASPROUT_UI_HOST`, `AURASPROUT_UI_PORT`, `AURASPROUT_UI_TOKEN`, `AURASPROUT_DEFAULT_CATALOG`, `AURAGRID_IPC_TOKEN`, `AURAGRID_NODE_ID`.
@@ -0,0 +1,61 @@
1
+ # AuraSprout
2
+
3
+ Zero-privilege, user-space application distribution, local supervision, and shared-filesystem coordination layer for **AuraCore**.
4
+
5
+ ## Architecture Boundaries
6
+
7
+ - **`auragrid-api`**: Canonical OpenAPI (`ipc-bridge.openapi.json`, `aurasprout-management.openapi.json`) and JSON Schemas (`app.manifest.schema.json`, `catalog.schema.json`, `lease-collaboration.schema.json`).
8
+ - **`auragrid-py`**: Typed Python SDK consumed by `LocalOnly`, `CellSingleton`, and `Distributed` MAS workloads.
9
+ - **`auraseed`**: Strictly Python discovery & sovereign virtual environment (`mvenv`) management (`aurasprout` composes `auraseed.gui.MvenvManagerPanel`).
10
+ - **`aurasprout`**: Application cataloging & packaging (`.apkg`), HTTP catalog hosting/push, user-space installation/updates/rollback (`active_version.json` + preserved `app_data/`), cleanroom standalone self-install/cleanup, SMB-safe SQLite event bus (`PRAGMA journal_mode = TRUNCATE`), atomic directory mutex & fenced leases/batches (`SproutMutex`), Tier 2 loopback IPC bridge (`X-AuraGrid-IPC-Token`), Tier 3 loopback Management Web UI (`aurasprout_session` + `X-CSRF-Token`), and Tkinter Desktop Hub.
11
+
12
+ ## Quick Start
13
+
14
+ Both `aurasprout` (installed console script / standalone launcher) and `python -m aurasprout` expose the full CLI (supporting global `--cell-root` and `--user-root` flags):
15
+
16
+ ```powershell
17
+ # Install AuraSprout standalone into %LOCALAPPDATA%\AuraSprout\standalone
18
+ python -m aurasprout self-install --add-to-path --shortcut --json
19
+
20
+ # Package & publish an app to a static zero-CDN catalog (supports --wheelhouse, --format zip|tar.gz)
21
+ aurasprout package .\my_app --out .\dist --publish .\catalog --channel stable --json
22
+
23
+ # Push an .apkg or source directory to a running AuraSprout HTTP server
24
+ aurasprout catalog push .\dist\my-app-1.0.0.apkg --server http://127.0.0.1:8765 --token-file ui_daemon.json --json
25
+
26
+ # Install locally from directory, .apkg, or HTTP/FS catalog (user-space only, zero admin privileges)
27
+ aurasprout install my-app --from-catalog http://127.0.0.1:8765/catalog --channel stable --shortcut --json
28
+
29
+ # Manage catalog entries & attached sources
30
+ aurasprout catalog list --installed --json
31
+ aurasprout catalog sources --json
32
+ aurasprout catalog add-source https://example.internal/catalog --default
33
+ aurasprout catalog remove-source https://example.internal/catalog
34
+ aurasprout catalog delete my-app --json
35
+
36
+ # Rollback, uninstall, or clean up user state completely
37
+ aurasprout rollback my-app --json
38
+ aurasprout uninstall my-app --remove-mvenv --json
39
+ aurasprout self-uninstall --yes --json
40
+
41
+ # Inspect or opt in to cell volunteering (opt-in default: false) & resolve conflicts
42
+ aurasprout volunteer status --json
43
+ aurasprout volunteer enable my-service --mode CellSingleton --json
44
+ aurasprout volunteer disable my-service --json
45
+ aurasprout conflicts list --json
46
+ aurasprout conflicts resolve <conflictId> TakeOverStaleLock --json
47
+
48
+ # Start Tier 3 loopback Management Web UI (plus /catalog hosting), Tier 2 IPC bridge, or Desktop Hub
49
+ aurasprout ui --port 8765 --token-file ui_daemon.json --json
50
+ aurasprout ipc --port 8766
51
+ aurasprout desktop
52
+ ```
53
+
54
+ ## HTTP Endpoints & Storage
55
+
56
+ - **Tier 3 Management UI & Hosted Catalog (`aurasprout ui`)**:
57
+ - Public: `GET /`, `GET /api/health`, `GET /api/v1/health`, `GET /catalog/`, `GET /catalog/index.html`, `GET /catalog/catalog.json`, `GET /catalog/apps/<path>`, `GET /catalog/channels/<path>`.
58
+ - Auth & CSRF (`POST /api/v1/auth/session`): `/api/v1/apps`, `/api/v1/apps/install`, `/api/v1/apps/{appId}`, `/api/v1/apps/{appId}/launch`, `/api/v1/catalog`, `/api/v1/catalog/sources`, `POST /api/v1/catalog/publish` (binary/multipart/base64/path), `/api/v1/catalog/apps/{appId}/download`, `/api/v1/volunteer*`, `/api/v1/conflicts*`, `/api/v1/runtimes`.
59
+ - **Tier 2 IPC Bridge (`aurasprout ipc`)**: Loopback `X-AuraGrid-IPC-Token` endpoints under `/health`, `/ready`, `/cell/config*`, `/cell/members*`, `/cell/governance*`, `/cell/events*`, `/cell/lease*`, `/cell/collaboration*`, `/cell/registry*`, `/cell/storage*`, `/cell/tattle`, and `/cell/chaos*`.
60
+ - **User Data Roots**: Resolved via `AURASPROUT_HOME` -> `%LOCALAPPDATA%\AuraSprout` (Windows), `~/Library/Application Support/aurasprout` (macOS), or `$XDG_DATA_HOME/aurasprout` / `~/.local/share/aurasprout` (Linux).
61
+ - **Environment Variables**: `AURASPROUT_HOME`, `AURASPROUT_CELL_ROOT`, `AURAGRID_CELL_ROOT`, `AURASPROUT_STANDALONE_DIR`, `AURASPROUT_UI_HOST`, `AURASPROUT_UI_PORT`, `AURASPROUT_UI_TOKEN`, `AURASPROUT_DEFAULT_CATALOG`, `AURAGRID_IPC_TOKEN`, `AURAGRID_NODE_ID`.
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aurasprout"
7
+ version = "0.1.1"
8
+ description = "User-space zero-privilege AuraGrid cell coordination, application catalog, installer, and supervision layer."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "auraseed>=1.1.0",
13
+ "auragrid>=4.3.0",
14
+ "pydantic>=2.0.0",
15
+ "httpx>=0.25.0",
16
+ ]
17
+
18
+ [project.scripts]
19
+ aurasprout = "aurasprout.cli:main"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+ include = ["aurasprout*"]
24
+
25
+ [tool.setuptools.package-data]
26
+ aurasprout = ["ui/static/*"]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
30
+ pythonpath = ["src"]
31
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """AuraSprout canonical package."""
2
+
3
+ __version__ = "0.1.1"
4
+
@@ -0,0 +1,8 @@
1
+ """AuraSprout main entrypoint wrapper."""
2
+
3
+ import sys
4
+ from aurasprout.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
8
+
@@ -0,0 +1,2 @@
1
+ """Merged Application Catalog & Distribution Engine for AuraSprout."""
2
+
@@ -0,0 +1,101 @@
1
+ """Cryptographic SHA-256 verification and traversal-safe archive extraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import stat
7
+ import tarfile
8
+ import zipfile
9
+ from pathlib import Path
10
+ from typing import List
11
+
12
+ from aurasprout.contracts.paths import validate_within_root
13
+
14
+
15
+ def compute_sha256_bytes(data: bytes) -> str:
16
+ return hashlib.sha256(data).hexdigest()
17
+
18
+
19
+ def compute_sha256(file_path: Path) -> str:
20
+ target = Path(file_path).resolve()
21
+ h = hashlib.sha256()
22
+ with open(target, "rb") as handle:
23
+ while True:
24
+ chunk = handle.read(65536)
25
+ if not chunk:
26
+ break
27
+ h.update(chunk)
28
+ return h.hexdigest()
29
+
30
+
31
+ def verify_sha256(file_path: Path, expected_sha256: str) -> str:
32
+ """Compute SHA-256 of ``file_path`` and raise ValueError on mismatch."""
33
+ expected = (expected_sha256 or "").strip().lower()
34
+ if len(expected) != 64:
35
+ raise ValueError(f"Invalid SHA-256 checksum format: {expected_sha256!r}")
36
+ actual = compute_sha256(file_path).lower()
37
+ if actual != expected:
38
+ raise ValueError(f"SHA-256 mismatch for {file_path}: expected {expected}, got {actual}")
39
+ return actual
40
+
41
+
42
+ def _validate_archive_member_name(name: str) -> None:
43
+ if not name or "\x00" in name:
44
+ raise ValueError(f"Invalid archive member name: {name!r}")
45
+ normalized = name.replace("\\", "/")
46
+ if normalized.startswith("/") or (len(normalized) >= 2 and normalized[1] == ":"):
47
+ raise ValueError(f"Absolute member path rejected in archive: {name!r}")
48
+ parts = [p for p in normalized.split("/") if p]
49
+ if ".." in parts:
50
+ raise ValueError(f"Path traversal member rejected in archive: {name!r}")
51
+
52
+
53
+ def extract_archive_safely(archive_path: Path, dest_dir: Path) -> List[Path]:
54
+ """Safely extract a `.apkg` / `.zip` / `.tar.gz` archive into ``dest_dir``.
55
+
56
+ Rejects Zip-Slip, Tar-Slip (`..`, rooted paths, Windows drive letters),
57
+ symlinks, hardlinks, and device nodes BEFORE writing any files to ``dest_dir``.
58
+ """
59
+ src = Path(archive_path).resolve()
60
+ dst = Path(dest_dir).resolve()
61
+ dst.mkdir(parents=True, exist_ok=True)
62
+ extracted: List[Path] = []
63
+
64
+ if zipfile.is_zipfile(src):
65
+ with zipfile.ZipFile(src, "r") as zf:
66
+ members = zf.infolist()
67
+ for info in members:
68
+ _validate_archive_member_name(info.filename)
69
+ mode = (info.external_attr >> 16) & 0o170000
70
+ if stat.S_ISLNK(mode):
71
+ raise ValueError(f"ZIP symlink member rejected: {info.filename!r}")
72
+ validate_within_root(dst, info.filename)
73
+
74
+ for info in members:
75
+ target = validate_within_root(dst, info.filename)
76
+ if info.is_dir():
77
+ target.mkdir(parents=True, exist_ok=True)
78
+ else:
79
+ target.parent.mkdir(parents=True, exist_ok=True)
80
+ with zf.open(info, "r") as reader, open(target, "wb") as writer:
81
+ writer.write(reader.read())
82
+ extracted.append(target)
83
+ return extracted
84
+
85
+ if tarfile.is_tarfile(src):
86
+ with tarfile.open(src, "r:*") as tf:
87
+ members = tf.getmembers()
88
+ for member in members:
89
+ _validate_archive_member_name(member.name)
90
+ if member.issym() or member.islnk() or member.isdev() or member.isfifo():
91
+ raise ValueError(f"Unsafe tar member type rejected: {member.name!r}")
92
+ validate_within_root(dst, member.name)
93
+
94
+ tf.extractall(path=dst, members=members, filter="data")
95
+ for member in members:
96
+ if member.isfile():
97
+ extracted.append(validate_within_root(dst, member.name))
98
+ return extracted
99
+
100
+ raise ValueError(f"Unsupported package archive format: {src}")
101
+
@@ -0,0 +1,202 @@
1
+ """Zero-CDN self-contained static HTML5 Application Catalog Portal generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import json
7
+ from typing import Optional
8
+
9
+ from aurasprout.catalog.models import CatalogManifest
10
+
11
+
12
+ def generate_catalog_html(manifest: CatalogManifest, catalog_url: Optional[str] = None) -> str:
13
+ """Render a completely offline, zero-CDN HTML5 portal for a CatalogManifest."""
14
+ title = html.escape(manifest.catalog_name)
15
+ desc = html.escape(manifest.description or "Unified Zero-Privilege Application Catalog")
16
+ updated = html.escape(manifest.updated_at[:19].replace("T", " ") + " UTC")
17
+ effective_catalog_url = html.escape(catalog_url or manifest.base_url or "./catalog.json")
18
+
19
+ all_tags = sorted({tag for app in manifest.apps.values() for tag in app.tags})
20
+ cards_html = []
21
+ for app in sorted(manifest.apps.values(), key=lambda a: a.app_id):
22
+ v_info = app.get_version(app.latest_version)
23
+ v_str = html.escape(v_info.version if v_info else app.latest_version)
24
+ ch_str = html.escape(v_info.channel if v_info else app.channel)
25
+ app_disp = html.escape(app.display_name or app.name)
26
+ app_id_esc = html.escape(app.app_id)
27
+ desc_esc = html.escape(app.description or "No description provided.")
28
+ tags_html = " ".join(f'<span class="pill">{html.escape(t)}</span>' for t in app.tags)
29
+ sha_str = html.escape(v_info.bundle_sha256 if v_info else "")
30
+ sha_short = f"{sha_str[:16]}..." if sha_str else "none"
31
+ letter = html.escape((app.display_name or app.app_id)[:1].upper() or "A")
32
+ bundle_url = html.escape(
33
+ (v_info.bundle_url if v_info and v_info.bundle_url else f"apps/{app_id_esc}/{app_id_esc}-{v_str}.apkg")
34
+ )
35
+ ver_options = "".join(
36
+ f'<option value="{html.escape(ver_k)}">v{html.escape(ver_k)} ({html.escape(ver_obj.channel)})</option>'
37
+ for ver_k, ver_obj in sorted(app.versions.items(), reverse=True)
38
+ )
39
+
40
+ cards_html.append(
41
+ f"""
42
+ <div class="app-card" data-app-id="{app_id_esc}" data-channel="{ch_str}">
43
+ <div>
44
+ <div class="card-header">
45
+ <div class="app-avatar">{letter}</div>
46
+ <div class="header-text">
47
+ <div class="app-title" title="{app_disp}">{app_disp}</div>
48
+ <div class="app-meta">
49
+ <span class="app-slug">{app_id_esc}</span>
50
+ <span class="version-badge">v{v_str}</span>
51
+ <span class="channel-badge">{ch_str}</span>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ <p class="app-desc">{desc_esc}</p>
56
+ <div class="tags-list">{tags_html}</div>
57
+ </div>
58
+ <div class="card-actions">
59
+ <select class="ver-select" aria-label="Available versions">{ver_options}</select>
60
+ <div class="checksum-bar" title="SHA-256: {sha_str}">
61
+ <span>SHA-256:</span>
62
+ <span class="checksum-hash">{sha_short}</span>
63
+ <button class="copy-btn" type="button" onclick="copyText('{sha_str}', this)">Copy</button>
64
+ </div>
65
+ <div class="buttons-row">
66
+ <button class="btn-install" type="button" onclick="openInstallModal('{app_id_esc}', '{v_str}')">
67
+ Install CLI Command
68
+ </button>
69
+ <a href="{bundle_url}" class="btn-download" download>Download .apkg</a>
70
+ </div>
71
+ </div>
72
+ </div>"""
73
+ )
74
+
75
+ tag_buttons = '<button class="tag-btn active" type="button" data-tag="all">All</button>\n' + "\n".join(
76
+ f'<button class="tag-btn" type="button" data-tag="{html.escape(t)}">{html.escape(t)}</button>'
77
+ for t in all_tags
78
+ )
79
+ manifest_json_escaped = json.dumps(manifest.to_dict()).replace("</script>", "<\\/script>")
80
+ rendered_cards = "\n".join(cards_html) or "<p class='empty'>No applications published yet.</p>"
81
+
82
+ return f"""<!DOCTYPE html>
83
+ <html lang="en">
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>{title}</title>
88
+ <style>
89
+ :root {{
90
+ --bg: #0d1117;
91
+ --card-bg: #161b22;
92
+ --card-border: #30363d;
93
+ --text: #e6edf3;
94
+ --muted: #8b949e;
95
+ --accent: #58a6ff;
96
+ --green: #238636;
97
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
98
+ }}
99
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
100
+ body {{ font-family: var(--font); background: var(--bg); color: var(--text); padding: 1.5rem; }}
101
+ header {{ max-width: 1200px; margin: 0 auto 1.5rem auto; border-bottom: 1px solid var(--card-border); padding-bottom: 1rem; }}
102
+ h1 {{ font-size: 1.75rem; margin-bottom: 0.25rem; }}
103
+ .sub {{ color: var(--muted); font-size: 0.9rem; }}
104
+ .controls {{ max-width: 1200px; margin: 0 auto 1.25rem auto; display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }}
105
+ input, select {{ background: var(--card-bg); color: var(--text); border: 1px solid var(--card-border); border-radius: 6px; padding: 0.45rem 0.75rem; }}
106
+ .tags-bar {{ display: flex; gap: 0.4rem; flex-wrap: wrap; }}
107
+ .tag-btn {{ background: var(--card-bg); color: var(--text); border: 1px solid var(--card-border); border-radius: 999px; padding: 0.3rem 0.75rem; cursor: pointer; }}
108
+ .tag-btn.active {{ border-color: var(--accent); color: var(--accent); }}
109
+ .grid {{ max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }}
110
+ .app-card {{ background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 10px; padding: 1rem; display: flex; flex-direction: column; justify-content: space-between; gap: 0.85rem; }}
111
+ .card-header {{ display: flex; align-items: center; gap: 0.75rem; }}
112
+ .app-avatar {{ width: 38px; height: 38px; border-radius: 8px; background: #1f6feb; display: grid; place-items: center; font-weight: 700; }}
113
+ .app-title {{ font-weight: 600; font-size: 1.05rem; }}
114
+ .app-meta {{ font-size: 0.8rem; color: var(--muted); display: flex; gap: 0.4rem; margin-top: 0.15rem; }}
115
+ .version-badge, .channel-badge, .pill {{ background: #21262d; padding: 0.1rem 0.45rem; border-radius: 6px; font-size: 0.75rem; }}
116
+ .app-desc {{ color: var(--muted); font-size: 0.88rem; margin-top: 0.6rem; }}
117
+ .tags-list {{ display: flex; gap: 0.3rem; flex-wrap: wrap; margin-top: 0.5rem; }}
118
+ .checksum-bar {{ font-size: 0.75rem; color: var(--muted); display: flex; align-items: center; justify-content: space-between; margin: 0.4rem 0; }}
119
+ .copy-btn {{ background: #21262d; color: var(--text); border: 1px solid var(--card-border); border-radius: 5px; padding: 0.15rem 0.45rem; cursor: pointer; }}
120
+ .buttons-row {{ display: flex; gap: 0.5rem; }}
121
+ .btn-install {{ flex: 1; background: var(--green); color: #fff; border: none; border-radius: 6px; padding: 0.45rem; cursor: pointer; font-weight: 600; }}
122
+ .btn-download {{ background: #21262d; color: var(--text); border: 1px solid var(--card-border); border-radius: 6px; padding: 0.45rem 0.75rem; text-decoration: none; font-size: 0.85rem; }}
123
+ .modal {{ position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); display: none; align-items: center; justify-content: center; }}
124
+ .modal-box {{ background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 10px; max-width: 600px; width: 92%; padding: 1.25rem; }}
125
+ pre {{ background: #0d1117; border: 1px solid var(--card-border); padding: 0.75rem; border-radius: 6px; overflow-x: auto; margin: 0.75rem 0; }}
126
+ </style>
127
+ </head>
128
+ <body>
129
+ <header>
130
+ <h1>{title}</h1>
131
+ <p class="sub">{desc} | Updated: {updated} | Apps: {len(manifest.apps)}</p>
132
+ </header>
133
+ <div class="controls">
134
+ <input id="search-input" type="search" placeholder="Filter applications...">
135
+ <select id="channel-filter">
136
+ <option value="all">All channels</option>
137
+ <option value="stable">stable</option>
138
+ <option value="preview">preview</option>
139
+ <option value="canary">canary</option>
140
+ <option value="nightly">nightly</option>
141
+ </select>
142
+ <div class="tags-bar">{tag_buttons}</div>
143
+ </div>
144
+ <div id="app-grid" class="grid">{rendered_cards}</div>
145
+ <div id="install-modal" class="modal">
146
+ <div class="modal-box">
147
+ <h2 id="modal-app-title">Deploy Application</h2>
148
+ <pre id="modal-code"></pre>
149
+ <button id="modal-copy-btn" class="btn-install" type="button" onclick="copyModalCode()">Copy Command</button>
150
+ <button class="copy-btn" type="button" onclick="closeModal()">Close</button>
151
+ </div>
152
+ </div>
153
+ <script>
154
+ const CATALOG_URL = {json.dumps(effective_catalog_url)};
155
+ const MANIFEST = {manifest_json_escaped};
156
+ let activeTag = "all";
157
+ function filterCards() {{
158
+ const q = (document.getElementById("search-input").value || "").toLowerCase();
159
+ const ch = document.getElementById("channel-filter").value;
160
+ document.querySelectorAll(".app-card").forEach((card) => {{
161
+ const appId = card.dataset.appId;
162
+ const entry = (MANIFEST.apps || {{}})[appId] || {{}};
163
+ const text = (appId + " " + (entry.displayName || "") + " " + (entry.description || "")).toLowerCase();
164
+ const tags = entry.tags || [];
165
+ const matchQ = !q || text.includes(q);
166
+ const matchCh = ch === "all" || card.dataset.channel === ch;
167
+ const matchTag = activeTag === "all" || tags.includes(activeTag);
168
+ card.style.display = (matchQ && matchCh && matchTag) ? "flex" : "none";
169
+ }});
170
+ }}
171
+ document.getElementById("search-input").addEventListener("input", filterCards);
172
+ document.getElementById("channel-filter").addEventListener("change", filterCards);
173
+ document.querySelectorAll(".tag-btn").forEach((btn) => {{
174
+ btn.addEventListener("click", () => {{
175
+ document.querySelectorAll(".tag-btn").forEach((b) => b.classList.remove("active"));
176
+ btn.classList.add("active");
177
+ activeTag = btn.dataset.tag;
178
+ filterCards();
179
+ }});
180
+ }});
181
+ function copyText(text, btn) {{
182
+ if (navigator.clipboard) navigator.clipboard.writeText(text);
183
+ const orig = btn.textContent;
184
+ btn.textContent = "Copied";
185
+ setTimeout(() => {{ btn.textContent = orig; }}, 1200);
186
+ }}
187
+ function openInstallModal(appId, version) {{
188
+ document.getElementById("modal-app-title").textContent = "Install " + appId + " v" + version;
189
+ document.getElementById("modal-code").textContent =
190
+ 'aurasprout install ' + appId + ' --version ' + version + ' --from-catalog "' + CATALOG_URL + '"';
191
+ document.getElementById("install-modal").style.display = "flex";
192
+ }}
193
+ function closeModal() {{ document.getElementById("install-modal").style.display = "none"; }}
194
+ function copyModalCode() {{
195
+ const code = document.getElementById("modal-code").textContent;
196
+ copyText(code, document.getElementById("modal-copy-btn"));
197
+ }}
198
+ </script>
199
+ </body>
200
+ </html>
201
+ """
202
+