"""Install, hook, service, and container operations.""" import getpass import os import re import shutil import sqlite3 import subprocess import sys import importlib.util from tai_pkg.consts import Consts from tai_pkg.errors import BackendError from tai_pkg.sandbox import Sandbox from tai_pkg.telegram import Telegram from tai_pkg.telegram import TelegramBot from tai_pkg.text import Text __all__ = ["Install"] class Install: """Install.""" @staticmethod def run_host(argv, timeout=120): try: done = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=timeout) return done.returncode == 0, (done.stdout or "") + (done.stderr or "") except (OSError, subprocess.SubprocessError) as exc: return False, Text.short_error(exc) @staticmethod def venv_available(): if importlib.util.find_spec("venv") is None: return False if importlib.util.find_spec("ensurepip") is not None: return True return shutil.which("pip3") is not None or shutil.which("pip") is not None @staticmethod def venv_dir(home): return os.path.join(home, "venv") @staticmethod def venv_python(home): cand = os.path.join(Install.venv_dir(home), "bin", "python") if os.path.isfile(cand) and os.access(cand, os.X_OK): return cand return None @staticmethod def ensure_venv(home): found = Install.venv_python(home) if found: return True, "kept %s" % found if not Install.venv_available(): return False, "python venv module unavailable" try: done = subprocess.run([sys.executable, "-m", "venv", Install.venv_dir(home)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=300) except (OSError, subprocess.SubprocessError) as exc: return False, Text.short_error(exc) if done.returncode != 0 or not Install.venv_python(home): return False, ((done.stderr or "").strip()[:200] or "venv creation failed") return True, "created %s" % Install.venv_python(home) @staticmethod def service_exec(home, script, *args): parts = [] python = Install.venv_python(home) if python: parts.append(python) parts.append(script) parts.extend(args) return " ".join(parts) @staticmethod def unit_dir(home): return os.path.join(os.path.expanduser("~"), ".config", "systemd", "user") @staticmethod def service_state(name): if not shutil.which("systemctl"): return "unknown", "unknown" active, _out = Install.run_host(["systemctl", "--user", "is-active", name], timeout=15) enabled, _out = Install.run_host(["systemctl", "--user", "is-enabled", name], timeout=15) return ("active" if active else "inactive"), ("enabled" if enabled else "disabled") @staticmethod def activate_service(name): if not shutil.which("systemctl"): return "systemctl missing, unit written only" Install.run_host(["systemctl", "--user", "daemon-reload"]) active, _enabled = Install.service_state(name) if active == "active": ok, out = Install.run_host(["systemctl", "--user", "restart", name]) return "restarted, changes effective immediately" if ok else "restart failed: " + out.strip()[:150] ok, out = Install.run_host(["systemctl", "--user", "enable", "--now", name]) if ok: user = os.environ.get("USER") or getpass.getuser() Install.run_host(["loginctl", "enable-linger", user]) return "enabled and started" return "enable failed: " + out.strip()[:150] @staticmethod def remove_service_unit(home, name): path = os.path.join(Install.unit_dir(home), name) if shutil.which("systemctl"): Install.run_host(["systemctl", "--user", "disable", "--now", name]) Install.run_host(["systemctl", "--user", "daemon-reload"]) if os.path.exists(path): try: os.remove(path) return True, "removed " + path except OSError as exc: return False, Text.short_error(exc) return True, "not present" @staticmethod def script_version(path): try: with open(path, "r", encoding="utf-8") as handle: match = Consts.VERSION_RE.search(handle.read()) except OSError: return None return ".".join(match.groups()) if match else None @staticmethod def shell_rc_path(shell): """Where each shell's startup file lives. bash/zsh: a dotfile sourced for interactive shells only. fish: config.fish, which fish sources on every startup (interactive or not, login or not) -- hence the `status is-interactive` guard inside FISH_PLEXER_BLOCK rather than relying on the file only being read at the right time.""" home = os.path.expanduser("~") if shell == "fish": config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.join(home, ".config") return os.path.join(config_home, "fish", "config.fish") if shell == "zsh": return os.path.join(home, ".zshrc") return os.path.join(home, ".bashrc") @staticmethod def shell_present(shell): return shutil.which(shell) is not None @staticmethod def _rc_block_present(path, block): try: with open(path, "r", encoding="utf-8") as handle: return block in handle.read() except OSError: return False @staticmethod def _rc_block_remove(path, mark_begin, mark_end): try: with open(path, "r", encoding="utf-8") as handle: raw = handle.read() except OSError: return False stripped = re.sub(re.escape(mark_begin) + r".*?" + re.escape(mark_end) + r"\n?", "", raw, flags=re.DOTALL) if stripped == raw: return False backup = path + ".bak-tai" if raw and not os.path.exists(backup): with open(backup, "w", encoding="utf-8") as handle: handle.write(raw) with open(path, "w", encoding="utf-8") as handle: handle.write(stripped) return True @staticmethod def _rc_block_upsert(path, mark_begin, mark_end, block): try: with open(path, "r", encoding="utf-8") as handle: raw = handle.read() except OSError: raw = "" if block in raw: return False backup = path + ".bak-tai" if raw and not os.path.exists(backup): with open(backup, "w", encoding="utf-8") as handle: handle.write(raw) stripped = re.sub(re.escape(mark_begin) + r".*?" + re.escape(mark_end) + r"\n?", "", raw, flags=re.DOTALL) if stripped and not stripped.endswith("\n"): stripped += "\n" parent = os.path.dirname(path) if parent and not os.path.isdir(parent): os.makedirs(parent, exist_ok=True) with open(path, "w", encoding="utf-8") as handle: handle.write(stripped + block) return True @staticmethod def hook_present(path): return Install._rc_block_present(path, Consts.BASHRC_BLOCK) @staticmethod def remove_bashrc_block(path): return Install._rc_block_remove(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END) @staticmethod def zsh_hook_present(path): return Install._rc_block_present(path, Consts.ZSH_NOTFOUND_BLOCK) @staticmethod def remove_zsh_hook_block(path): return Install._rc_block_remove(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END) @staticmethod def upsert_zsh_hook_block(path): return Install._rc_block_upsert(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END, Consts.ZSH_NOTFOUND_BLOCK) @staticmethod def fish_hook_present(path): return Install._rc_block_present(path, Consts.FISH_NOTFOUND_BLOCK) @staticmethod def remove_fish_hook_block(path): return Install._rc_block_remove(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END) @staticmethod def upsert_fish_hook_block(path): return Install._rc_block_upsert(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END, Consts.FISH_NOTFOUND_BLOCK) @staticmethod def plexer_hook_present(path): return Install._rc_block_present(path, Consts.PLEXER_BASHRC_BLOCK) @staticmethod def remove_plexer_bashrc_block(path): return Install._rc_block_remove(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END) @staticmethod def upsert_plexer_bashrc_block(path): return Install._rc_block_upsert(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END, Consts.PLEXER_BASHRC_BLOCK) @staticmethod # zsh reuses PLEXER_BASHRC_BLOCK byte-for-byte -- zsh is POSIX-enough that # the plain `[`/`case "$-"` bash content works unchanged; only the target # file differs (~/.zshrc, not ~/.bashrc). No ZSH_PLEXER_BLOCK constant exists # on purpose -- there is nothing shell-specific to put in one. def zsh_plexer_hook_present(path): return Install._rc_block_present(path, Consts.PLEXER_BASHRC_BLOCK) @staticmethod def remove_zsh_plexer_block(path): return Install._rc_block_remove(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END) @staticmethod def upsert_zsh_plexer_block(path): return Install._rc_block_upsert(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END, Consts.PLEXER_BASHRC_BLOCK) @staticmethod def fish_plexer_hook_present(path): return Install._rc_block_present(path, Consts.FISH_PLEXER_BLOCK) @staticmethod def remove_fish_plexer_block(path): return Install._rc_block_remove(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END) @staticmethod def upsert_fish_plexer_block(path): return Install._rc_block_upsert(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END, Consts.FISH_PLEXER_BLOCK) @staticmethod def box_venv_state(engine): try: probe = Sandbox.box_exec(engine, ["test", "-x", Consts.BOX_PYTHON], timeout=30) except (OSError, subprocess.SubprocessError): return "unknown" return "venv ready" if probe.returncode == 0 else "no venv (reinstall container to rebuild)" @staticmethod def shell_hook_status_line(label, shell, present_fn): path = Install.shell_rc_path(shell) note = "" if Install.shell_present(shell) else " (%s not found)" % shell return "%s: %s in %s%s" % (label, "installed" if present_fn(path) else "not installed", path, note) @staticmethod def status_lines(home, store=None): lines = [] binary = os.path.join(os.path.expanduser("~"), ".local", "bin", "tai.py") if os.path.isfile(binary): consts_path = os.path.join(os.path.dirname(binary), "tai_pkg", "consts.py") lines.append("binary: installed %s (%s, running %s)" % (binary, Install.script_version(consts_path) or "unknown", Consts.VERSION)) else: lines.append("binary: not installed") lines.append("bash-hook: %s in ~/.bashrc" % ("installed" if Install.hook_present(os.path.join(os.path.expanduser("~"), ".bashrc")) else "not installed")) lines.append(Install.shell_hook_status_line("zsh-hook", "zsh", Install.zsh_hook_present)) lines.append(Install.shell_hook_status_line("fish-hook", "fish", Install.fish_hook_present)) lines.append("plexer-hook: %s in ~/.bashrc" % ("installed" if Install.plexer_hook_present(os.path.join(os.path.expanduser("~"), ".bashrc")) else "not installed")) lines.append(Install.shell_hook_status_line("zsh-plexer-hook", "zsh", Install.zsh_plexer_hook_present)) lines.append(Install.shell_hook_status_line("fish-plexer-hook", "fish", Install.fish_plexer_hook_present)) python = Install.venv_python(home) if python: lines.append("venv: ready %s" % python) elif Install.venv_available(): lines.append("venv: not created") else: lines.append("venv: unavailable (no venv module)") for name, label in (("tai-scheduler.service", "scheduler-service"), ("tai-telegram.service", "telegram-service")): unit = os.path.join(Install.unit_dir(home), name) if not os.path.isfile(unit): lines.append("%s: not installed" % label) continue active, enabled = Install.service_state(name) lines.append("%s: unit present, %s, %s" % (label, active, enabled)) engine = Sandbox.container_engine() if not engine: lines.append("container: no engine (install podman)") else: state = Sandbox.box_state(engine) if state == "running": lines.append("container: running, %s" % Install.box_venv_state(engine)) else: lines.append("container: %s" % state) lines.append(Install.vault_line(home, store)) return lines @staticmethod def vault_line(home, store=None): db_path = os.path.join(home, "memory.db") if not os.path.isfile(db_path): return "vault: empty, install ops never touch data" if store is None: return "vault: kept %s, install ops never touch data" % db_path try: secrets = store.db.execute("SELECT COUNT(*) FROM secrets").fetchone()[0] schedules = store.db.execute("SELECT COUNT(*) FROM schedules").fetchone()[0] records = store.db.execute("SELECT COUNT(*) FROM records").fetchone()[0] except sqlite3.Error: return "vault: kept %s, install ops never touch data" % db_path return "vault: kept (%d secrets, %d schedules, %d records), install ops never touch data" % (secrets, schedules, records) @staticmethod def op_binary(mode, home): target = os.path.join(os.path.expanduser("~"), ".local", "bin", "tai.py") pkg_target = os.path.join(os.path.dirname(target), "tai_pkg") consts_target = os.path.join(pkg_target, "consts.py") src = os.path.abspath(Consts.TAI_SCRIPT) src_pkg = os.path.dirname(os.path.abspath(Consts.TAI_CONSTS)) current = Install.script_version(consts_target) if os.path.isfile(consts_target) else None if mode == "install" and current is not None: return "binary: already installed %s (running %s)" % (current, Consts.VERSION) if mode == "uninstall": if not os.path.exists(target) and not os.path.exists(pkg_target): return "binary: not present" try: if os.path.exists(target): os.remove(target) if os.path.isdir(pkg_target): shutil.rmtree(pkg_target) return "binary: removed %s" % target except OSError as exc: return "binary: remove failed: %s" % Text.short_error(exc) try: os.makedirs(os.path.dirname(target), exist_ok=True) with open(src, "rb") as handle: blob = handle.read() with open(target, "wb") as handle: handle.write(blob) os.chmod(target, 0o755) if os.path.abspath(src_pkg) != os.path.abspath(pkg_target): if os.path.isdir(pkg_target): shutil.rmtree(pkg_target) shutil.copytree(src_pkg, pkg_target, ignore=shutil.ignore_patterns("__pycache__")) except OSError as exc: return "binary: copy failed: %s" % Text.short_error(exc) if current is None: return "binary: installed %s" % Consts.VERSION return "binary: refreshed %s -> %s" % (current, Consts.VERSION) @staticmethod def op_hook(mode, home): path = os.path.join(os.path.expanduser("~"), ".bashrc") if mode == "uninstall": return "bash-hook: %s" % ("removed" if Install.remove_bashrc_block(path) else "not present") if mode == "reinstall": Install.remove_bashrc_block(path) try: changed = Install.upsert_bashrc_block(path) except OSError as exc: return "bash-hook: write failed: %s" % Text.short_error(exc) if changed: return "bash-hook: installed, run: source ~/.bashrc" return "bash-hook: already present" @staticmethod def op_plexer_hook(mode, home): path = os.path.join(os.path.expanduser("~"), ".bashrc") if mode == "uninstall": return "plexer-hook: %s" % ("removed" if Install.remove_plexer_bashrc_block(path) else "not present") if mode == "reinstall": Install.remove_plexer_bashrc_block(path) try: changed = Install.upsert_plexer_bashrc_block(path) except OSError as exc: return "plexer-hook: write failed: %s" % Text.short_error(exc) if changed: return "plexer-hook: installed, run: source ~/.bashrc (new interactive shells auto-wrap; existing ones are unaffected until restarted)" return "plexer-hook: already present" @staticmethod def op_shell_hook(mode, shell, label, upsert_fn, remove_fn, source_hint): """Shared body for the zsh/fish hook variants -- mirrors op_hook/op_plexer_hook exactly, plus one addition: install/upgrade skip loudly (never write a dangling hook into a config file for a shell that isn't even on this machine) when the shell binary isn't found. uninstall/reinstall/status still act on the file regardless, so a hook installed before a shell was later removed still cleans up.""" path = Install.shell_rc_path(shell) if mode in ("install", "upgrade") and not Install.shell_present(shell): return "%s: %s not found, skipped" % (label, shell) if mode == "uninstall": return "%s: %s" % (label, "removed" if remove_fn(path) else "not present") if mode == "reinstall": remove_fn(path) try: changed = upsert_fn(path) except OSError as exc: return "%s: write failed: %s" % (label, Text.short_error(exc)) if changed: return "%s: installed, run: %s" % (label, source_hint) return "%s: already present" % label @staticmethod def op_zsh_hook(mode, home): return Install.op_shell_hook(mode, "zsh", "zsh-hook", Install.upsert_zsh_hook_block, Install.remove_zsh_hook_block, "source ~/.zshrc") @staticmethod def op_fish_hook(mode, home): return Install.op_shell_hook(mode, "fish", "fish-hook", Install.upsert_fish_hook_block, Install.remove_fish_hook_block, "open a new fish shell") @staticmethod def op_zsh_plexer_hook(mode, home): return Install.op_shell_hook( mode, "zsh", "zsh-plexer-hook", Install.upsert_zsh_plexer_block, Install.remove_zsh_plexer_block, "source ~/.zshrc (new interactive shells auto-wrap; existing ones are unaffected until restarted)", ) @staticmethod def op_fish_plexer_hook(mode, home): return Install.op_shell_hook( mode, "fish", "fish-plexer-hook", Install.upsert_fish_plexer_block, Install.remove_fish_plexer_block, "open a new fish shell (new interactive shells auto-wrap; existing ones are unaffected until restarted)", ) @staticmethod def op_venv(mode, home): if mode == "uninstall": folder = Install.venv_dir(home) if not os.path.isdir(folder): return "venv: not present" try: shutil.rmtree(folder) return "venv: removed %s" % folder except OSError as exc: return "venv: remove failed: %s" % Text.short_error(exc) if mode == "reinstall" and os.path.isdir(Install.venv_dir(home)): try: shutil.rmtree(Install.venv_dir(home)) except OSError as exc: return "venv: rebuild failed: %s" % Text.short_error(exc) ok, detail = Install.ensure_venv(home) return "venv: %s" % detail @staticmethod def op_service(mode, home, name, label, body): unit = os.path.join(Install.unit_dir(home), name) if mode == "uninstall": _done, detail = Install.remove_service_unit(home, name) return "%s: %s" % (label, detail) existed = os.path.isfile(unit) if mode == "install" and existed: active, enabled = Install.service_state(name) return "%s: already installed (%s, %s)" % (label, active, enabled) if mode == "reinstall" and shutil.which("systemctl"): Install.run_host(["systemctl", "--user", "disable", "--now", name]) try: os.makedirs(os.path.dirname(unit), exist_ok=True) with open(unit, "w", encoding="utf-8") as handle: handle.write(body) except OSError as exc: return "%s: unit write failed: %s" % (label, Text.short_error(exc)) return "%s: unit %s, %s" % (label, "rewritten" if existed else "written", Install.activate_service(name)) @staticmethod def op_scheduler_service(mode, home): body = Consts.SCHEDULER_UNIT % Install.service_exec(home, os.path.abspath(Consts.TAI_SCRIPT)) return Install.op_service(mode, home, "tai-scheduler.service", "scheduler-service", body) @staticmethod def op_telegram_service(mode, home): if mode == "uninstall": _done, detail = Install.remove_service_unit(home, "tai-telegram.service") return "telegram-service: %s" % detail unit = os.path.join(Install.unit_dir(home), "tai-telegram.service") if mode == "install" and os.path.isfile(unit): active, enabled = Install.service_state("tai-telegram.service") return "telegram-service: already installed (%s, %s)" % (active, enabled) token = os.environ.get("TELEGRAM_BOT_TOKEN") or TelegramBot.load_telegram_token(home) if not token: return "telegram-service: needs TELEGRAM_BOT_TOKEN in the environment" try: me = Telegram(token).call("getMe", {}) except (OSError, ValueError) as exc: return "telegram-service: token rejected: %s" % Text.short_error(exc) env_path = TelegramBot.telegram_env_path(home) try: with open(env_path, "w", encoding="utf-8") as handle: handle.write("TELEGRAM_BOT_TOKEN=%s\n" % token) os.chmod(env_path, 0o600) except OSError as exc: return "telegram-service: env write failed: %s" % Text.short_error(exc) body = Consts.TELEGRAM_UNIT % (Install.service_exec(home, os.path.abspath(Consts.TAI_SCRIPT)), env_path) if mode == "reinstall" and shutil.which("systemctl"): Install.run_host(["systemctl", "--user", "disable", "--now", "tai-telegram.service"]) try: os.makedirs(os.path.dirname(unit), exist_ok=True) with open(unit, "w", encoding="utf-8") as handle: handle.write(body) except OSError as exc: return "telegram-service: unit write failed: %s" % Text.short_error(exc) who = (me.get("result") or {}).get("username", "?") return "telegram-service: @%s, %s" % (who, Install.activate_service("tai-telegram.service")) @staticmethod def op_container(mode, _home): engine = Sandbox.container_engine() if not engine: return "container: no engine (install podman)" if mode == "uninstall": Install.run_host([engine, "rm", "-f", Consts.BOX_NAME]) ok, _out = Install.run_host([engine, "rmi", "-f", Consts.BOX_IMAGE]) return "container: removed%s" % (", image purged" if ok else "") if mode == "reinstall": Install.run_host([engine, "rm", "-f", Consts.BOX_NAME]) Install.run_host([engine, "rmi", "-f", Consts.BOX_IMAGE]) try: engine = Sandbox.ensure_box() except BackendError as exc: return "container: failed: %s" % exc state = Sandbox.box_state(engine) detail = ", %s" % Install.box_venv_state(engine) if state == "running" else "" if mode == "reinstall": return "container: rebuilt and %s%s" % (state, detail) if mode == "install" and state == "running": return "container: already running%s" % detail return "container: %s%s" % (state, detail) @staticmethod def install_report(action, targets, home, store=None): if action == "status": return "\n".join(Install.status_lines(home, store)) lines = [] for target in targets: if target == "binary": lines.append(Install.op_binary(action, home)) elif target == "bash-hook": lines.append(Install.op_hook(action, home)) elif target == "zsh-hook": lines.append(Install.op_zsh_hook(action, home)) elif target == "fish-hook": lines.append(Install.op_fish_hook(action, home)) elif target == "plexer-hook": lines.append(Install.op_plexer_hook(action, home)) elif target == "zsh-plexer-hook": lines.append(Install.op_zsh_plexer_hook(action, home)) elif target == "fish-plexer-hook": lines.append(Install.op_fish_plexer_hook(action, home)) elif target == "venv": lines.append(Install.op_venv(action, home)) elif target == "scheduler-service": lines.append(Install.op_scheduler_service(action, home)) elif target == "telegram-service": lines.append(Install.op_telegram_service(action, home)) elif target == "container": lines.append(Install.op_container(action, home)) lines.append(Install.vault_line(home, store)) return "\n".join(lines) @staticmethod def install_telegram(): token = os.environ.get("TELEGRAM_BOT_TOKEN") or "" if token: print("using TELEGRAM_BOT_TOKEN from environment") else: try: token = getpass.getpass("Telegram bot token: ").strip() except (EOFError, KeyboardInterrupt): print("\naborted") return 1 if not token: print("empty token, aborted") return 1 try: me = Telegram(token).call("getMe", {}) except (OSError, ValueError) as exc: print("token rejected: %s" % Text.short_error(exc)) return 1 print("token ok for @%s" % (me.get("result") or {}).get("username", "?")) home = os.environ.get("TAI_HOME") or os.path.join(os.path.expanduser("~"), ".tai") os.makedirs(home, exist_ok=True) env_path = TelegramBot.telegram_env_path(home) with open(env_path, "w", encoding="utf-8") as handle: handle.write("TELEGRAM_BOT_TOKEN=%s\n" % token) try: os.chmod(env_path, 0o600) except OSError: pass print("wrote %s" % env_path) try: Sandbox.ensure_box() except BackendError as exc: print("container failed: %s" % exc) return 1 unit_folder = os.path.join(os.path.expanduser("~"), ".config", "systemd", "user") os.makedirs(unit_folder, exist_ok=True) unit_path = os.path.join(unit_folder, "tai-telegram.service") with open(unit_path, "w", encoding="utf-8") as handle: handle.write(Consts.TELEGRAM_UNIT % (Install.service_exec(home, os.path.abspath(Consts.TAI_SCRIPT)), env_path)) print("wrote %s" % unit_path) print("service: %s" % Install.activate_service("tai-telegram.service")) return 0 @staticmethod def uninstall_telegram(): unit_path = os.path.join(os.path.expanduser("~"), ".config", "systemd", "user", "tai-telegram.service") if shutil.which("systemctl"): Install.run_host(["systemctl", "--user", "disable", "--now", "tai-telegram.service"]) if os.path.exists(unit_path): try: os.remove(unit_path) print("removed %s" % unit_path) except OSError as exc: print("unit remove failed: %s" % Text.short_error(exc)) else: print("no service unit found") if shutil.which("systemctl"): Install.run_host(["systemctl", "--user", "daemon-reload"]) engine = Sandbox.container_engine() if not engine: print("no container engine found") return 0 ok, _out = Install.run_host([engine, "rm", "-f", Consts.BOX_NAME]) print("container remove: %s" % ("ok" if ok else "not present")) ok, _out = Install.run_host([engine, "rmi", "-f", Consts.BOX_IMAGE]) print("image purge: %s" % ("ok" if ok else "not present")) return 0 @staticmethod def upsert_bashrc_block(path): return Install._rc_block_upsert(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END, Consts.BASHRC_BLOCK) @staticmethod def install_self(): home = os.environ.get("TAI_HOME") or os.path.join(os.path.expanduser("~"), ".tai") print(Install.op_binary("upgrade", home)) print(Install.op_hook("install", home)) # bash-hook always goes in, unconditionally, for back-compat -- it's harmless clutter # in ~/.bashrc for a zsh/fish user who never touches bash. $SHELL additionally gets its # own matching hook so the one-shot CLI bootstrap (--install) actually works out of the # box on a non-bash login shell, not just when the user later runs /install zsh-hook. login_shell = os.path.basename(os.environ.get("SHELL") or "") rc = "~/.bashrc" if login_shell == "zsh": print(Install.op_zsh_hook("install", home)) rc = "~/.zshrc" elif login_shell == "fish": print(Install.op_fish_hook("install", home)) rc = "~/.config/fish/config.fish" print(Install.op_venv("install", home)) print("restart your shell or run: source %s" % rc) return 0