Compare commits

...

14 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
55a0ae4337 Release version 1.5.0
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:43:19 +01:00
Kevin Veen-Birkenbach
bcf284c5d6 Solved variable naming bug
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:33:38 +01:00
Kevin Veen-Birkenbach
db23b1a445 Solved ruff hints
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:30:10 +01:00
Kevin Veen-Birkenbach
506f69d8a7 Solved variable bug
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:27:06 +01:00
Kevin Veen-Birkenbach
097e64408f Fix repository deinstall logic and add unit tests for repository helpers
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
- Fix undefined repo_dir usage in repository deinstall action
- Centralize and harden get_repo_dir with strict validation and clear errors
- Expand user paths for repository base and binary directories
- Add unit tests for get_repo_dir and deinstall_repos
- Add comprehensive tests for resolve_repos identifier matching
- Remove obsolete command resolution tests no longer applicable

https://chatgpt.com/share/693d7442-c2d0-800f-9ff3-fb84d60eaeb4
2025-12-13 15:12:12 +01:00
Kevin Veen-Birkenbach
a3913d9489 Solved variable bug
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:05:34 +01:00
Kevin Veen-Birkenbach
c92fd44dd3 fix(uninstall): robustly remove pkgmgr venv auto-activation and leftover shell RC entries
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 14:48:59 +01:00
Kevin Veen-Birkenbach
2c3efa7a27 Solved shellcheck quoting issue
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 14:38:37 +01:00
Kevin Veen-Birkenbach
f388bc51bc Ruff autofix
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 14:36:55 +01:00
Kevin Veen-Birkenbach
4e28eba883 refactor(ci,build,test): rename distro to PKGMGR_DISTRO for consistent environment handling
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
https://chatgpt.com/share/693d6b63-12cc-800f-b55f-abc52ee7fb52
2025-12-13 14:34:15 +01:00
Kevin Veen-Birkenbach
b8acd634f8 Improve run_command error diagnostics with live output capture
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Switch run_command to a single-run execution model that streams stdout/stderr
live while capturing both streams in memory using selectors. This guarantees
that command errors (e.g. make install, pip, nix) always show full diagnostics
without re-running commands or risking deadlocks.

Add unit tests for preview mode, success execution, failure handling, and
allow_failure behavior.

Context:
https://chatgpt.com/share/replace-with-this-conversation-link
2025-12-13 14:29:53 +01:00
Kevin Veen-Birkenbach
fb68b325d6 Fix ShellCheck warnings and harden shell scripts
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
- Quote Docker volume names to avoid word splitting
- Add missing shebangs for proper shell detection
- Annotate sourced scripts for ShellCheck resolution
- Remove unused variables
- Explicitly disable SC2016 where literal RC strings are intended
- Improve robustness of cleanup logic

https://chatgpt.com/share/693d6557-a080-800f-8915-c57476569232
2025-12-13 14:08:35 +01:00
Kevin Veen-Birkenbach
650a22d425 Changed other formatation codesniffer solution
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 14:00:06 +01:00
Kevin Veen-Birkenbach
6a590d8780 Solved save user config bug 2025-12-13 13:55:49 +01:00
51 changed files with 484 additions and 285 deletions

View File

