Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions bin/dotfiles
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ usage() {
printf ' --non-interactive Fail instead of prompting in delegated commands\n'
printf ' --skip-apply Skip the apply phase for install/update\n'
printf ' --skip-tools Skip default agent-tool installation for install/update\n'
printf ' --yolo Full install: rich profile + all packages + all tools + auto-yes\n'
printf ' --help, -h Show this help\n'
printf '\n'
printf '%bCommands:%b\n' "$CLR_CYAN" "$CLR_RESET"
Expand Down Expand Up @@ -101,6 +102,9 @@ DOTFILES_NONINTERACTIVE=${DOTFILES_NONINTERACTIVE:-0}
DOTFILES_SKIP_APPLY=${DOTFILES_SKIP_APPLY:-0}
DOTFILES_SKIP_TOOLS=${DOTFILES_SKIP_TOOLS:-0}
DOTFILES_CHECKOUT_ALREADY_UPDATED=${DOTFILES_CHECKOUT_ALREADY_UPDATED:-0}
DOTFILES_PREFER_RICH=${DOTFILES_PREFER_RICH:-0}
DOTFILES_ALL_PACKAGES=${DOTFILES_ALL_PACKAGES:-0}
DOTFILES_ALL_TOOLS=${DOTFILES_ALL_TOOLS:-0}

while [ "$#" -gt 0 ]; do
case "$1" in
Expand Down Expand Up @@ -129,6 +133,13 @@ while [ "$#" -gt 0 ]; do
DOTFILES_SKIP_TOOLS=1
shift
;;
--yolo)
DOTFILES_YES=1
DOTFILES_PREFER_RICH=1
DOTFILES_ALL_PACKAGES=1
DOTFILES_ALL_TOOLS=1
shift
;;
--help|-h)
usage
exit 0
Expand All @@ -151,7 +162,7 @@ if [ "$#" -gt 0 ]; then
shift
fi

export DOTFILES_DRY_RUN DOTFILES_YES DOTFILES_NONINTERACTIVE DOTFILES_SKIP_APPLY DOTFILES_SKIP_TOOLS DOTFILES_CHECKOUT_ALREADY_UPDATED DOTFILES_REPO_ROOT=$REPO_ROOT
export DOTFILES_DRY_RUN DOTFILES_YES DOTFILES_NONINTERACTIVE DOTFILES_SKIP_APPLY DOTFILES_SKIP_TOOLS DOTFILES_CHECKOUT_ALREADY_UPDATED DOTFILES_PREFER_RICH DOTFILES_ALL_PACKAGES DOTFILES_ALL_TOOLS DOTFILES_REPO_ROOT=$REPO_ROOT

