diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f224e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.pyc +*.pyo +dist/ +build/ +*.spec +*.egg-info/ +.pytest_cache/ diff --git a/Makefile b/Makefile index 6e29c4d..0af2ce6 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PYTHON ?= python3 -PROJECT_ROOT := $(abspath ..) -ENTRYPOINT := __main__.py +SRC_DIR := $(CURDIR)/src +ENTRYPOINT := src/flow/__main__.py DIST_DIR := dist BUILD_DIR := build SPEC_FILE := flow.spec @@ -17,7 +17,7 @@ help: @printf " make clean Remove build artifacts\n" build: - $(PYTHON) -m PyInstaller --noconfirm --clean --onefile --name flow --paths "$(PROJECT_ROOT)" "$(ENTRYPOINT)" + $(PYTHON) -m PyInstaller --noconfirm --clean --onefile --name flow --paths "$(SRC_DIR)" "$(ENTRYPOINT)" install-local: build mkdir -p "$(INSTALL_DIR)" diff --git a/README.md b/README.md index 7d98ffd..84df593 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,15 @@ # flow -`flow` is a CLI for managing development instances, containers, dotfiles, -bootstrap profiles, and binary packages. +`flow` is a CLI for managing development instances, containers, dotfiles, bootstrap profiles, and +binary packages. -This repository contains the Python implementation of the tool and its command -modules. +This repository contains the Python implementation of the tool and its command modules. ## What is implemented - Instance access via `flow enter` -- Container lifecycle commands under `flow dev` (`create`, `exec`, `connect`, - `list`, `stop`, `remove`, `respawn`) +- Container lifecycle commands under `flow dev` (`create`, `exec`, `connect`, `list`, `stop`, + `remove`, `respawn`) - Dotfiles management (`dotfiles` / `dot`) - Bootstrap planning and execution (`bootstrap` / `setup` / `provision`) - Binary package installation from manifest definitions (`package` / `pkg`) @@ -72,35 +71,35 @@ Example: ```yaml profiles: - linux-vm: - os: linux - hostname: "$HOSTNAME" - shell: zsh - locale: en_US.UTF-8 - requires: [HOSTNAME] - packages: - standard: [git, tmux, zsh] - binary: [neovim] - ssh_keygen: - - type: ed25519 - comment: "$USER@$HOSTNAME" - runcmd: - - mkdir -p ~/projects + linux-vm: + os: linux + hostname: "$HOSTNAME" + shell: zsh + locale: en_US.UTF-8 + requires: [HOSTNAME] + packages: + standard: [git, tmux, zsh] + binary: [neovim] + ssh_keygen: + - type: ed25519 + comment: "$USER@$HOSTNAME" + runcmd: + - mkdir -p ~/projects binaries: - neovim: - source: github:neovim/neovim - version: "0.10.4" - asset-pattern: "nvim-{{os}}-{{arch}}.tar.gz" - platform-map: - linux-amd64: { os: linux, arch: x86_64 } - linux-arm64: { os: linux, arch: arm64 } - macos-arm64: { os: macos, arch: arm64 } - install-script: | - curl -fL "{{downloadUrl}}" -o /tmp/nvim.tar.gz - tar -xzf /tmp/nvim.tar.gz -C /tmp - rm -rf ~/.local/bin/nvim - cp /tmp/nvim-*/bin/nvim ~/.local/bin/nvim + neovim: + source: github:neovim/neovim + version: "0.10.4" + asset-pattern: "nvim-{{os}}-{{arch}}.tar.gz" + platform-map: + linux-amd64: { os: linux, arch: x86_64 } + linux-arm64: { os: linux, arch: arm64 } + macos-arm64: { os: macos, arch: arm64 } + install-script: | + curl -fL "{{downloadUrl}}" -o /tmp/nvim.tar.gz + tar -xzf /tmp/nvim.tar.gz -C /tmp + rm -rf ~/.local/bin/nvim + cp /tmp/nvim-*/bin/nvim ~/.local/bin/nvim ``` ## Command overview @@ -113,6 +112,9 @@ flow enter root@personal@orb flow enter personal@orb --dry-run ``` +If your local terminal uses `xterm-ghostty` or `wezterm`, `flow enter` shows a terminfo warning and +a manual fix command before connecting. `flow` never installs terminfo on the target automatically. + ### Containers ```bash @@ -184,14 +186,13 @@ Passing an explicit file path to internal loaders bypasses this cascade. ## State format policy -`flow` currently supports only the v2 dotfiles link state format -(`linked.json`). Older state formats are intentionally not supported. +`flow` currently supports only the v2 dotfiles link state format (`linked.json`). Older state +formats are intentionally not supported. ## CLI behavior - User errors return non-zero exit codes. -- External command failures are surfaced as concise one-line errors (no - traceback spam). +- External command failures are surfaced as concise one-line errors (no traceback spam). - `Ctrl+C` exits with code `130`. ## Zsh completion @@ -216,9 +217,8 @@ fpath=(~/.zsh/completions $fpath) autoload -Uz compinit && compinit ``` -Completion is dynamic and pulls values from your current config/manifest/state -(for example bootstrap profiles, package names, dotfiles packages, and -configured `enter` targets). +Completion is dynamic and pulls values from your current config/manifest/state (for example +bootstrap profiles, package names, dotfiles packages, and configured `enter` targets). ## Development @@ -236,22 +236,16 @@ Useful targets: make clean ``` -Run a syntax check: - -```bash -python3 -m compileall . -``` - -Run tests (when `pytest` is available): +Run tests: ```bash python3 -m pytest ``` -Optional local venv setup: +Local development setup: ```bash python3 -m venv .venv -.venv/bin/pip install -U pip pytest pyyaml -PYTHONPATH=/path/to/src .venv/bin/pytest +.venv/bin/pip install -e ".[dev]" +.venv/bin/pytest ``` diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 6337090..0000000 Binary files a/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/__pycache__/__main__.cpython-313.pyc b/__pycache__/__main__.cpython-313.pyc deleted file mode 100644 index 0a461d6..0000000 Binary files a/__pycache__/__main__.cpython-313.pyc and /dev/null differ diff --git a/__pycache__/cli.cpython-313.pyc b/__pycache__/cli.cpython-313.pyc deleted file mode 100644 index deede71..0000000 Binary files a/__pycache__/cli.cpython-313.pyc and /dev/null differ diff --git a/commands/__pycache__/__init__.cpython-313.pyc b/commands/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 9844ac0..0000000 Binary files a/commands/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/commands/__pycache__/bootstrap.cpython-313.pyc b/commands/__pycache__/bootstrap.cpython-313.pyc deleted file mode 100644 index e3fbecd..0000000 Binary files a/commands/__pycache__/bootstrap.cpython-313.pyc and /dev/null differ diff --git a/commands/__pycache__/completion.cpython-313.pyc b/commands/__pycache__/completion.cpython-313.pyc deleted file mode 100644 index 119b844..0000000 Binary files a/commands/__pycache__/completion.cpython-313.pyc and /dev/null differ diff --git a/commands/__pycache__/container.cpython-313.pyc b/commands/__pycache__/container.cpython-313.pyc deleted file mode 100644 index 1c6eeef..0000000 Binary files a/commands/__pycache__/container.cpython-313.pyc and /dev/null differ diff --git a/commands/__pycache__/dotfiles.cpython-313.pyc b/commands/__pycache__/dotfiles.cpython-313.pyc deleted file mode 100644 index aab5461..0000000 Binary files a/commands/__pycache__/dotfiles.cpython-313.pyc and /dev/null differ diff --git a/commands/__pycache__/enter.cpython-313.pyc b/commands/__pycache__/enter.cpython-313.pyc deleted file mode 100644 index 8aa927d..0000000 Binary files a/commands/__pycache__/enter.cpython-313.pyc and /dev/null differ diff --git a/commands/__pycache__/package.cpython-313.pyc b/commands/__pycache__/package.cpython-313.pyc deleted file mode 100644 index e9d41b8..0000000 Binary files a/commands/__pycache__/package.cpython-313.pyc and /dev/null differ diff --git a/commands/__pycache__/sync.cpython-313.pyc b/commands/__pycache__/sync.cpython-313.pyc deleted file mode 100644 index 7aa1424..0000000 Binary files a/commands/__pycache__/sync.cpython-313.pyc and /dev/null differ diff --git a/core/__pycache__/__init__.cpython-313.pyc b/core/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index a79bc8f..0000000 Binary files a/core/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/core/__pycache__/action.cpython-313.pyc b/core/__pycache__/action.cpython-313.pyc deleted file mode 100644 index 66dd5fe..0000000 Binary files a/core/__pycache__/action.cpython-313.pyc and /dev/null differ diff --git a/core/__pycache__/config.cpython-313.pyc b/core/__pycache__/config.cpython-313.pyc deleted file mode 100644 index cfcc83c..0000000 Binary files a/core/__pycache__/config.cpython-313.pyc and /dev/null differ diff --git a/core/__pycache__/console.cpython-313.pyc b/core/__pycache__/console.cpython-313.pyc deleted file mode 100644 index 24ef17f..0000000 Binary files a/core/__pycache__/console.cpython-313.pyc and /dev/null differ diff --git a/core/__pycache__/paths.cpython-313.pyc b/core/__pycache__/paths.cpython-313.pyc deleted file mode 100644 index 8cb964b..0000000 Binary files a/core/__pycache__/paths.cpython-313.pyc and /dev/null differ diff --git a/core/__pycache__/platform.cpython-313.pyc b/core/__pycache__/platform.cpython-313.pyc deleted file mode 100644 index 1c954a1..0000000 Binary files a/core/__pycache__/platform.cpython-313.pyc and /dev/null differ diff --git a/core/__pycache__/process.cpython-313.pyc b/core/__pycache__/process.cpython-313.pyc deleted file mode 100644 index 97265a5..0000000 Binary files a/core/__pycache__/process.cpython-313.pyc and /dev/null differ diff --git a/core/__pycache__/stow.cpython-313.pyc b/core/__pycache__/stow.cpython-313.pyc deleted file mode 100644 index e768eba..0000000 Binary files a/core/__pycache__/stow.cpython-313.pyc and /dev/null differ diff --git a/core/__pycache__/variables.cpython-313.pyc b/core/__pycache__/variables.cpython-313.pyc deleted file mode 100644 index 0f08159..0000000 Binary files a/core/__pycache__/variables.cpython-313.pyc and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index 60f70c3..25bcd66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,16 +4,23 @@ build-backend = "hatchling.build" [project] name = "flow" -version = "0.1.0" +dynamic = ["version"] description = "DevFlow - A unified toolkit for managing development instances, containers, and profiles" requires-python = ">=3.9" dependencies = ["pyyaml>=6.0"] [project.optional-dependencies] build = ["pyinstaller>=6.0"] +dev = ["pytest>=7.0"] [project.scripts] flow = "flow.cli:main" +[tool.hatch.version] +path = "src/flow/__init__.py" + [tool.hatch.build.targets.wheel] packages = ["src/flow"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/__init__.py b/src/flow/__init__.py similarity index 100% rename from __init__.py rename to src/flow/__init__.py diff --git a/__main__.py b/src/flow/__main__.py similarity index 100% rename from __main__.py rename to src/flow/__main__.py diff --git a/cli.py b/src/flow/cli.py similarity index 100% rename from cli.py rename to src/flow/cli.py diff --git a/commands/__init__.py b/src/flow/commands/__init__.py similarity index 100% rename from commands/__init__.py rename to src/flow/commands/__init__.py diff --git a/commands/bootstrap.py b/src/flow/commands/bootstrap.py similarity index 91% rename from commands/bootstrap.py rename to src/flow/commands/bootstrap.py index c6c40e3..0deb527 100644 --- a/commands/bootstrap.py +++ b/src/flow/commands/bootstrap.py @@ -1,8 +1,9 @@ """flow bootstrap — environment provisioning with plan-then-execute model.""" +import argparse import os +import shlex import shutil -import subprocess import sys from pathlib import Path from typing import Any, Dict, List @@ -209,16 +210,16 @@ def _register_handlers(executor: ActionExecutor, ctx: FlowContext, variables: di raise RuntimeError(f"Required variable not set: {var}") def handle_set_hostname(data): - hostname = data["hostname"] + hostname = shlex.quote(data["hostname"]) if ctx.platform.os == "macos": - run_command(f"sudo scutil --set ComputerName '{hostname}'", ctx.console) - run_command(f"sudo scutil --set HostName '{hostname}'", ctx.console) - run_command(f"sudo scutil --set LocalHostName '{hostname}'", ctx.console) + run_command(f"sudo scutil --set ComputerName {hostname}", ctx.console) + run_command(f"sudo scutil --set HostName {hostname}", ctx.console) + run_command(f"sudo scutil --set LocalHostName {hostname}", ctx.console) else: - run_command(f"sudo hostnamectl set-hostname '{hostname}'", ctx.console) + run_command(f"sudo hostnamectl set-hostname {hostname}", ctx.console) def handle_set_locale(data): - locale = data["locale"] + locale = shlex.quote(data["locale"]) run_command(f"sudo locale-gen {locale}", ctx.console) run_command(f"sudo update-locale LANG={locale}", ctx.console) @@ -227,13 +228,14 @@ def _register_handlers(executor: ActionExecutor, ctx: FlowContext, variables: di shell_path = shutil.which(shell) if not shell_path: raise RuntimeError(f"Shell not found: {shell}") + quoted_path = shlex.quote(shell_path) try: with open("/etc/shells") as f: if shell_path not in f.read(): - run_command(f"echo '{shell_path}' | sudo tee -a /etc/shells", ctx.console) + run_command(f"echo {quoted_path} | sudo tee -a /etc/shells", ctx.console) except FileNotFoundError: pass - run_command(f"chsh -s {shell_path}", ctx.console) + run_command(f"chsh -s {quoted_path}", ctx.console) def handle_pm_update(data): pm = data["pm"] @@ -249,7 +251,7 @@ def _register_handlers(executor: ActionExecutor, ctx: FlowContext, variables: di pm = data["pm"] packages = data["packages"] pkg_type = data.get("type", "standard") - pkg_str = " ".join(packages) + pkg_str = " ".join(shlex.quote(p) for p in packages) if pm in ("apt-get", "apt"): cmd = f"sudo {pm} install -y {pkg_str}" @@ -301,7 +303,11 @@ def _register_handlers(executor: ActionExecutor, ctx: FlowContext, variables: di if key_path.exists(): ctx.console.warn(f"SSH key already exists: {key_path}") return - run_command(f'ssh-keygen -t {key_type} -f "{key_path}" -N "" -C "{comment}"', ctx.console) + run_command( + f"ssh-keygen -t {shlex.quote(key_type)} -f {shlex.quote(str(key_path))}" + f' -N "" -C {shlex.quote(comment)}', + ctx.console, + ) def handle_link_config(data): config_name = data["config_name"] @@ -328,18 +334,18 @@ def run_bootstrap(ctx: FlowContext, args): flow_pkg = DOTFILES_DIR / "common" / "flow" if flow_pkg.exists() and (flow_pkg / ".config" / "flow").exists(): ctx.console.info("Found flow config in dotfiles, linking...") - # Link flow package first - result = subprocess.run( - [sys.executable, "-m", "flow", "dotfiles", "link", "flow"], - capture_output=True, text=True, + # Call the link function directly instead of spawning a subprocess + from flow.commands.dotfiles import run_link + link_args = argparse.Namespace( + packages=["flow"], profile=None, copy=False, force=False, dry_run=False, ) - if result.returncode == 0: + try: + run_link(ctx, link_args) ctx.console.success("Flow config linked from dotfiles") # Reload manifest from newly linked location ctx.manifest = load_manifest() - else: - detail = (result.stderr or "").strip() or (result.stdout or "").strip() or "unknown error" - ctx.console.warn(f"Failed to link flow config: {detail}") + except (RuntimeError, SystemExit) as e: + ctx.console.warn(f"Failed to link flow config: {e}") profiles = _get_profiles(ctx) if not profiles: diff --git a/commands/completion.py b/src/flow/commands/completion.py similarity index 100% rename from commands/completion.py rename to src/flow/commands/completion.py diff --git a/commands/container.py b/src/flow/commands/container.py similarity index 96% rename from commands/container.py rename to src/flow/commands/container.py index a97efc5..8eaeb94 100644 --- a/commands/container.py +++ b/src/flow/commands/container.py @@ -195,18 +195,18 @@ def run_exec(ctx: FlowContext, args): result = subprocess.run(exec_cmd) sys.exit(result.returncode) - # No command — try shells in order - last_code = 0 + # No command — try shells in order; 126/127 means the shell binary + # wasn't found, so we fall through. Any other exit code means the user + # exited the shell normally and we respect it. for shell in ("zsh -l", "bash -l", "sh"): parts = shell.split() exec_cmd = [rt, "exec", "--detach-keys", "ctrl-q,ctrl-p", "-it", cname] + parts result = subprocess.run(exec_cmd) - if result.returncode == 0: - return - last_code = result.returncode + if result.returncode not in (126, 127): + sys.exit(result.returncode) ctx.console.error(f"Unable to start an interactive shell in {cname}") - sys.exit(last_code or 1) + sys.exit(1) def run_connect(ctx: FlowContext, args): @@ -323,6 +323,9 @@ def run_remove(ctx: FlowContext, args): def run_respawn(ctx: FlowContext, args): + if not shutil.which("tmux"): + ctx.console.error("tmux is required for respawn but was not found") + sys.exit(1) cname = _cname(args.name) result = subprocess.run( ["tmux", "list-panes", "-t", cname, "-s", diff --git a/commands/dotfiles.py b/src/flow/commands/dotfiles.py similarity index 97% rename from commands/dotfiles.py rename to src/flow/commands/dotfiles.py index 449d42d..55958d7 100644 --- a/commands/dotfiles.py +++ b/src/flow/commands/dotfiles.py @@ -324,7 +324,11 @@ def run_relink(ctx: FlowContext, args): ctx.console.info("Unlinking current symlinks...") run_unlink(ctx, args) - # Then link again + # Then link again — set defaults for attributes that run_link expects + # but the relink parser doesn't define. + args.copy = False + args.force = False + args.dry_run = False ctx.console.info("Relinking with updated configuration...") run_link(ctx, args) @@ -413,7 +417,11 @@ def run_edit(ctx: FlowContext, args): ) # Ask before pushing - response = input("Push changes to remote? [Y/n] ") + try: + response = input("Push changes to remote? [Y/n] ") + except (EOFError, KeyboardInterrupt): + response = "n" + print() # newline after ^C / EOF if response.lower() != "n": subprocess.run(["git", "-C", str(DOTFILES_DIR), "push"], check=True) ctx.console.success("Changes committed and pushed") diff --git a/commands/enter.py b/src/flow/commands/enter.py similarity index 70% rename from commands/enter.py rename to src/flow/commands/enter.py index e4250a7..0f335e9 100644 --- a/commands/enter.py +++ b/src/flow/commands/enter.py @@ -3,6 +3,7 @@ import getpass import os import sys +from typing import Optional from flow.core.config import FlowContext @@ -57,6 +58,51 @@ def _build_destination(user: str, host: str, preserve_host_user: bool = False) - return f"{user}@{host}" +def _terminfo_fix_command(term: Optional[str], destination: str) -> Optional[str]: + normalized_term = (term or "").strip().lower() + + if normalized_term == "xterm-ghostty": + return f"infocmp -x xterm-ghostty | ssh {destination} -- tic -x -" + + if normalized_term == "wezterm": + return ( + f"ssh {destination} -- sh -lc " + "'tempfile=$(mktemp) && curl -fsSL -o \"$tempfile\" " + "https://raw.githubusercontent.com/wezterm/wezterm/main/termwiz/data/wezterm.terminfo " + "&& tic -x -o ~/.terminfo \"$tempfile\" && rm \"$tempfile\"'" + ) + + return None + + +def _handle_terminfo_warning(ctx: FlowContext, term: Optional[str], destination: str, dry_run: bool) -> bool: + install_cmd = _terminfo_fix_command(term, destination) + if not install_cmd: + return True + + ctx.console.warn( + f"Detected TERM={term}. Remote host may be missing this terminfo entry." + ) + ctx.console.info("flow will not install or modify terminfo on the target automatically.") + ctx.console.info("If needed, run this command manually before reconnecting:") + print(f" {install_cmd}") + + if dry_run or not sys.stdin.isatty(): + return True + + response = "" + try: + response = input("Continue with SSH connection? [Y/n] ").strip().lower() + except EOFError: + return True + + if response in {"n", "no"}: + ctx.console.warn("Cancelled before opening SSH session") + return False + + return True + + def run(ctx: FlowContext, args): # Warn if already inside an instance if os.environ.get("DF_NAMESPACE") and os.environ.get("DF_PLATFORM"): @@ -106,6 +152,9 @@ def run(ctx: FlowContext, args): ssh_host = host_template.replace("", namespace) destination = _build_destination(user, ssh_host, preserve_host_user=not user_was_explicit) + if not _handle_terminfo_warning(ctx, os.environ.get("TERM"), destination, dry_run=args.dry_run): + sys.exit(1) + # Build SSH command ssh_cmd = ["ssh", "-tt"] if ssh_identity: diff --git a/commands/package.py b/src/flow/commands/package.py similarity index 100% rename from commands/package.py rename to src/flow/commands/package.py diff --git a/commands/sync.py b/src/flow/commands/sync.py similarity index 100% rename from commands/sync.py rename to src/flow/commands/sync.py diff --git a/core/__init__.py b/src/flow/core/__init__.py similarity index 100% rename from core/__init__.py rename to src/flow/core/__init__.py diff --git a/core/action.py b/src/flow/core/action.py similarity index 100% rename from core/action.py rename to src/flow/core/action.py diff --git a/core/config.py b/src/flow/core/config.py similarity index 100% rename from core/config.py rename to src/flow/core/config.py diff --git a/core/console.py b/src/flow/core/console.py similarity index 100% rename from core/console.py rename to src/flow/core/console.py diff --git a/core/paths.py b/src/flow/core/paths.py similarity index 100% rename from core/paths.py rename to src/flow/core/paths.py diff --git a/core/platform.py b/src/flow/core/platform.py similarity index 77% rename from core/platform.py rename to src/flow/core/platform.py index 7c83c01..c71b57a 100644 --- a/core/platform.py +++ b/src/flow/core/platform.py @@ -1,9 +1,7 @@ """OS and architecture detection.""" import platform as _platform -import shutil from dataclasses import dataclass -from typing import Optional @dataclass @@ -33,11 +31,3 @@ def detect_platform() -> PlatformInfo: raise RuntimeError(f"Unsupported architecture: {raw_arch}") return PlatformInfo(os=os_name, arch=arch, platform=f"{os_name}-{arch}") - - -def detect_container_runtime() -> Optional[str]: - """Return 'docker' or 'podman' if available, else None.""" - for runtime in ("docker", "podman"): - if shutil.which(runtime): - return runtime - return None diff --git a/core/process.py b/src/flow/core/process.py similarity index 71% rename from core/process.py rename to src/flow/core/process.py index 6e1ad2e..3f28326 100644 --- a/core/process.py +++ b/src/flow/core/process.py @@ -26,14 +26,17 @@ def run_command( ) output_lines = [] - for line in process.stdout: - line = line.rstrip() - if line: - if not capture: - console.step_output(line) - output_lines.append(line) - - process.wait() + assert process.stdout is not None # guaranteed by stdout=PIPE + try: + for line in process.stdout: + line = line.rstrip() + if line: + if not capture: + console.step_output(line) + output_lines.append(line) + finally: + process.stdout.close() + process.wait() if check and process.returncode != 0: raise RuntimeError( diff --git a/core/stow.py b/src/flow/core/stow.py similarity index 100% rename from core/stow.py rename to src/flow/core/stow.py diff --git a/core/variables.py b/src/flow/core/variables.py similarity index 100% rename from core/variables.py rename to src/flow/core/variables.py diff --git a/tests/__pycache__/__init__.cpython-313.pyc b/tests/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 335789e..0000000 Binary files a/tests/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_action.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_action.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index b0623ab..0000000 Binary files a/tests/__pycache__/test_action.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_action.cpython-313.pyc b/tests/__pycache__/test_action.cpython-313.pyc deleted file mode 100644 index 1bdfc64..0000000 Binary files a/tests/__pycache__/test_action.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_bootstrap.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_bootstrap.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 5f0c1cc..0000000 Binary files a/tests/__pycache__/test_bootstrap.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_bootstrap.cpython-313.pyc b/tests/__pycache__/test_bootstrap.cpython-313.pyc deleted file mode 100644 index 3bcf76d..0000000 Binary files a/tests/__pycache__/test_bootstrap.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_cli.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_cli.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 3e7381c..0000000 Binary files a/tests/__pycache__/test_cli.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_cli.cpython-313.pyc b/tests/__pycache__/test_cli.cpython-313.pyc deleted file mode 100644 index 9873893..0000000 Binary files a/tests/__pycache__/test_cli.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_commands.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_commands.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index b2f2c39..0000000 Binary files a/tests/__pycache__/test_commands.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_commands.cpython-313.pyc b/tests/__pycache__/test_commands.cpython-313.pyc deleted file mode 100644 index 4e79f0b..0000000 Binary files a/tests/__pycache__/test_commands.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_completion.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_completion.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index bcd483d..0000000 Binary files a/tests/__pycache__/test_completion.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_completion.cpython-313.pyc b/tests/__pycache__/test_completion.cpython-313.pyc deleted file mode 100644 index 8c92901..0000000 Binary files a/tests/__pycache__/test_completion.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_config.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_config.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index c16fe75..0000000 Binary files a/tests/__pycache__/test_config.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_config.cpython-313.pyc b/tests/__pycache__/test_config.cpython-313.pyc deleted file mode 100644 index 78a6490..0000000 Binary files a/tests/__pycache__/test_config.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_console.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_console.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 170038b..0000000 Binary files a/tests/__pycache__/test_console.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_console.cpython-313.pyc b/tests/__pycache__/test_console.cpython-313.pyc deleted file mode 100644 index 75b50ff..0000000 Binary files a/tests/__pycache__/test_console.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_dotfiles.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_dotfiles.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 616e5b0..0000000 Binary files a/tests/__pycache__/test_dotfiles.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_dotfiles.cpython-313.pyc b/tests/__pycache__/test_dotfiles.cpython-313.pyc deleted file mode 100644 index fe451b8..0000000 Binary files a/tests/__pycache__/test_dotfiles.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_dotfiles_folding.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_dotfiles_folding.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 6cdb437..0000000 Binary files a/tests/__pycache__/test_dotfiles_folding.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_dotfiles_folding.cpython-313.pyc b/tests/__pycache__/test_dotfiles_folding.cpython-313.pyc deleted file mode 100644 index fd3bfb7..0000000 Binary files a/tests/__pycache__/test_dotfiles_folding.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_paths.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_paths.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 5362077..0000000 Binary files a/tests/__pycache__/test_paths.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_paths.cpython-313.pyc b/tests/__pycache__/test_paths.cpython-313.pyc deleted file mode 100644 index 9159138..0000000 Binary files a/tests/__pycache__/test_paths.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_platform.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_platform.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 335d4df..0000000 Binary files a/tests/__pycache__/test_platform.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_platform.cpython-313.pyc b/tests/__pycache__/test_platform.cpython-313.pyc deleted file mode 100644 index 89cb573..0000000 Binary files a/tests/__pycache__/test_platform.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_self_hosting.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_self_hosting.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 9ad5087..0000000 Binary files a/tests/__pycache__/test_self_hosting.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_self_hosting.cpython-313.pyc b/tests/__pycache__/test_self_hosting.cpython-313.pyc deleted file mode 100644 index 2caca4e..0000000 Binary files a/tests/__pycache__/test_self_hosting.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_stow.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_stow.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 6847b3c..0000000 Binary files a/tests/__pycache__/test_stow.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_stow.cpython-313.pyc b/tests/__pycache__/test_stow.cpython-313.pyc deleted file mode 100644 index 99e7f4d..0000000 Binary files a/tests/__pycache__/test_stow.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_variables.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_variables.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 0a239b0..0000000 Binary files a/tests/__pycache__/test_variables.cpython-313-pytest-9.0.2.pyc and /dev/null differ diff --git a/tests/__pycache__/test_variables.cpython-313.pyc b/tests/__pycache__/test_variables.cpython-313.pyc deleted file mode 100644 index 559e733..0000000 Binary files a/tests/__pycache__/test_variables.cpython-313.pyc and /dev/null differ diff --git a/tests/test_cli.py b/tests/test_cli.py index 7aff0bb..b92be51 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -135,6 +135,20 @@ def test_enter_dry_run_with_user(): assert "root@personal.orb" in result.stdout +def test_enter_dry_run_shows_terminfo_hint_for_ghostty(): + env = _clean_env() + env["TERM"] = "xterm-ghostty" + + result = subprocess.run( + [sys.executable, "-m", "flow", "enter", "--dry-run", "personal@orb"], + capture_output=True, text=True, env=env, + ) + + assert result.returncode == 0 + assert "flow will not install or modify terminfo" in result.stdout + assert "infocmp -x xterm-ghostty | ssh" in result.stdout + + def test_aliases(): """Test that command aliases work.""" for alias, cmd in [("dot", "dotfiles"), ("pkg", "package"), ("setup", "bootstrap")]: diff --git a/tests/test_commands.py b/tests/test_commands.py index 0ffc93e..ddc88d4 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,6 +1,6 @@ """Tests for command modules — registration and target parsing.""" -from flow.commands.enter import _parse_target +from flow.commands.enter import _parse_target, _terminfo_fix_command from flow.commands.container import _cname, _parse_image_ref @@ -24,6 +24,21 @@ class TestParseTarget: assert plat is None +class TestTerminfoFixCommand: + def test_ghostty_command(self): + cmd = _terminfo_fix_command("xterm-ghostty", "devbox.core.lan") + assert cmd == "infocmp -x xterm-ghostty | ssh devbox.core.lan -- tic -x -" + + def test_wezterm_command(self): + cmd = _terminfo_fix_command("wezterm", "user@devbox.core.lan") + assert cmd is not None + assert "wezterm.terminfo" in cmd + assert "ssh user@devbox.core.lan" in cmd + + def test_unknown_term(self): + assert _terminfo_fix_command("xterm-256color", "devbox.core.lan") is None + + class TestCname: def test_adds_prefix(self): assert _cname("api") == "dev-api" diff --git a/tests/test_dotfiles.py b/tests/test_dotfiles.py index 19981af..3455774 100644 --- a/tests/test_dotfiles.py +++ b/tests/test_dotfiles.py @@ -2,7 +2,6 @@ import json from pathlib import Path -from unittest.mock import MagicMock import pytest diff --git a/tests/test_dotfiles_folding.py b/tests/test_dotfiles_folding.py index c7b677f..62d1897 100644 --- a/tests/test_dotfiles_folding.py +++ b/tests/test_dotfiles_folding.py @@ -2,14 +2,12 @@ import os from pathlib import Path -from unittest.mock import MagicMock import pytest -from flow.commands.dotfiles import _discover_packages, _walk_package, run_link, run_status +from flow.commands.dotfiles import _discover_packages, _walk_package from flow.core.config import AppConfig, FlowContext from flow.core.console import ConsoleLogger -from flow.core.paths import LINKED_STATE from flow.core.platform import PlatformInfo from flow.core.stow import LinkTree, TreeFolder diff --git a/tests/test_platform.py b/tests/test_platform.py index edb9611..86bea94 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -4,7 +4,7 @@ import platform as _platform import pytest -from flow.core.platform import PlatformInfo, detect_container_runtime, detect_platform +from flow.core.platform import PlatformInfo, detect_platform def test_detect_platform_returns_platforminfo(): @@ -27,6 +27,4 @@ def test_detect_platform_unsupported_arch(monkeypatch): detect_platform() -def test_detect_container_runtime_returns_string_or_none(): - result = detect_container_runtime() - assert result is None or result in ("docker", "podman") +