"""Package the optional Windows CLI/MCP client; reuse the installed desktop models.""" import argparse import hashlib import json import os from pathlib import Path import tempfile import zipfile from package_windows import pe_machine from stage_extraction_worker import verify ROOT = Path(__file__).resolve().parents[1] MAX_FILE = 64 * 1024 * 1024 def package(root, executable, resources, output): root, executable, resources, output = map(Path, (root, executable, resources, output)) def read(path): if path.is_symlink() or not path.is_file() or path.absolute() != path.resolve(): raise ValueError('Package input missing or linked') with path.open('rb') as stream: data = stream.read(MAX_FILE + 1) if not data or len(data) > MAX_FILE: raise ValueError('Package input empty or oversized') return data cli = read(executable) architecture = {0x8664: 'x86_64', 0xAA64: 'aarch64'}.get(pe_machine(executable)) if architecture is None: raise ValueError('Unsupported Windows CLI architecture') expected = verify(resources, root, 'windows', architecture) selected = {'searchi-cli.exe': cli, 'LICENSE.txt': read(root / 'LICENSE')} for name, record in expected.items(): data = read(resources / name) if len(data) != record['bytes'] or hashlib.sha256(data).hexdigest() != record['sha256']: raise ValueError('Extraction resource changed while packaging') selected[name] = data # The captured CLI must still match the header used for architecture validation. if read(executable) != cli: raise ValueError('CLI changed while packaging') records = [{'path': name, 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()} for name, data in selected.items()] output.parent.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile(dir=output.parent, suffix='.partial', delete=False) as temporary: temporary_path = Path(temporary.name) try: with zipfile.ZipFile(temporary_path, 'w', zipfile.ZIP_DEFLATED) as archive: for name, data in selected.items(): archive.writestr(name, data) archive.writestr('PACKAGE.json', json.dumps({'platform': 'windows', 'architecture': architecture, 'status': 'optional unsigned CLI/MCP client; install desktop for model setup', 'files': records}, indent=2)) os.replace(temporary_path, output) finally: temporary_path.unlink(missing_ok=True) return architecture def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--executable', type=Path, required=True) parser.add_argument('--resources', type=Path, default=ROOT / 'apps/desktop/src-tauri/resources') parser.add_argument('--output', type=Path, default=ROOT / '.searchi/packages/searchi-cli_0.1.0_x64-013.zip') args = parser.parse_args() print(f"Packaged Windows {package(ROOT, args.executable, args.resources, args.output)} CLI: {args.output}") if __name__ == '__main__': main()