@@ -22,4 +22,4 @@ jobs:
- name: Run E2E tests via make (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make test-e2e
PKGMGR_DISTRO="${{ matrix.distro }}" make test-e2e

View File

@@ -23,4 +23,4 @@ jobs:
- name: Nix flake-only test (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make test-env-nix
PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-nix

View File

@@ -25,4 +25,4 @@ jobs:
- name: Run container tests (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make test-env-virtual
PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-virtual

View File

@@ -16,4 +16,4 @@ jobs:
run: docker version
- name: Run integration tests via make (Arch container)
run: make test-integration distro="arch"
run: make test-integration PKGMGR_DISTRO="arch"

View File

@@ -16,4 +16,4 @@ jobs:
run: docker version
- name: Run unit tests via make (Arch container)
run: make test-unit distro="arch"
run: make test-unit PKGMGR_DISTRO="arch"

View File

@@ -23,7 +23,7 @@ jobs:
- name: Build virgin container (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make build-missing-virgin
PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
# 🔹 RUN test inside virgin image
- name: Virgin ${{ matrix.distro }} pkgmgr test (root)

View File

@@ -23,7 +23,7 @@ jobs:
- name: Build virgin container (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make build-missing-virgin
PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
# 🔹 RUN test inside virgin image as non-root
- name: Virgin ${{ matrix.distro }} pkgmgr test (user)

View File

@@ -1,3 +1,16 @@
## [1.5.0] - 2025-12-13
* - Commands now show live output while running, making long operations easier to follow
- Error messages include full command output, making failures easier to understand and debug
- Deinstallation is more complete and predictable, removing CLI links and properly cleaning up repositories
- Preview mode is more trustworthy, clearly showing what would happen without making changes
- Repository configuration problems are detected earlier with clear, user-friendly explanations
- More consistent behavior across different Linux distributions
- More reliable execution in Docker containers and CI environments
- Nix-based execution works more smoothly, especially when running as root or inside containers
- Existing commands, scripts, and workflows continue to work without any breaking changes
## [1.4.1] - 2025-12-12
* Fixed stable release container publishing

View File

@@ -7,8 +7,8 @@
# Distro
# Options: arch debian ubuntu fedora centos
DISTROS ?= arch debian ubuntu fedora centos
distro ?= arch
export distro
PKGMGR_DISTRO ?= arch
export PKGMGR_DISTRO
# ------------------------------------------------------------
# Base images
@@ -75,7 +75,7 @@ build-no-cache-all:
@set -e; \
for d in $(DISTROS); do \
echo "=== build-no-cache: $$d ==="; \
distro="$$d" $(MAKE) build-no-cache; \
PKGMGR_DISTRO="$$d" $(MAKE) build-no-cache; \
done
# ------------------------------------------------------------
@@ -101,7 +101,7 @@ test-env-nix: build-missing
test: test-env-virtual test-unit test-integration test-e2e
delete-volumes:
@docker volume rm pkgmgr_nix_store_${distro} pkgmgr_nix_cache_${distro} || true
@docker volume rm "pkgmgr_nix_store_${PKGMGR_DISTRO}" "pkgmgr_nix_cache_${PKGMGR_DISTRO}" || echo "No volumes to delete."
purge: delete-volumes build-no-cache

View File

@@ -26,17 +26,13 @@
packages = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
# Single source of truth for pkgmgr: Python 3.11
# - Matches pyproject.toml: requires-python = ">=3.11"
# - Uses python311Packages so that PyYAML etc. are available
python = pkgs.python311;
pyPkgs = pkgs.python311Packages;
in
rec {
pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager";
version = "1.4.1";
version = "1.5.0";
# Use the git repo as source
src = ./.;

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "package-manager"
version = "1.4.1"
version = "1.5.0"
description = "Kevin's package-manager tool (pkgmgr)"
readme = "README.md"
requires-python = ">=3.9"

View File

@@ -8,13 +8,13 @@ set -euo pipefail
: "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:stream9}"
resolve_base_image() {
local distro="$1"
case "$distro" in
local PKGMGR_DISTRO="$1"
case "$PKGMGR_DISTRO" in
arch) echo "$BASE_IMAGE_ARCH" ;;
debian) echo "$BASE_IMAGE_DEBIAN" ;;
ubuntu) echo "$BASE_IMAGE_UBUNTU" ;;
fedora) echo "$BASE_IMAGE_FEDORA" ;;
centos) echo "$BASE_IMAGE_CENTOS" ;;
*) echo "ERROR: Unknown distro '$distro'" >&2; exit 1 ;;
*) echo "ERROR: Unknown distro '$PKGMGR_DISTRO'" >&2; exit 1 ;;
esac
}

View File