load_packages_support() {
package_lib="$REPO_ROOT/scripts/sh/packages.sh"
Expand Down Expand Up @@ -225,8 +236,17 @@ run_install() {
fi

if _dotfiles_is_truthy "$DOTFILES_SKIP_TOOLS"; then
dotfiles_skip "default agent tools (--skip-tools)"
dotfiles_skip "agent tools (--skip-tools)"
_install_skip=$(( _install_skip + 1 ))
elif _dotfiles_is_truthy "$DOTFILES_ALL_TOOLS"; then
dotfiles_step "Installing ALL agent tools (yolo)"
if load_tools_support && dotfiles_install_all_tools; then
dotfiles_ok "All agent tools installed"
_install_ok=$(( _install_ok + 1 ))
else
dotfiles_fail "Agent tools installation had failures"
_install_fail=$(( _install_fail + 1 ))
fi
else
dotfiles_step "Installing default tools"
if load_tools_support && dotfiles_install_default_tools; then
Expand All @@ -238,6 +258,17 @@ run_install() {
fi
fi

if _dotfiles_is_truthy "$DOTFILES_ALL_PACKAGES"; then
dotfiles_step "Installing ALL packages (yolo)"
if load_packages_support && dotfiles_packages_main --all; then
dotfiles_ok "All packages installed"
_install_ok=$(( _install_ok + 1 ))
else
dotfiles_fail "Package installation had failures"
_install_fail=$(( _install_fail + 1 ))
fi
fi

# Summary line.
dotfiles_header "Install complete"
if [ -n "$CLR_GREEN" ]; then
Expand Down
9 changes: 9 additions & 0 deletions scripts/dotfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ def resolve_profile(repo_root: Path, manifest: dict[str, Any], profile_name: str
else:
effective_name = default_profiles.get("linux", "linux-desktop")

# --yolo / DOTFILES_PREFER_RICH: upgrade to rich variant if available.
# Skipped when profile was explicitly specified or resolved to ssh-server.
if os.environ.get("DOTFILES_PREFER_RICH", "0") in ("1", "true", "yes"):
ssh_profile = default_profiles.get("ssh", "ssh-server")
if effective_name != ssh_profile:
rich_candidate = f"{effective_name}-rich"
if (repo_root / "profiles" / f"{rich_candidate}.json").is_file():
effective_name = rich_candidate

cache: dict[str, dict[str, Any]] = {}
resolving: set[str] = set()

Expand Down
62 changes: 62 additions & 0 deletions tests/test_dotfiles_bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,67 @@ def test_relative_symlinked_launcher_resolves_repo_root(self) -> None:
self.assertIn(f"Usage: {launcher} [global-options] <command> [command-args]", completed.stdout)


class YoloFlagTests(unittest.TestCase):
def run_bin(self, *args: str, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
base_env = {**os.environ, "NO_COLOR": "1"}
if env:
base_env.update(env)
return subprocess.run(
[str(DOTFILES_BIN), *args],
capture_output=True,
text=True,
env=base_env,
)

def test_yolo_in_help(self) -> None:
completed = self.run_bin("--help")

self.assertEqual(
completed.returncode,
0,
msg=f"--help failed\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}",
)
self.assertIn("--yolo", completed.stdout)

def test_yolo_dry_run_all_phases(self) -> None:
completed = self.run_bin("--yolo", "--dry-run", "install")

self.assertEqual(
completed.returncode,
0,
msg=f"--yolo --dry-run install failed\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}",
)
self.assertIn("ALL agent tools", completed.stdout)
self.assertIn("ALL packages", completed.stdout)

def test_yolo_skip_tools_wins(self) -> None:
completed = self.run_bin("--yolo", "--skip-tools", "--dry-run", "install")

self.assertEqual(
completed.returncode,
0,
msg=(
"--yolo --skip-tools --dry-run install failed\n"
f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
),
)
self.assertIn("agent tools (--skip-tools)", completed.stdout)
self.assertNotIn("ALL agent tools", completed.stdout)

def test_yolo_skip_apply(self) -> None:
completed = self.run_bin("--yolo", "--skip-apply", "--dry-run", "install")

self.assertEqual(
completed.returncode,
0,
msg=(
"--yolo --skip-apply --dry-run install failed\n"
f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
),
)
self.assertIn("ALL agent tools", completed.stdout)
self.assertIn("ALL packages", completed.stdout)


if __name__ == "__main__":
unittest.main(verbosity=2)
176 changes: 176 additions & 0 deletions tests/test_resolve_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from __future__ import annotations

import importlib.util
import json
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT_PATH = REPO_ROOT / "scripts" / "dotfiles.py"


def load_script_module():
spec = importlib.util.spec_from_file_location("alohays_dotfiles_script", SCRIPT_PATH)
if spec is None or spec.loader is None:
raise RuntimeError("unable to load scripts/dotfiles.py")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module


def _write_json(path: Path, payload: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload), encoding="utf-8")


class ResolveProfileRichUpgradeTests(unittest.TestCase):
"""Unit tests for the DOTFILES_PREFER_RICH upgrade logic in resolve_profile()."""

@classmethod
def setUpClass(cls) -> None:
cls.script = load_script_module()

def setUp(self) -> None:
self._tmp = Path(tempfile.mkdtemp(prefix="dotfiles-resolve-profile-"))
self.addCleanup(lambda: shutil.rmtree(self._tmp, ignore_errors=True))
self._build_repo(self._tmp)

def _build_repo(self, root: Path) -> None:
"""Write a minimal repo layout with the profiles needed by the tests."""
modules = [
{"name": "core", "source_root": "modules/core"},
{"name": "tmux", "source_root": "modules/tmux"},
{"name": "nvim", "source_root": "modules/nvim"},
{"name": "visual", "source_root": "modules/visual"},
{"name": "terminal", "source_root": "modules/terminal"},
{"name": "prompt", "source_root": "modules/prompt"},
{"name": "ssh-server", "source_root": "modules/ssh-server"},
]
manifest = {
"schema_version": 1,
"default_profiles": {
"darwin": "macos-desktop",
"linux": "linux-desktop",
"ssh": "ssh-server",
},
"modules": modules,
}
_write_json(root / "manifests" / "manifest.json", manifest)

profiles_dir = root / "profiles"
_write_json(
profiles_dir / "base.json",
{"name": "base", "extends": [], "modules": ["core"]},
)
_write_json(
profiles_dir / "macos-desktop.json",
{
"name": "macos-desktop",
"extends": ["base"],
"modules": ["tmux", "nvim", "visual"],
},
)
_write_json(
profiles_dir / "macos-desktop-rich.json",
{
"name": "macos-desktop-rich",
"extends": ["macos-desktop"],
"modules": ["terminal", "prompt"],
},
)
_write_json(
profiles_dir / "linux-desktop.json",
{
"name": "linux-desktop",
"extends": ["base"],
"modules": ["tmux", "nvim", "visual"],
},
)
_write_json(
profiles_dir / "linux-desktop-rich.json",
{
"name": "linux-desktop-rich",
"extends": ["linux-desktop"],
"modules": ["terminal", "prompt"],
},
)
_write_json(
profiles_dir / "ssh-server.json",
{
"name": "ssh-server",
"extends": ["base"],
"modules": ["ssh-server", "tmux"],
},
)

def _resolve_clean(
self,
profile_name: str | None,
*,
prefer_rich: str = "0",
platform_system: str = "Darwin",
ssh_connection: str | None = None,
display: str | None = None,
) -> dict:
"""Resolve a profile with a fully controlled environment."""
patch_env: dict[str, str] = {"DOTFILES_PREFER_RICH": prefer_rich}
remove_keys = {"SSH_CONNECTION", "SSH_TTY", "DISPLAY", "WAYLAND_DISPLAY"}

if ssh_connection is not None:
patch_env["SSH_CONNECTION"] = ssh_connection
remove_keys.discard("SSH_CONNECTION")
if display is not None:
patch_env["DISPLAY"] = display
remove_keys.discard("DISPLAY")

manifest = self.script.load_manifest(self._tmp)

import os as _os
saved = {k: _os.environ.pop(k, None) for k in remove_keys}
try:
with mock.patch("platform.system", return_value=platform_system), \
mock.patch.dict(_os.environ, patch_env, clear=False):
return self.script.resolve_profile(self._tmp, manifest, profile_name)
finally:
for k, v in saved.items():
if v is not None:
_os.environ[k] = v

def test_prefer_rich_darwin_resolves_macos_desktop_rich(self) -> None:
result = self._resolve_clean(None, prefer_rich="1", platform_system="Darwin")
self.assertEqual(result["name"], "macos-desktop-rich")

def test_prefer_rich_linux_no_ssh_resolves_linux_desktop_rich(self) -> None:
result = self._resolve_clean(None, prefer_rich="1", platform_system="Linux")
self.assertEqual(result["name"], "linux-desktop-rich")

def test_prefer_rich_ssh_headless_resolves_ssh_server_not_upgraded(self) -> None:
result = self._resolve_clean(
None,
prefer_rich="1",
platform_system="Linux",
ssh_connection="10.0.0.1 22 10.0.0.2 54321",
)
self.assertEqual(result["name"], "ssh-server")

def test_prefer_rich_explicit_profile_name_not_upgraded(self) -> None:
result = self._resolve_clean("macos-desktop", prefer_rich="1", platform_system="Darwin")
self.assertEqual(result["name"], "macos-desktop")

def test_no_prefer_rich_does_not_upgrade(self) -> None:
result = self._resolve_clean(None, prefer_rich="0", platform_system="Darwin")
self.assertEqual(result["name"], "macos-desktop")

def test_prefer_rich_missing_rich_profile_returns_base_profile(self) -> None:
(self._tmp / "profiles" / "macos-desktop-rich.json").unlink()
result = self._resolve_clean(None, prefer_rich="1", platform_system="Darwin")
self.assertEqual(result["name"], "macos-desktop")


if __name__ == "__main__":
unittest.main(verbosity=2)
Loading