This commit is contained in:
2026-02-12 09:42:59 +02:00
commit 906adb539d
87 changed files with 5288 additions and 0 deletions

181
commands/package.py Normal file
View File

@@ -0,0 +1,181 @@
"""flow package — binary package management from manifest definitions."""
import json
import subprocess
import sys
from typing import Any, Dict, Optional, Tuple
from flow.core.config import FlowContext
from flow.core.paths import INSTALLED_STATE
from flow.core.variables import substitute_template
def register(subparsers):
p = subparsers.add_parser("package", aliases=["pkg"], help="Manage binary packages")
sub = p.add_subparsers(dest="package_command")
# install
inst = sub.add_parser("install", help="Install packages from manifest")
inst.add_argument("packages", nargs="+", help="Package names to install")
inst.add_argument("--dry-run", action="store_true", help="Show what would be done")
inst.set_defaults(handler=run_install)
# list
ls = sub.add_parser("list", help="List installed and available packages")
ls.add_argument("--all", action="store_true", help="Show all available packages")
ls.set_defaults(handler=run_list)
# remove
rm = sub.add_parser("remove", help="Remove installed packages")
rm.add_argument("packages", nargs="+", help="Package names to remove")
rm.set_defaults(handler=run_remove)
p.set_defaults(handler=lambda ctx, args: p.print_help())
def _load_installed() -> dict:
if INSTALLED_STATE.exists():
with open(INSTALLED_STATE) as f:
return json.load(f)
return {}
def _save_installed(state: dict):
INSTALLED_STATE.parent.mkdir(parents=True, exist_ok=True)
with open(INSTALLED_STATE, "w") as f:
json.dump(state, f, indent=2)
def _get_definitions(ctx: FlowContext) -> dict:
"""Get package definitions from manifest (binaries section)."""
return ctx.manifest.get("binaries", {})
def _resolve_download_url(
pkg_def: Dict[str, Any],
platform_str: str,
) -> Optional[Tuple[str, Dict[str, str]]]:
"""Build GitHub release download URL from package definition."""
source = pkg_def.get("source", "")
if not source.startswith("github:"):
return None
owner_repo = source[len("github:"):]
version = pkg_def.get("version", "")
asset_pattern = pkg_def.get("asset-pattern", "")
platform_map = pkg_def.get("platform-map", {})
mapping = platform_map.get(platform_str)
if not mapping:
return None
# Build template context
template_ctx = {**mapping, "version": version}
asset = substitute_template(asset_pattern, template_ctx)
url = f"https://github.com/{owner_repo}/releases/download/v{version}/{asset}"
template_ctx["downloadUrl"] = url
return url, template_ctx
def run_install(ctx: FlowContext, args):
definitions = _get_definitions(ctx)
installed = _load_installed()
platform_str = ctx.platform.platform
had_error = False
for pkg_name in args.packages:
pkg_def = definitions.get(pkg_name)
if not pkg_def:
ctx.console.error(f"Package not found in manifest: {pkg_name}")
had_error = True
continue
ctx.console.info(f"Installing {pkg_name} v{pkg_def.get('version', '?')}...")
result = _resolve_download_url(pkg_def, platform_str)
if not result:
ctx.console.error(f"No download available for {pkg_name} on {platform_str}")
had_error = True
continue
url, template_ctx = result
if args.dry_run:
ctx.console.info(f"[{pkg_name}] Would download: {url}")
install_script = pkg_def.get("install-script", "")
if install_script:
ctx.console.info(f"[{pkg_name}] Would run install script")
continue
# Run install script with template vars resolved
install_script = pkg_def.get("install-script", "")
if not install_script:
ctx.console.error(f"Package '{pkg_name}' has no install-script")
had_error = True
continue
resolved_script = substitute_template(install_script, template_ctx)
ctx.console.info(f"Running install script for {pkg_name}...")
proc = subprocess.run(
resolved_script, shell=True,
capture_output=False,
)
if proc.returncode != 0:
ctx.console.error(f"Install script failed for {pkg_name}")
had_error = True
continue
installed[pkg_name] = {
"version": pkg_def.get("version", ""),
"source": pkg_def.get("source", ""),
}
ctx.console.success(f"Installed {pkg_name} v{pkg_def.get('version', '')}")
_save_installed(installed)
if had_error:
sys.exit(1)
def run_list(ctx: FlowContext, args):
definitions = _get_definitions(ctx)
installed = _load_installed()
headers = ["PACKAGE", "INSTALLED", "AVAILABLE"]
rows = []
if args.all:
# Show all defined packages
if not definitions:
ctx.console.info("No packages defined in manifest.")
return
for name, pkg_def in sorted(definitions.items()):
inst_ver = installed.get(name, {}).get("version", "-")
avail_ver = pkg_def.get("version", "?")
rows.append([name, inst_ver, avail_ver])
else:
# Show installed only
if not installed:
ctx.console.info("No packages installed.")
return
for name, info in sorted(installed.items()):
avail = definitions.get(name, {}).get("version", "?")
rows.append([name, info.get("version", "?"), avail])
ctx.console.table(headers, rows)
def run_remove(ctx: FlowContext, args):
installed = _load_installed()
for pkg_name in args.packages:
if pkg_name not in installed:
ctx.console.warn(f"Package not installed: {pkg_name}")
continue
# Remove from installed state
del installed[pkg_name]
ctx.console.success(f"Removed {pkg_name} from installed packages")
ctx.console.warn("Note: binary files were not automatically deleted. Remove manually if needed.")
_save_installed(installed)