@@ -2,9 +2,11 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./scripts/build/base.sh
source "${SCRIPT_DIR}/base.sh"
: "${distro:?Environment variable 'distro' must be set (arch|debian|ubuntu|fedora|centos)}"
: "${PKGMGR_DISTRO:?Environment variable 'PKGMGR_DISTRO' must be set (arch|debian|ubuntu|fedora|centos)}"
NO_CACHE=0
MISSING_ONLY=0
@@ -20,13 +22,13 @@ IS_STABLE="false" # "true" -> publish stable tags
DEFAULT_DISTRO="arch"
usage() {
local default_tag="pkgmgr-${distro}"
local default_tag="pkgmgr-${PKGMGR_DISTRO}"
if [[ -n "${TARGET:-}" ]]; then
default_tag="${default_tag}-${TARGET}"
fi
cat <<EOF
Usage: distro=<distro> $0 [options]
Usage: PKGMGR_DISTRO=<distro> $0 [options]
Build options:
--missing Build only if the image does not already exist (local build only)
@@ -101,13 +103,13 @@ done
# Derive default local tag if not provided
if [[ -z "${IMAGE_TAG}" ]]; then
IMAGE_TAG="${REPO_PREFIX}-${distro}"
IMAGE_TAG="${REPO_PREFIX}-${PKGMGR_DISTRO}"
if [[ -n "${TARGET}" ]]; then
IMAGE_TAG="${IMAGE_TAG}-${TARGET}"
fi
fi
BASE_IMAGE="$(resolve_base_image "$distro")"
BASE_IMAGE="$(resolve_base_image "$PKGMGR_DISTRO")"
# Local-only "missing" shortcut
if [[ "${MISSING_ONLY}" == "1" ]]; then
@@ -139,7 +141,7 @@ fi
echo
echo "------------------------------------------------------------"
echo "[build] Building image"
echo "distro = ${distro}"
echo "distro = ${PKGMGR_DISTRO}"
echo "BASE_IMAGE = ${BASE_IMAGE}"
if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
@@ -165,14 +167,14 @@ if [[ -n "${TARGET}" ]]; then
fi
compute_publish_tags() {
local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${distro}"
local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${PKGMGR_DISTRO}"
local alias_tag_base=""
if [[ -n "${TARGET}" ]]; then
distro_tag_base="${distro_tag_base}-${TARGET}"
fi
if [[ "${distro}" == "${DEFAULT_DISTRO}" ]]; then
if [[ "${PKGMGR_DISTRO}" == "${DEFAULT_DISTRO}" ]]; then
alias_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}"
if [[ -n "${TARGET}" ]]; then
alias_tag_base="${alias_tag_base}-${TARGET}"

View File

@@ -30,11 +30,11 @@ echo "[publish] DISTROS=${DISTROS}"
for d in ${DISTROS}; do
echo
echo "============================================================"
echo "[publish] distro=${d}"
echo "[publish] PKGMGR_DISTRO=${d}"
echo "============================================================"
# virgin
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
--publish \
--registry "${REGISTRY}" \
--owner "${OWNER}" \
@@ -43,7 +43,7 @@ for d in ${DISTROS}; do
--target virgin
# full (default target)
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
--publish \
--registry "${REGISTRY}" \
--owner "${OWNER}" \

View File

@@ -1,8 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "[docker] Starting package-manager container"
# ---------------------------------------------------------------------------

View File

@@ -1,22 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck source=lib/bootstrap_config.sh
# shellcheck source=lib/detect.sh
# shellcheck source=lib/path.sh
# shellcheck source=lib/symlinks.sh
# shellcheck source=lib/users.sh
# shellcheck source=lib/install.sh
# shellcheck source=lib/nix_conf_file.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./scripts/nix/lib/bootstrap_config.sh
source "${SCRIPT_DIR}/lib/bootstrap_config.sh"
# shellcheck source=./scripts/nix/lib/detect.sh
source "${SCRIPT_DIR}/lib/detect.sh"
# shellcheck source=./scripts/nix/lib/path.sh
source "${SCRIPT_DIR}/lib/path.sh"
# shellcheck source=./scripts/nix/lib/symlinks.sh
source "${SCRIPT_DIR}/lib/symlinks.sh"
# shellcheck source=./scripts/nix/lib/users.sh
source "${SCRIPT_DIR}/lib/users.sh"
# shellcheck source=./scripts/nix/lib/install.sh
source "${SCRIPT_DIR}/lib/install.sh"
# shellcheck source=./scripts/nix/lib/nix_conf_file.sh
source "${SCRIPT_DIR}/lib/nix_conf_file.sh"
echo "[init-nix] Starting Nix initialization..."

View File

@@ -1,3 +1,5 @@
#!/usr/bin/env bash
# ------------------------------------------------------------
# Nix shell mode: do not touch venv, only run install
# ------------------------------------------------------------

View File

@@ -7,6 +7,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${PROJECT_ROOT}"
VENV_DIR="${HOME}/.venvs/pkgmgr"
# shellcheck disable=SC2016
RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi'
# ------------------------------------------------------------

View File

@@ -2,17 +2,17 @@
set -euo pipefail
echo "============================================================"
echo ">>> Running E2E tests: $distro"
echo ">>> Running E2E tests: $PKGMGR_DISTRO"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v "pkgmgr_nix_store_${distro}:/nix" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
-e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \
--workdir /src \
"pkgmgr-${distro}" \
"pkgmgr-${PKGMGR_DISTRO}" \
bash -lc '
set -euo pipefail

View File

@@ -1,17 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
IMAGE="pkgmgr-${distro}"
IMAGE="pkgmgr-${PKGMGR_DISTRO}"
echo "============================================================"
echo ">>> Running Nix flake-only test in ${distro} container"
echo ">>> Running Nix flake-only test in ${PKGMGR_DISTRO} container"
echo ">>> Image: ${IMAGE}"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v "pkgmgr_nix_store_${distro}:/nix" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
--workdir /src \
-e REINSTALL_PKGMGR=1 \
"${IMAGE}" \
@@ -27,7 +27,7 @@ docker run --rm \
echo ">>> preflight: nix must exist in image"
if ! command -v nix >/dev/null 2>&1; then
echo "NO_NIX"
echo "ERROR: nix not found in image '\'''"${IMAGE}"''\'' (distro='"${distro}"')"
echo "ERROR: nix not found in image '\'''"${IMAGE}"''\'' (PKGMGR_DISTRO='"${PKGMGR_DISTRO}"')"
echo "HINT: Ensure Nix is installed during image build for this distro."
exit 1
fi

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
IMAGE="pkgmgr-$distro"
IMAGE="pkgmgr-$PKGMGR_DISTRO"
echo
echo "------------------------------------------------------------"
@@ -16,9 +16,9 @@ echo
# Run the command and capture the output
if OUTPUT=$(docker run --rm \
-e REINSTALL_PKGMGR=1 \
-v pkgmgr_nix_store_${distro}:/nix \
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "$(pwd):/src" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
"$IMAGE" 2>&1); then
echo "$OUTPUT"
echo

View File

@@ -2,17 +2,17 @@
set -euo pipefail
echo "============================================================"
echo ">>> Running INTEGRATION tests in ${distro} container"
echo ">>> Running INTEGRATION tests in ${PKGMGR_DISTRO} container"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v pkgmgr_nix_store_${distro}:/nix \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
--workdir /src \
-e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \
"pkgmgr-${distro}" \
"pkgmgr-${PKGMGR_DISTRO}" \
bash -lc '
set -e;
git config --global --add safe.directory /src || true;

View File

@@ -2,17 +2,17 @@
set -euo pipefail
echo "============================================================"
echo ">>> Running UNIT tests in ${distro} container"
echo ">>> Running UNIT tests in ${PKGMGR_DISTRO} container"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
-v pkgmgr_nix_store_${distro}:/nix \
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
--workdir /src \
-e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \
"pkgmgr-${distro}" \
"pkgmgr-${PKGMGR_DISTRO}" \
bash -lc '
set -e;
git config --global --add safe.directory /src || true;

View File

@@ -19,12 +19,20 @@ fi
# ------------------------------------------------------------
# Remove auto-activation lines from shell RC files
# ------------------------------------------------------------
RC_PATTERN='\.venvs\/pkgmgr\/bin\/activate"; if \[ -n "\$${PS1:-}" \]; then echo "Global Python virtual environment '\''~\/\.venvs\/pkgmgr'\'' activated."; fi; fi'
# Matches:
# ~/.venvs/pkgmgr/bin/activate
# ./.venvs/pkgmgr/bin/activate
RC_PATTERN='(\./)?\.venvs/pkgmgr/bin/activate'
echo "[uninstall] Cleaning up ~/.bashrc and ~/.zshrc entries..."
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
if [[ -f "$rc" ]]; then
sed -i "/$RC_PATTERN/d" "$rc"
# Remove activation lines (functional)
sed -E -i "/$RC_PATTERN/d" "$rc"
# Remove leftover echo / cosmetic lines referencing pkgmgr venv
sed -i '/\.venvs\/pkgmgr/d' "$rc"
echo "[uninstall] Cleaned $rc"
else
echo "[uninstall] File not found: $rc (skipped)"

View File

@@ -1,5 +1,6 @@
import yaml
import os
from pkgmgr.core.config.save import save_user_config
def interactive_add(config,USER_CONFIG_PATH:str):
"""Interactively prompt the user to add a new repository entry to the user config."""

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
"""
High-level mirror actions.
@@ -10,6 +8,7 @@ Public API:
- setup_mirrors
"""
from __future__ import annotations
from .types import Repository, MirrorMap
from .list_cmd import list_mirrors
from .diff_cmd import diff_mirrors

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import os
from urllib.parse import urlparse
from typing import List, Mapping
from typing import Mapping
from .types import MirrorMap, Repository

View File

@@ -1,6 +1,5 @@
import os
import subprocess
import sys
import yaml
from pkgmgr.core.command.alias import generate_alias
from pkgmgr.core.config.save import save_user_config

View File

@@ -1,15 +1,32 @@
import os
import sys
from pkgmgr.core.repository.identifier import get_repo_identifier
from pkgmgr.core.repository.dir import get_repo_dir
def deinstall_repos(selected_repos, repositories_base_dir, bin_dir, all_repos, preview=False):
from pkgmgr.core.command.run import run_command
from pkgmgr.core.repository.dir import get_repo_dir
from pkgmgr.core.repository.identifier import get_repo_identifier
def deinstall_repos(
selected_repos,
repositories_base_dir,
bin_dir,
all_repos,
preview: bool = False,
) -> None:
for repo in selected_repos:
repo_identifier = get_repo_identifier(repo, all_repos)
alias_path = os.path.join(bin_dir, repo_identifier)
# Resolve repository directory
repo_dir = get_repo_dir(repositories_base_dir, repo)
# Prefer alias if available; fall back to identifier
alias_name = str(repo.get("alias") or repo_identifier)
alias_path = os.path.join(os.path.expanduser(bin_dir), alias_name)
# Remove alias link/file (interactive)
if os.path.exists(alias_path):
confirm = input(f"Are you sure you want to delete link '{alias_path}' for {repo_identifier}? [y/N]: ").strip().lower()
confirm = input(
f"Are you sure you want to delete link '{alias_path}' for {repo_identifier}? [y/N]: "
).strip().lower()
if confirm == "y":
if preview:
print(f"[Preview] Would remove link '{alias_path}'.")
@@ -19,10 +36,13 @@ def deinstall_repos(selected_repos, repositories_base_dir, bin_dir, all_repos, p
else:
print(f"No link found for {repo_identifier} in {bin_dir}.")
# Run make deinstall if repository exists and has a Makefile
makefile_path = os.path.join(repo_dir, "Makefile")
if os.path.exists(makefile_path):
print(f"Makefile found in {repo_identifier}, running 'make deinstall'...")
try:
run_command("make deinstall", cwd=repo_dir, preview=preview)
except SystemExit as e:
print(f"[Warning] Failed to run 'make deinstall' for {repo_identifier}: {e}")
print(
f"[Warning] Failed to run 'make deinstall' for {repo_identifier}: {e}"
)

View File

@@ -1,4 +1,3 @@
import sys
import shutil
from pkgmgr.actions.proxy import exec_proxy_command

View File

@@ -1,4 +1,3 @@
import sys
import shutil
from pkgmgr.actions.repository.pull import pull_with_verification

View File

@@ -8,7 +8,7 @@ from pkgmgr.cli.context import CLIContext
from pkgmgr.core.repository.dir import get_repo_dir
from pkgmgr.core.repository.identifier import get_repo_identifier
from pkgmgr.core.git import get_tags
from pkgmgr.core.version.semver import SemVer, extract_semver_from_tags
from pkgmgr.core.version.semver import extract_semver_from_tags
from pkgmgr.actions.changelog import generate_changelog

View File

@@ -15,7 +15,6 @@ from pkgmgr.actions.repository.status import status_repos
from pkgmgr.actions.repository.list import list_repositories
from pkgmgr.core.command.run import run_command
from pkgmgr.actions.repository.create import create_repo
from pkgmgr.core.repository.selected import get_selected_repos
from pkgmgr.core.repository.dir import get_repo_dir
Repository = Dict[str, Any]

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
import selectors
import subprocess
import sys
from typing import List, Optional, Union
CommandType = Union[str, List[str]]
@@ -13,32 +15,97 @@ def run_command(
allow_failure: bool = False,
) -> subprocess.CompletedProcess:
"""
Run a command and optionally exit on error.
Run a command with live output while capturing stdout/stderr.
- If `cmd` is a string, it is executed with `shell=True`.
- If `cmd` is a list of strings, it is executed without a shell.
- Output is streamed live to the terminal.
- Output is captured in memory.
- On failure, captured stdout/stderr are printed again so errors are never lost.
- Command is executed exactly once.
"""
if isinstance(cmd, str):
display = cmd
else:
display = " ".join(cmd)
display = cmd if isinstance(cmd, str) else " ".join(cmd)
where = cwd or "."
if preview:
print(f"[Preview] In '{where}': {display}")
# Fake a successful result; most callers ignore the return value anyway
return subprocess.CompletedProcess(cmd, 0) # type: ignore[arg-type]
print(f"Running in '{where}': {display}")
if isinstance(cmd, str):
result = subprocess.run(cmd, cwd=cwd, shell=True)
else:
result = subprocess.run(cmd, cwd=cwd)
process = subprocess.Popen(
cmd,
cwd=cwd,
shell=isinstance(cmd, str),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
if result.returncode != 0 and not allow_failure:
print(f"Command failed with exit code {result.returncode}. Exiting.")
sys.exit(result.returncode)
assert process.stdout is not None
assert process.stderr is not None
return result
sel = selectors.DefaultSelector()
sel.register(process.stdout, selectors.EVENT_READ, data="stdout")
sel.register(process.stderr, selectors.EVENT_READ, data="stderr")
stdout_lines: List[str] = []
stderr_lines: List[str] = []
try:
while sel.get_map():
for key, _ in sel.select():
stream = key.fileobj
which = key.data
line = stream.readline()
if line == "":
# EOF: stop watching this stream
try:
sel.unregister(stream)
except Exception:
pass
continue
if which == "stdout":
stdout_lines.append(line)
print(line, end="")
else:
stderr_lines.append(line)
print(line, end="", file=sys.stderr)
finally:
# Ensure we don't leak FDs
try:
sel.close()
finally:
try:
process.stdout.close()
except Exception:
pass
try:
process.stderr.close()
except Exception:
pass
returncode = process.wait()
if returncode != 0 and not allow_failure:
print("\n[pkgmgr] Command failed, captured diagnostics:", file=sys.stderr)
print(f"[pkgmgr] Failed command: {display}", file=sys.stderr)
if stdout_lines:
print("----- stdout -----")
print("".join(stdout_lines), end="")
if stderr_lines:
print("----- stderr -----", file=sys.stderr)
print("".join(stderr_lines), end="", file=sys.stderr)
print(f"Command failed with exit code {returncode}. Exiting.")
sys.exit(returncode)
return subprocess.CompletedProcess(
cmd,
returncode,
stdout="".join(stdout_lines),
stderr="".join(stderr_lines),
)

View File

@@ -1,15 +1,48 @@
import sys
import os
import sys
from typing import Any, Dict
def get_repo_dir(repositories_base_dir:str,repo:{})->str:
try:
return os.path.join(repositories_base_dir, repo.get("provider"), repo.get("account"), repo.get("repository"))
except TypeError as e:
if repositories_base_dir:
print(f"Error: {e} \nThe repository {repo} seems not correct configured.\nPlease configure it correct.")
for key in ["provider","account","repository"]:
if not repo.get(key,False):
print(f"Key '{key}' is missing.")
else:
print(f"Error: {e} \nThe base {base} seems not correct configured.\nPlease configure it correct.")
sys.exit(3)
def get_repo_dir(repositories_base_dir: str, repo: Dict[str, Any]) -> str:
"""
Build the local repository directory path from:
repositories_base_dir/provider/account/repository
Exits with code 3 and prints diagnostics if the input config is invalid.
"""
# Base dir must be set and non-empty
if not repositories_base_dir:
print(
"Error: repositories_base_dir is missing.\n"
"The base directory for repositories seems not correctly configured.\n"
"Please configure it correctly."
)
sys.exit(3)
# Repo must be a dict-like object
if not isinstance(repo, dict):
print(
f"Error: invalid repo object '{repo}'.\n"
"The repository entry seems not correctly configured.\n"
"Please configure it correctly."
)
sys.exit(3)
base_dir = os.path.expanduser(str(repositories_base_dir))
provider = repo.get("provider")
account = repo.get("account")
repository = repo.get("repository")
missing = [k for k, v in [("provider", provider), ("account", account), ("repository", repository)] if not v]
if missing:
print(
"Error: repository entry is missing required keys.\n"
f"Repository: {repo}\n"
"Please configure it correctly."
)
for k in missing:
print(f"Key '{k}' is missing.")
sys.exit(3)
return os.path.join(base_dir, str(provider), str(account), str(repository))

View File

@@ -1,4 +1,3 @@
import os
def resolve_repos(identifiers:[], all_repos:[]):
"""

View File

@@ -12,7 +12,6 @@ which we treat as success and suppress in the helper.
from __future__ import annotations
import os
import runpy
import sys
import unittest

View File

@@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import patch
from pkgmgr.actions.branch.utils import _resolve_base_branch
from pkgmgr.core.git import GitError

View File

@@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import patch
from pkgmgr.actions.branch.close_branch import close_branch
from pkgmgr.core.git import GitError

View File

@@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import patch
from pkgmgr.actions.branch.drop_branch import drop_branch
from pkgmgr.core.git import GitError

View File

@@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import patch
from pkgmgr.actions.branch.open_branch import open_branch

View File

@@ -1,6 +1,5 @@
# tests/unit/pkgmgr/test_capabilities.py
import os
import unittest
from unittest.mock import patch, mock_open

View File

@@ -0,0 +1,79 @@
import unittest
from unittest.mock import patch
from pkgmgr.actions.repository.deinstall import deinstall_repos
class TestDeinstallRepos(unittest.TestCase):
def test_preview_removes_nothing_but_runs_make_if_makefile_exists(self):
repo = {"provider": "github.com", "account": "alice", "repository": "demo", "alias": "demo"}
selected = [repo]
with patch("pkgmgr.actions.repository.deinstall.get_repo_identifier", return_value="demo"), \
patch("pkgmgr.actions.repository.deinstall.get_repo_dir", return_value="/repos/github.com/alice/demo"), \
patch("pkgmgr.actions.repository.deinstall.os.path.expanduser", return_value="/home/u/.local/bin"), \
patch("pkgmgr.actions.repository.deinstall.os.path.exists") as mock_exists, \
patch("pkgmgr.actions.repository.deinstall.os.remove") as mock_remove, \
patch("pkgmgr.actions.repository.deinstall.run_command") as mock_run, \
patch("builtins.input", return_value="y"):
# alias exists, Makefile exists
def exists_side_effect(path):
if path == "/home/u/.local/bin/demo":
return True
if path == "/repos/github.com/alice/demo/Makefile":
return True
return False
mock_exists.side_effect = exists_side_effect
deinstall_repos(
selected_repos=selected,
repositories_base_dir="/repos",
bin_dir="~/.local/bin",
all_repos=selected,
preview=True,
)
# Preview: do not remove
mock_remove.assert_not_called()
# But still "would run" make deinstall via run_command (preview=True)
mock_run.assert_called_once_with(
"make deinstall",
cwd="/repos/github.com/alice/demo",
preview=True,
)
def test_non_preview_removes_alias_when_confirmed(self):
repo = {"provider": "github.com", "account": "alice", "repository": "demo", "alias": "demo"}
selected = [repo]
with patch("pkgmgr.actions.repository.deinstall.get_repo_identifier", return_value="demo"), \
patch("pkgmgr.actions.repository.deinstall.get_repo_dir", return_value="/repos/github.com/alice/demo"), \
patch("pkgmgr.actions.repository.deinstall.os.path.expanduser", return_value="/home/u/.local/bin"), \
patch("pkgmgr.actions.repository.deinstall.os.path.exists") as mock_exists, \
patch("pkgmgr.actions.repository.deinstall.os.remove") as mock_remove, \
patch("pkgmgr.actions.repository.deinstall.run_command") as mock_run, \
patch("builtins.input", return_value="y"):
# alias exists, Makefile does NOT exist
def exists_side_effect(path):
if path == "/home/u/.local/bin/demo":
return True
if path == "/repos/github.com/alice/demo/Makefile":
return False
return False
mock_exists.side_effect = exists_side_effect
deinstall_repos(
selected_repos=selected,
repositories_base_dir="/repos",
bin_dir="~/.local/bin",
all_repos=selected,
preview=False,
)
mock_remove.assert_called_once_with("/home/u/.local/bin/demo")
mock_run.assert_not_called()

View File

@@ -24,12 +24,11 @@ Goals:
from __future__ import annotations
import io
import sys
import unittest
from contextlib import redirect_stdout
from types import SimpleNamespace
from typing import Any, Dict, List
from unittest.mock import MagicMock, patch
from unittest.mock import patch
from pkgmgr.cli.context import CLIContext
from pkgmgr.cli.commands.repos import handle_repos_command

View File

@@ -0,0 +1,47 @@
import unittest
from unittest.mock import patch
import pkgmgr.core.command.run as run_mod
class TestRunCommand(unittest.TestCase):
def test_preview_returns_success_without_running(self) -> None:
with patch.object(run_mod.subprocess, "Popen") as popen_mock:
result = run_mod.run_command("echo hi", cwd="/tmp", preview=True)
self.assertEqual(result.returncode, 0)
popen_mock.assert_not_called()
def test_success_streams_and_returns_completed_process(self) -> None:
cmd = ["python3", "-c", "print('out'); import sys; print('err', file=sys.stderr)"]
with patch.object(run_mod.sys, "exit") as exit_mock:
result = run_mod.run_command(cmd, allow_failure=False)
self.assertEqual(result.returncode, 0)
self.assertIn("out", result.stdout)
self.assertIn("err", result.stderr)
exit_mock.assert_not_called()
def test_failure_exits_when_not_allowed(self) -> None:
cmd = ["python3", "-c", "import sys; print('oops', file=sys.stderr); sys.exit(2)"]
with patch.object(run_mod.sys, "exit", side_effect=SystemExit(2)) as exit_mock:
with self.assertRaises(SystemExit) as ctx:
run_mod.run_command(cmd, allow_failure=False)
self.assertEqual(ctx.exception.code, 2)
exit_mock.assert_called_once_with(2)
def test_failure_does_not_exit_when_allowed(self) -> None:
cmd = ["python3", "-c", "import sys; print('oops', file=sys.stderr); sys.exit(3)"]
with patch.object(run_mod.sys, "exit") as exit_mock:
result = run_mod.run_command(cmd, allow_failure=True)
self.assertEqual(result.returncode, 3)
self.assertIn("oops", result.stderr)
exit_mock.assert_not_called()
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,26 @@
import unittest
from unittest.mock import patch
from pkgmgr.core.repository.dir import get_repo_dir
class TestGetRepoDir(unittest.TestCase):
def test_builds_path_with_expanded_base_dir(self):
repo = {"provider": "github.com", "account": "alice", "repository": "demo"}
with patch("pkgmgr.core.repository.dir.os.path.expanduser", return_value="/home/u/repos"):
result = get_repo_dir("~/repos", repo)
self.assertEqual(result, "/home/u/repos/github.com/alice/demo")
def test_exits_with_code_3_if_base_dir_is_none(self):
repo = {"provider": "github.com", "account": "alice", "repository": "demo"}
with self.assertRaises(SystemExit) as ctx:
get_repo_dir(None, repo) # type: ignore[arg-type]
self.assertEqual(ctx.exception.code, 3)
def test_exits_with_code_3_if_repo_is_invalid_type(self):
with self.assertRaises(SystemExit) as ctx:
get_repo_dir("/repos", None) # type: ignore[arg-type]
self.assertEqual(ctx.exception.code, 3)

View File

@@ -1,166 +0,0 @@
# tests/unit/pkgmgr/test_resolve_command.py
import unittest
from unittest.mock import patch
import pkgmgr.core.command.resolve as resolve_command_module
class TestResolveCommandForRepo(unittest.TestCase):
def test_explicit_command_wins(self):
repo = {"command": "/custom/cmd"}
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, "/custom/cmd")
@patch("pkgmgr.core.command.resolve.shutil.which", return_value="/usr/bin/tool")
def test_system_binary_returns_none_and_no_error(self, mock_which):
repo = {}
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
# System binary → no link
self.assertIsNone(result)
@patch("pkgmgr.core.command.resolve.os.access")
@patch("pkgmgr.core.command.resolve.os.path.exists")
@patch("pkgmgr.core.command.resolve.shutil.which", return_value=None)
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/fakehome")
def test_nix_profile_binary(
self,
mock_expanduser,
mock_which,
mock_exists,
mock_access,
):
"""
No system/PATH binary, but a Nix profile binary exists:
→ must return the Nix binary path.
"""
repo = {}
fake_home = "/fakehome"
nix_path = f"{fake_home}/.nix-profile/bin/tool"
def fake_exists(path):
# Only the Nix binary exists
return path == nix_path
def fake_access(path, mode):
# Only the Nix binary is executable
return path == nix_path
mock_exists.side_effect = fake_exists
mock_access.side_effect = fake_access
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, nix_path)
@patch("pkgmgr.core.command.resolve.os.access")
@patch("pkgmgr.core.command.resolve.os.path.exists")
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/home/user")
@patch("pkgmgr.core.command.resolve.shutil.which", return_value="/home/user/.local/bin/tool")
def test_non_system_binary_on_path(
self,
mock_which,
mock_expanduser,
mock_exists,
mock_access,
):
"""
No system (/usr) binary and no Nix binary, but a non-system
PATH binary exists (e.g. venv or ~/.local/bin):
→ must return that PATH binary.
"""
repo = {}
non_system_path = "/home/user/.local/bin/tool"
nix_candidate = "/home/user/.nix-profile/bin/tool"
def fake_exists(path):
# Only the non-system PATH binary "exists".
return path == non_system_path
def fake_access(path, mode):
# Only the non-system PATH binary is executable.
return path == non_system_path
mock_exists.side_effect = fake_exists
mock_access.side_effect = fake_access
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, non_system_path)
@patch("pkgmgr.core.command.resolve.os.access")
@patch("pkgmgr.core.command.resolve.os.path.exists")
@patch("pkgmgr.core.command.resolve.shutil.which", return_value=None)
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/fakehome")
def test_fallback_to_main_py(
self,
mock_expanduser,
mock_which,
mock_exists,
mock_access,
):
"""
No system/non-system PATH binary, no Nix binary, but main.py exists:
→ must fall back to main.py in the repo.
"""
repo = {}
main_py = "/repos/tool/main.py"
def fake_exists(path):
return path == main_py
def fake_access(path, mode):
return path == main_py
mock_exists.side_effect = fake_exists
mock_access.side_effect = fake_access
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, main_py)
@patch("pkgmgr.core.command.resolve.os.access", return_value=False)
@patch("pkgmgr.core.command.resolve.os.path.exists", return_value=False)
@patch("pkgmgr.core.command.resolve.shutil.which", return_value=None)
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/fakehome")
def test_no_command_results_in_system_exit(
self,
mock_expanduser,
mock_which,
mock_exists,
mock_access,
):
"""
Nothing available at any layer:
→ must raise SystemExit with a descriptive error message.
"""
repo = {}
with self.assertRaises(SystemExit) as cm:
resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
msg = str(cm.exception)
self.assertIn("No executable command could be resolved for repository 'tool'", msg)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,77 @@
import unittest
from unittest.mock import patch
from pkgmgr.core.repository.resolve import resolve_repos
class TestResolveRepos(unittest.TestCase):
def setUp(self) -> None:
# Two repos share the same repository name "common" to test uniqueness logic
self.repos = [
{
"provider": "github.com",
"account": "alice",
"repository": "demo",
"alias": "d",
},
{
"provider": "github.com",
"account": "bob",
"repository": "common",
"alias": "c1",
},
{
"provider": "gitlab.com",
"account": "carol",
"repository": "common",
"alias": "c2",
},
]
def test_matches_full_identifier(self):
result = resolve_repos(["github.com/alice/demo"], self.repos)
self.assertEqual(result, [self.repos[0]])
def test_matches_alias(self):
result = resolve_repos(["d"], self.repos)
self.assertEqual(result, [self.repos[0]])
def test_matches_unique_repository_name_only_if_unique(self):
# "demo" is unique -> match
result = resolve_repos(["demo"], self.repos)
self.assertEqual(result, [self.repos[0]])
# "common" is NOT unique -> should not match anything
result2 = resolve_repos(["common"], self.repos)
self.assertEqual(result2, [])
def test_multiple_identifiers_accumulate_matches_in_order(self):
result = resolve_repos(["d", "github.com/bob/common"], self.repos)
self.assertEqual(result, [self.repos[0], self.repos[1]])
def test_unknown_identifier_prints_message(self):
with patch("builtins.print") as mock_print:
result = resolve_repos(["does-not-exist"], self.repos)
self.assertEqual(result, [])
mock_print.assert_called_with(
"Identifier 'does-not-exist' did not match any repository in config."
)
def test_duplicate_identifiers_return_duplicates(self):
# Current behavior: duplicates are not de-duplicated
result = resolve_repos(["d", "d"], self.repos)
self.assertEqual(result, [self.repos[0], self.repos[0]])
def test_empty_identifiers_returns_empty_list(self):
result = resolve_repos([], self.repos)
self.assertEqual(result, [])
def test_empty_repo_list_returns_empty_list_and_prints(self):
with patch("builtins.print") as mock_print:
result = resolve_repos(["github.com/alice/demo"], [])
self.assertEqual(result, [])
mock_print.assert_called_with(
"Identifier 'github.com/alice/demo' did not match any repository in config."
)

View File

@@ -79,7 +79,6 @@ class TestCreateInk(unittest.TestCase):
patch("pkgmgr.core.command.ink.os.chmod") as mock_chmod, \
patch("pkgmgr.core.command.ink.os.path.exists", return_value=False), \
patch("pkgmgr.core.command.ink.os.path.islink", return_value=False), \
patch("pkgmgr.core.command.ink.os.remove") as mock_remove, \
patch("pkgmgr.core.command.ink.os.path.realpath", side_effect=lambda p: p):
create_ink_module.create_ink(
repo=repo,