"""`mld import ` : brouillons de manifestes à partir des processus PM2 / launchd vivants d'un nœud. Regroupe par préfixe de nom (lou-ka-web, lou-ka-sync, lou-ka-ngrok → lou-ka ; un vieux tunnel ngrok devient une route MacLustr Tunnel `tunnel.domain`). Les brouillons vont dans ~/dispatch/apps/_drafts/.json ; on les relit, on ajuste, puis on les déplace dans ~/dispatch/apps/. """ import json import os import re from . import config, manifest, nodes, ssh SUFFIXES = ("-web", "-sync", "-ngrok", "-api", "-tunnel", "-server", "-etl", "-scheduler", "-ingest", "-market", "-worker", "-proxy", "-crawler", "-enrichment", "-sitemaps", "-tun-search", "-tun-embed") ENV_DROP_PREFIX = ("PM2_", "npm_", "__", "TERM", "SSH_", "LC_", "HOMEBREW", "ITERM", "XPC_", "COLOR", "LS_", "SHELL", "PWD", "OLDPWD", "PATH", "HOME", "USER", "LOGNAME", "LANG", "TMPDIR", "SECURITYSESSIONID", "COMMAND_MODE", "unique_id", "FPATH", "CLAUDE", "OTEL", "DISPLAY", "MallocNanoZone", "ZSH", "EDITOR", "PAGER", "LESS", "NoDefaultCurrentDirectoryInExePath", "COREPACK", "AI_AGENT", "OSLogRateLimit", "GIT_", "ANTHROPIC_MODEL", "DISABLE_", "MANPATH", "INFOPATH", "STARSHIP", "TERMINFO", "WINDOWID", "NVM", "P9K", "TMUX", "SSH", "_") def app_of(name): for s in SUFFIXES: if name.endswith(s): return name[: -len(s)] return name def clean_env(env, name): out = {} for k, v in (env or {}).items(): if k == name or k.startswith(ENV_DROP_PREFIX) or not isinstance(v, str): continue out[k] = v return out def import_node(alias, apps_filter=None): ip = nodes.ip_of(alias) rc, out, err = ssh.run(ip, "pm2 jlist 2>/dev/null; echo; echo __MEM__; pm2 jlist 2>/dev/null | python3 -c \"import json,sys; print(json.dumps({p['name']: p.get('monit',{}).get('memory',0) for p in json.load(sys.stdin)}))\"", timeout=60) raw, _, mem = out.partition("__MEM__") L = json.loads(raw.strip() or "[]") mems = json.loads(mem.strip() or "{}") groups = {} for p in L: e = p.get("pm2_env", {}) app = app_of(p["name"]) if apps_filter and app not in apps_filter: continue g = groups.setdefault(app, {"app": app, "processes": [], "ngrok": None, "cwd": None, "ram": 0}) g["ram"] += int(mems.get(p["name"], 0)) // (1024 * 1024) args = e.get("args") or [] script = e.get("pm_exec_path") or "" if "ngrok" in script or (args and "ngrok" in " ".join(map(str, args))): blob = " ".join([script] + [str(a) for a in args]) mo = re.search(r"--(?:url|domain)=(?:https?://)?([^\s]+)", blob) port = re.search(r"\s(\d{2,5})(?:\s|$)", blob) g["ngrok"] = {"name": p["name"], "url": mo.group(1) if mo else None, "port": int(port.group(1)) if port else None} continue g["processes"].append({ "name": p["name"], "manager": "pm2", "script": script, "args": args, "interpreter": e.get("exec_interpreter") if e.get("exec_interpreter") not in (None, "none", "N/A") else None, "cwd": e.get("pm_cwd"), "env": clean_env(e.get("env"), p["name"]), "cron_restart": e.get("cron_restart"), "autorestart": e.get("autorestart", True), "max_memory_restart": e.get("max_memory_restart"), }) g["cwd"] = g["cwd"] or e.get("pm_cwd") drafts = os.path.join(config.APPS_DIR, "_drafts") os.makedirs(drafts, exist_ok=True) written = [] for app, g in groups.items(): cwd = g["cwd"] or "" m = json.loads(json.dumps(manifest.DEFAULTS)) mo = re.match(r"(/Users/[^/]+/(?:apps/)?[^/]+|/opt/[^/]+)", cwd) d = mo.group(1) if mo else cwd # un tunnel ngrok encore vivant sur le nœud devient une route MacLustr Tunnel (le processus ngrok sera supprimé au 1er start) ng = g["ngrok"] or {} m.update({"app": app, "label": app, "dir": d.replace(config.home_of(alias), "~"), "processes": g["processes"], "ngrok": None, "tunnel": ({"domain": ng.get("url"), "gateway": config.TUNNEL_DEFAULT} if ng.get("url") else None), "legacy_pm2": [ng["name"]] if ng.get("name") else [], "domain": ng.get("url"), "port": ng.get("port"), "ram_mb_observed": max(g["ram"], 128)}) m["requires"]["ports"] = [m["port"]] if m["port"] else [] m["placement"]["reason"] = "importé de %s" % alias manifest.detect_requirements(m) p = os.path.join(drafts, app + ".json") json.dump(m, open(p, "w"), indent=2, ensure_ascii=False) written.append(p) return written