#!/usr/bin/env python3 """Install Helas reviewed local skill bundle. No APIs or background jobs.""" import argparse import hashlib import json import os from pathlib import Path import shutil import tempfile import urllib.request from datetime import datetime BASE='https://helasmichi.de/verlag/' NAMES={'verlagsloop-hela','rauschwerk-buchplan','100m-offers-practice'} def fetch(path): with urllib.request.urlopen(BASE+path,timeout=40) as r: return r.read() def main(): p=argparse.ArgumentParser(description=__doc__) p.add_argument('--home',type=Path,default=Path.home()) p.add_argument('--source',type=Path,help='Use an already downloaded verlag directory') a=p.parse_args(); home=a.home.expanduser().resolve() def get(rel): return (a.source/rel).read_bytes() if a.source else fetch(rel) manifest=json.loads(get('manifest.json')) if manifest['version']!='2.0.0': raise ValueError('Unbekannte Paketversion') target=home/'.agents/skills'; target.mkdir(parents=True,exist_ok=True) backup=home/'HELA_Buecher/Installationssicherungen'/datetime.now().strftime('%Y%m%d-%H%M%S-%f') # Verify the entire bundle before changing installed skills. with tempfile.TemporaryDirectory(prefix='hela-install-') as tmp: stage=Path(tmp) for rel,digest in manifest['files'].items(): parts=Path(rel).parts if len(parts)<3 or parts[0]!='paket' or parts[1] not in NAMES or '..' in parts: raise ValueError('Ungueltiger Paketpfad') data=get(rel) if hashlib.sha256(data).hexdigest()!=digest: raise ValueError('Pruefsumme falsch: '+rel) dest=stage.joinpath(*parts[1:]); dest.parent.mkdir(parents=True,exist_ok=True); dest.write_bytes(data) for name in sorted(NAMES): src=stage/name; dest=target/name if not (src/'SKILL.md').is_file(): raise ValueError('Skill fehlt: '+name) # Preserve edits; never remove a whole existing skill directory. for source in sorted(src.rglob('*')): if not source.is_file(): continue relative=source.relative_to(src); out=dest/relative if out.exists() and out.read_bytes()!=source.read_bytes(): old=backup/name/relative; old.parent.mkdir(parents=True,exist_ok=True); shutil.copy2(out,old) out.parent.mkdir(parents=True,exist_ok=True) fd,temp=tempfile.mkstemp(dir=out.parent,prefix='.install-') with os.fdopen(fd,'wb') as f: f.write(source.read_bytes()) os.replace(temp,out) root=home/'HELA_Buecher'; root.mkdir(exist_ok=True) skill=target/'verlagsloop-hela/SKILL.md' templates={ 'RICHTUNG.md':'# Helas Buchrichtung\n\nNoch nicht festgelegt. Thema und Leser im Chat mit Hela klaeren.\n', 'STATUS.json':json.dumps(dict(version=2,revision=0,phase='onboarding',next_step='Hela nach Thema und Leserinnen fragen',checkpoints=[],lease=None),ensure_ascii=False,indent=2)+'\n', 'ZEITPLAN.json':json.dumps(dict(active=False,reason='Im Einrichtungschat nach Themenwahl anlegen und verifizieren'),ensure_ascii=False,indent=2)+'\n', 'AUFGABE.md':f'Lies {skill} und {root / "STATUS.json"}. Arbeitsordner: {root}. Beachte STOP, Phase und Sperre. Fuehre genau einen naechsten gespeicherten Schritt an Helas bestaetigtem Buch aus. Speichere und pruefe ihn nach dem Skill. Keine erneute Vollrecherche, kein erfundenes Thema, keine Zusatzkosten, keine Veroeffentlichung. Bei Nutzungsgrenze keine schnellen Wiederholungen; naechster regulaerer Lauf. Bei fertigem Lesepaket stoppen.\n', } for name,content in templates.items(): path=root/name try: with path.open('x',encoding='utf-8') as f:f.write(content) except FileExistsError: pass print(json.dumps(dict(installed=sorted(NAMES),skills_dir=str(target),workspace=str(root),version=manifest['version'],schedule='Nicht durch Installer aktiviert; echte Aufgabenfunktion im Einrichtungschat verwenden',next='SKILL.md jetzt lesen und Hela im Chat nach Thema und Leserinnen fragen'),ensure_ascii=False,indent=2)) if __name__=='__main__': main()