"""Publish command handler for script runner.""" import argparse import subprocess import sys from pathlib import Path # Publishable packages PUBLISHABLE_PACKAGES = { "imagen-app": { "dir": "imagen-app", "name": "@lilith/imagen-app", }, "imagen-react": { "dir": "react", "name": "@lilith/imagen-react", }, "imagen-electron": { "dir": "electron", "name": "@lilith/imagen-electron", }, "image-generation-types": { "dir": "image-generation/types", "name": "@lilith/image-generation-types", }, "image-generation-client": { "dir": "image-generation/client", "name": "@lilith/image-generation-client", }, "imagegen-assistant-types": { "dir": "imagegen-assistant/types", "name": "@lilith/imagegen-assistant-types", }, "imagegen-assistant-client": { "dir": "imagegen-assistant/client", "name": "@lilith/imagegen-assistant-client", }, "image-processing-types": { "dir": "image-processing/types", "name": "@lilith/image-processing-types", }, "image-processing-client": { "dir": "image-processing/client", "name": "@lilith/image-processing-client", }, } def publish_command(args, workspace_root: Path): """Publish packages to registry. Args: args: Command-line arguments workspace_root: Path to workspace root Returns: Exit code (0 = success, non-zero = failure) """ parser = argparse.ArgumentParser( prog="./run publish", description="Publish packages to registry", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=f""" Examples: ./run publish # Publish all packages (interactive) ./run publish --all --dry-run # Preview what would be published ./run publish imagen-app # Publish specific package ./run publish --skip-build # Publish without building first Available packages: {', '.join(PUBLISHABLE_PACKAGES.keys())} Registry: http://forge.black.lan/api/packages/lilith/npm/ Note: Packages are published in dependency order. Related packages (imagen-app + imagen-react + imagen-electron) should be versioned together. """, ) parser.add_argument( "package", nargs="?", choices=list(PUBLISHABLE_PACKAGES.keys()) + ["all"], help="Package to publish (default: interactive)", ) parser.add_argument( "--all", action="store_true", help="Publish all packages", ) parser.add_argument( "--dry-run", action="store_true", help="Preview what would be published without actually publishing", ) parser.add_argument( "--skip-build", action="store_true", help="Skip building before publishing", ) parser.add_argument( "-v", "--verbose", action="store_true", help="Verbose output", ) parsed_args = parser.parse_args(args) # Determine packages to publish if parsed_args.all or parsed_args.package == "all": packages = list(PUBLISHABLE_PACKAGES.keys()) elif parsed_args.package: packages = [parsed_args.package] else: # Interactive mode print("Available packages:") for i, pkg in enumerate(PUBLISHABLE_PACKAGES.keys(), 1): print(f" {i}. {pkg}") print(f" {len(PUBLISHABLE_PACKAGES) + 1}. all") print() try: choice = input("Select package to publish (number or name): ").strip() if choice.isdigit(): idx = int(choice) - 1 if idx == len(PUBLISHABLE_PACKAGES): packages = list(PUBLISHABLE_PACKAGES.keys()) elif 0 <= idx < len(PUBLISHABLE_PACKAGES): packages = [list(PUBLISHABLE_PACKAGES.keys())[idx]] else: print("Invalid selection") return 1 elif choice in PUBLISHABLE_PACKAGES: packages = [choice] elif choice == "all": packages = list(PUBLISHABLE_PACKAGES.keys()) else: print(f"Unknown package: {choice}") return 1 except (KeyboardInterrupt, EOFError): print("\nCancelled") return 0 print(f"Publishing {len(packages)} package(s)...") if parsed_args.dry_run: print("DRY RUN MODE - No actual publishing will occur\n") print() failed = [] succeeded = [] for pkg in packages: pkg_info = PUBLISHABLE_PACKAGES[pkg] pkg_path = workspace_root / pkg_info["dir"] if not pkg_path.exists(): print(f"⊘ SKIP: {pkg} (directory not found)") continue print(f"▶ Publishing: {pkg} ({pkg_info['name']})") # Build if not skipped if not parsed_args.skip_build: print(" Building...") build_cmd = ["npm", "run", "build"] result = subprocess.run( build_cmd, cwd=pkg_path, capture_output=not parsed_args.verbose, check=False, ) if result.returncode != 0: print(f"✗ FAIL: {pkg} (build failed)") if not parsed_args.verbose and result.stderr: print(f" Error: {result.stderr.decode()[:200]}") failed.append(pkg) print() continue # Publish publish_cmd = ["npm", "publish"] if parsed_args.dry_run: publish_cmd.append("--dry-run") result = subprocess.run( publish_cmd, cwd=pkg_path, capture_output=not parsed_args.verbose, check=False, ) if result.returncode == 0: print(f"✓ PASS: {pkg}") succeeded.append(pkg) else: print(f"✗ FAIL: {pkg}") if not parsed_args.verbose and result.stderr: print(f" Error: {result.stderr.decode()[:200]}") failed.append(pkg) print() # Summary print("─" * 50) print(f"Published: {len(succeeded)}/{len(packages)} succeeded") if failed: print(f"\nFailed packages:") for pkg in failed: print(f" ✗ {pkg}") return 1 print("\n✓ All packages published successfully") if parsed_args.dry_run: print("(DRY RUN - no actual changes made)") return 0 def register_publish_command(runner): """Register the publish command with the script runner. Args: runner: ScriptRunner instance """ runner.register_command( "publish", publish_command, "Publish packages to registry", )