mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 21:27:41 +02:00
[eric] marketplace: Get on every card and sheet, spinner, then Open; installs remembered by the backend and Open launches the app, workflow or skill
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9zwUaHucUgrdxvK8FvjYT
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
2afbdc5b05
commit
bc1e7acac9
@@ -0,0 +1,70 @@
|
||||
"""What this machine installed from the marketplace, by listing id.
|
||||
|
||||
The App Store knows what is installed; without a record every package showed Install again after a
|
||||
restart, and Open had nothing to open. The importer creates a fresh entity per commit and returns its
|
||||
id, so the record is written at commit time and maps a listing to the thing it became. A record whose
|
||||
entity has since been deleted is stale by design: the store re-checks the live lists before showing Open.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.config.paths import DATA_ROOT
|
||||
|
||||
INSTALLS_PATH = os.path.join(DATA_ROOT, "marketplace_installs.json")
|
||||
|
||||
|
||||
class InstallRecord(BaseModel):
|
||||
"""One pointer per importable root, typed, so an id can never point at a kind nobody can resolve."""
|
||||
|
||||
model_config = ConfigDict(validate_assignment=True)
|
||||
|
||||
listing_id: str
|
||||
root_type: str
|
||||
output_id: Optional[str] = None
|
||||
skill_id: Optional[str] = None
|
||||
workflow_id: Optional[str] = None
|
||||
dashboard_id: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
version: str = ""
|
||||
installed_at: float = 0.0
|
||||
|
||||
def root_id(self) -> Optional[str]:
|
||||
return self.output_id or self.skill_id or self.workflow_id or self.dashboard_id or self.session_id
|
||||
|
||||
|
||||
@typechecked
|
||||
def load_installs(path: Optional[str] = None) -> Dict[str, InstallRecord]:
|
||||
p = path or INSTALLS_PATH
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||
return {}
|
||||
out: Dict[str, InstallRecord] = {}
|
||||
for key, value in (raw or {}).items():
|
||||
try:
|
||||
out[key] = InstallRecord.model_validate(value)
|
||||
except Exception:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
@typechecked
|
||||
def record_install(rec: InstallRecord, path: Optional[str] = None) -> Dict[str, InstallRecord]:
|
||||
p = path or INSTALLS_PATH
|
||||
installs = load_installs(p)
|
||||
if rec.installed_at == 0.0:
|
||||
rec = rec.model_copy(update={"installed_at": time.time()})
|
||||
installs[rec.listing_id] = rec
|
||||
os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
|
||||
tmp = p + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump({k: v.model_dump() for k, v in installs.items()}, f, indent=1)
|
||||
os.replace(tmp, p)
|
||||
return installs
|
||||
@@ -14,6 +14,7 @@ from fastapi import HTTPException
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from backend.apps.marketplace import catalog
|
||||
from backend.apps.marketplace.installs import InstallRecord, load_installs, record_install
|
||||
from backend.apps.marketplace.package_download import (
|
||||
DownloadRefused,
|
||||
download_package,
|
||||
@@ -61,3 +62,21 @@ async def install_preflight(body: InstallRequest) -> ImportPreflightResponse:
|
||||
logger.warning("marketplace download refused for %s: %s", listing.id, e)
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return stage_bundle_for_import(raw, package_filename(listing.id, listing.title))
|
||||
|
||||
|
||||
class InstallsResponse(BaseModel):
|
||||
model_config = ConfigDict(validate_assignment=True)
|
||||
|
||||
installs: dict[str, InstallRecord]
|
||||
|
||||
|
||||
@marketplace.router.get("/installed")
|
||||
async def get_installed() -> InstallsResponse:
|
||||
return InstallsResponse(installs=load_installs())
|
||||
|
||||
|
||||
@marketplace.router.post("/installed")
|
||||
async def post_installed(body: InstallRecord) -> InstallsResponse:
|
||||
if not body.listing_id or not body.root_type or not body.root_id():
|
||||
raise HTTPException(status_code=400, detail="listing_id, root_type and the id of what was installed are required")
|
||||
return InstallsResponse(installs=record_install(body))
|
||||
|
||||
Reference in New Issue
Block a user