Compare commits

..

7 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
955028288f Release version 1.8.4
Some checks are pending
Mark stable commit / test-unit (push) Waiting to run
Mark stable commit / test-integration (push) Waiting to run
Mark stable commit / test-env-virtual (push) Waiting to run
Mark stable commit / test-env-nix (push) Waiting to run
Mark stable commit / test-e2e (push) Waiting to run
Mark stable commit / test-virgin-user (push) Waiting to run
Mark stable commit / test-virgin-root (push) Waiting to run
Mark stable commit / lint-shell (push) Waiting to run
Mark stable commit / lint-python (push) Waiting to run
Mark stable commit / mark-stable (push) Blocked by required conditions
2025-12-17 11:20:16 +01:00
Kevin Veen-Birkenbach
866572e252 ci(docker): fix repo mount path for pkgmgr as base layer of Infinito.Nexus
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 / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Standardize Docker/CI/test environments to mount pkgmgr at /opt/src/pkgmgr.
This makes the layering explicit: pkgmgr is the lower-level foundation used by
Infinito.Nexus.

Infra-only change (Docker, CI, shell scripts). No runtime or Nix semantics changed.

https://chatgpt.com/share/69427fe7-e288-800f-90a4-c1c3c11a8484
2025-12-17 11:03:02 +01:00
Kevin Veen-Birkenbach
b0a733369e Optimized output for debugging
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 / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-17 10:51:56 +01:00
Kevin Veen-Birkenbach
c5843ccd30 Release version 1.8.3
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 / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-16 19:49:51 +01:00
Kevin Veen-Birkenbach
3cb7852cb4 feat(mirrors): support URL-only MIRRORS entries and keep git config clean
- Allow MIRRORS to contain plain URLs (one per line) in addition to legacy "NAME URL"
- Treat strings as single URLs to avoid iterable pitfalls
- Write PyPI URLs as metadata-only entries (never added to git config)
- Keep MIRRORS as the single source of truth for mirror setup
- Update integration test to assert URL-only MIRRORS output

https://chatgpt.com/share/6941a9aa-b8b4-800f-963d-2486b34856b1
2025-12-16 19:49:09 +01:00
Kevin Veen-Birkenbach
f995e3d368 Release version 1.8.2
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 / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-16 19:22:41 +01:00
Kevin Veen-Birkenbach
ffa9d9660a gpt-5.2 ChatGPT: refactor tools code into cli.tools.vscode and add unit tests
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 / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
* Move VS Code workspace logic (incl. guards) from cli.commands.tools into cli.tools.vscode
* Extract shared repo path resolution into cli.tools.paths and reuse for explore/terminal
* Simplify cli.commands.tools to pure orchestration via open_vscode_workspace
* Update existing tools command unit test to assert delegation instead of patching removed internals
* Add new unit tests for cli.tools.paths and cli.tools.vscode (workspace creation, reuse, guard errors)

https://chatgpt.com/share/69419a6a-c9e4-800f-9538-b6652b2da6b3
2025-12-16 18:43:56 +01:00
26 changed files with 473 additions and 237 deletions

View File

@@ -31,15 +31,15 @@ jobs:
set -euo pipefail set -euo pipefail
docker run --rm \ docker run --rm \
-v "$PWD":/src \ -v "$PWD":/opt/src/pkgmgr \
-v pkgmgr_repos:/root/Repositories \ -v pkgmgr_repos:/root/Repositories \
-v pkgmgr_pip_cache:/root/.cache/pip \ -v pkgmgr_pip_cache:/root/.cache/pip \
-w /src \ -w /opt/src/pkgmgr \
"pkgmgr-${{ matrix.distro }}-virgin" \ "pkgmgr-${{ matrix.distro }}-virgin" \
bash -lc ' bash -lc '
set -euo pipefail set -euo pipefail
git config --global --add safe.directory /src git config --global --add safe.directory /opt/src/pkgmgr
make install make install
make setup make setup
@@ -50,5 +50,5 @@ jobs:
pkgmgr version pkgmgr pkgmgr version pkgmgr
echo ">>> Running Nix-based: nix run .#pkgmgr -- version pkgmgr" echo ">>> Running Nix-based: nix run .#pkgmgr -- version pkgmgr"
nix run /src#pkgmgr -- version pkgmgr nix run /opt/src/pkgmgr#pkgmgr -- version pkgmgr
' '

View File

@@ -31,8 +31,8 @@ jobs:
set -euo pipefail set -euo pipefail
docker run --rm \ docker run --rm \
-v "$PWD":/src \ -v "$PWD":/opt/src/pkgmgr \
-w /src \ -w /opt/src/pkgmgr \
"pkgmgr-${{ matrix.distro }}-virgin" \ "pkgmgr-${{ matrix.distro }}-virgin" \
bash -lc ' bash -lc '
set -euo pipefail set -euo pipefail
@@ -42,7 +42,7 @@ jobs:
useradd -m dev useradd -m dev
echo "dev ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/dev echo "dev ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/dev
chmod 0440 /etc/sudoers.d/dev chmod 0440 /etc/sudoers.d/dev
chown -R dev:dev /src chown -R dev:dev /opt/src/pkgmgr
mkdir -p /nix/store /nix/var/nix /nix/var/log/nix /nix/var/nix/profiles mkdir -p /nix/store /nix/var/nix /nix/var/log/nix /nix/var/nix/profiles
chown -R dev:dev /nix chown -R dev:dev /nix
@@ -51,7 +51,7 @@ jobs:
sudo -H -u dev env HOME=/home/dev PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 bash -lc " sudo -H -u dev env HOME=/home/dev PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 bash -lc "
set -euo pipefail set -euo pipefail
cd /src cd /opt/src/pkgmgr
make setup-venv make setup-venv
. \"\$HOME/.venvs/pkgmgr/bin/activate\" . \"\$HOME/.venvs/pkgmgr/bin/activate\"
@@ -59,6 +59,6 @@ jobs:
pkgmgr version pkgmgr pkgmgr version pkgmgr
export NIX_REMOTE=local export NIX_REMOTE=local
nix run /src#pkgmgr -- version pkgmgr nix run /opt/src/pkgmgr#pkgmgr -- version pkgmgr
" "
' '

View File

@@ -1,3 +1,18 @@
## [1.8.4] - 2025-12-17
* * Made pkgmgrs base-layer role explicit by standardizing the Docker/CI mount path to *`/opt/src/pkgmgr`*.
## [1.8.3] - 2025-12-16
* MIRRORS now supports plain URL entries, ensuring metadata-only sources like PyPI are recorded without ever being added to the Git configuration.
## [1.8.2] - 2025-12-16
* * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.
## [1.8.1] - 2025-12-16 ## [1.8.1] - 2025-12-16
* * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior. * * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior.

View File

@@ -50,6 +50,6 @@ RUN set -euo pipefail; \
# Entry point # Entry point
COPY scripts/docker/entry.sh /usr/local/bin/docker-entry.sh COPY scripts/docker/entry.sh /usr/local/bin/docker-entry.sh
WORKDIR /src WORKDIR /opt/src/pkgmgr
ENTRYPOINT ["/usr/local/bin/docker-entry.sh"] ENTRYPOINT ["/usr/local/bin/docker-entry.sh"]
CMD ["pkgmgr", "--help"] CMD ["pkgmgr", "--help"]

View File

@@ -32,7 +32,7 @@
rec { rec {
pkgmgr = pyPkgs.buildPythonApplication { pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager"; pname = "package-manager";
version = "1.8.1"; version = "1.8.4";
# Use the git repo as source # Use the git repo as source
src = ./.; src = ./.;

View File

@@ -1,7 +1,7 @@
# Maintainer: Kevin Veen-Birkenbach <info@veen.world> # Maintainer: Kevin Veen-Birkenbach <info@veen.world>
pkgname=package-manager pkgname=package-manager
pkgver=1.8.1 pkgver=1.8.4
pkgrel=1 pkgrel=1
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)." pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
arch=('any') arch=('any')

View File

@@ -1,3 +1,21 @@
package-manager (1.8.4-1) unstable; urgency=medium
* * Made pkgmgrs base-layer role explicit by standardizing the Docker/CI mount path to *`/opt/src/pkgmgr`*.
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 17 Dec 2025 11:20:16 +0100
package-manager (1.8.3-1) unstable; urgency=medium
* MIRRORS now supports plain URL entries, ensuring metadata-only sources like PyPI are recorded without ever being added to the Git configuration.
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 16 Dec 2025 19:49:51 +0100
package-manager (1.8.2-1) unstable; urgency=medium
* * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 16 Dec 2025 19:22:41 +0100
package-manager (1.8.1-1) unstable; urgency=medium package-manager (1.8.1-1) unstable; urgency=medium
* * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior. * * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior.

View File

@@ -1,5 +1,5 @@
Name: package-manager Name: package-manager
Version: 1.8.1 Version: 1.8.4
Release: 1%{?dist} Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake Summary: Wrapper that runs Kevin's package-manager via Nix flake
@@ -74,6 +74,15 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/ /usr/lib/package-manager/
%changelog %changelog
* Wed Dec 17 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.4-1
- * Made pkgmgrs base-layer role explicit by standardizing the Docker/CI mount path to *`/opt/src/pkgmgr`*.
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.3-1
- MIRRORS now supports plain URL entries, ensuring metadata-only sources like PyPI are recorded without ever being added to the Git configuration.
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.2-1
- * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.1-1 * Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.1-1
- * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior. - * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior.
* Mirrors are now handled cleanly: only valid Git remotes are used for Git operations, while non-Git URLs (e.g. PyPI) are excluded, preventing broken or confusing repository configs. * Mirrors are now handled cleanly: only valid Git remotes are used for Git operations, while non-Git URLs (e.g. PyPI) are excluded, preventing broken or confusing repository configs.

View File

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

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
echo "[docker] Starting package-manager container" echo "[docker-pkgmgr] Starting package-manager container"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Log distribution info # Log distribution info
@@ -9,19 +9,19 @@ echo "[docker] Starting package-manager container"
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091 # shellcheck disable=SC1091
. /etc/os-release . /etc/os-release
echo "[docker] Detected distro: ${ID:-unknown} (like: ${ID_LIKE:-})" echo "[docker-pkgmgr] Detected distro: ${ID:-unknown} (like: ${ID_LIKE:-})"
fi fi
# Always use /src (mounted from host) as working directory # Always use /opt/src/pkgmgr (mounted from host) as working directory
echo "[docker] Using /src as working directory" echo "[docker-pkgmgr] Using /opt/src/pkgmgr as working directory"
cd /src cd /opt/src/pkgmgr
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# DEV mode: rebuild package-manager from the mounted /src tree # DEV mode: rebuild package-manager from the mounted /opt/src/pkgmgr tree
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [[ "${REINSTALL_PKGMGR:-0}" == "1" ]]; then if [[ "${REINSTALL_PKGMGR:-0}" == "1" ]]; then
echo "[docker] DEV mode enabled (REINSTALL_PKGMGR=1)" echo "[docker-pkgmgr] DEV mode enabled (REINSTALL_PKGMGR=1)"
echo "[docker] Rebuilding package-manager from /src via scripts/installation/package.sh..." echo "[docker-pkgmgr] Rebuilding package-manager from /opt/src/pkgmgr via scripts/installation/package.sh..."
bash scripts/installation/package.sh || exit 1 bash scripts/installation/package.sh || exit 1
fi fi
@@ -29,9 +29,9 @@ fi
# Hand off to pkgmgr or arbitrary command # Hand off to pkgmgr or arbitrary command
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [[ $# -eq 0 ]]; then if [[ $# -eq 0 ]]; then
echo "[docker] No arguments provided. Showing pkgmgr help..." echo "[docker-pkgmgr] No arguments provided. Showing pkgmgr help..."
exec pkgmgr --help exec pkgmgr --help
else else
echo "[docker] Executing command: $*" echo "[docker-pkgmgr] Executing command: $*"
exec "$@" exec "$@"
fi fi

View File

@@ -6,7 +6,7 @@ echo "[arch/package] Building Arch package (makepkg --nodeps) in an isolated bui
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
# We must not build inside /src (mounted repo). Build in /tmp to avoid permission issues. # We must not build inside /opt/src/pkgmgr (mounted repo). Build in /tmp to avoid permission issues.
BUILD_ROOT="/tmp/package-manager-arch-build" BUILD_ROOT="/tmp/package-manager-arch-build"
PKG_SRC_DIR="${PROJECT_ROOT}/packaging/arch" PKG_SRC_DIR="${PROJECT_ROOT}/packaging/arch"
PKG_BUILD_DIR="${BUILD_ROOT}/packaging/arch" PKG_BUILD_DIR="${BUILD_ROOT}/packaging/arch"

View File

@@ -6,12 +6,12 @@ echo ">>> Running E2E tests: $PKGMGR_DISTRO"
echo "============================================================" echo "============================================================"
docker run --rm \ docker run --rm \
-v "$(pwd):/src" \ -v "$(pwd):/opt/src/pkgmgr" \
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \ -v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \ -v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \ -e TEST_PATTERN="${TEST_PATTERN}" \
--workdir /src \ --workdir /opt/src/pkgmgr \
"pkgmgr-${PKGMGR_DISTRO}" \ "pkgmgr-${PKGMGR_DISTRO}" \
bash -lc ' bash -lc '
set -euo pipefail set -euo pipefail
@@ -40,14 +40,14 @@ docker run --rm \
} }
# Mark the mounted repository as safe to avoid Git ownership errors. # Mark the mounted repository as safe to avoid Git ownership errors.
# Newer Git (e.g. on Ubuntu) complains about the gitdir (/src/.git), # Newer Git (e.g. on Ubuntu) complains about the gitdir (/opt/src/pkgmgr/.git),
# older versions about the worktree (/src). Nix turns "." into the # older versions about the worktree (/opt/src/pkgmgr). Nix turns "." into the
# flake input "git+file:///src", which then uses Git under the hood. # flake input "git+file:///opt/src/pkgmgr", which then uses Git under the hood.
if command -v git >/dev/null 2>&1; then if command -v git >/dev/null 2>&1; then
# Worktree path # Worktree path
git config --global --add safe.directory /src || true git config --global --add safe.directory /opt/src/pkgmgr || true
# Gitdir path shown in the "dubious ownership" error # Gitdir path shown in the "dubious ownership" error
git config --global --add safe.directory /src/.git || true git config --global --add safe.directory /opt/src/pkgmgr/.git || true
# Ephemeral CI containers: allow all paths as a last resort # Ephemeral CI containers: allow all paths as a last resort
git config --global --add safe.directory "*" || true git config --global --add safe.directory "*" || true
fi fi
@@ -55,6 +55,6 @@ docker run --rm \
# Run the E2E tests inside the Nix development shell # Run the E2E tests inside the Nix development shell
nix develop .#default --no-write-lock-file -c \ nix develop .#default --no-write-lock-file -c \
python3 -m unittest discover \ python3 -m unittest discover \
-s /src/tests/e2e \ -s /opt/src/pkgmgr/tests/e2e \
-p "$TEST_PATTERN" -p "$TEST_PATTERN"
' '

View File

@@ -9,18 +9,18 @@ echo ">>> Image: ${IMAGE}"
echo "============================================================" echo "============================================================"
docker run --rm \ docker run --rm \
-v "$(pwd):/src" \ -v "$(pwd):/opt/src/pkgmgr" \
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \ -v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \ -v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
--workdir /src \ --workdir /opt/src/pkgmgr \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
"${IMAGE}" \ "${IMAGE}" \
bash -lc ' bash -lc '
set -euo pipefail set -euo pipefail
if command -v git >/dev/null 2>&1; then if command -v git >/dev/null 2>&1; then
git config --global --add safe.directory /src || true git config --global --add safe.directory /opt/src/pkgmgr || true
git config --global --add safe.directory /src/.git || true git config --global --add safe.directory /opt/src/pkgmgr/.git || true
git config --global --add safe.directory "*" || true git config --global --add safe.directory "*" || true
fi fi
@@ -38,9 +38,9 @@ docker run --rm \
# ------------------------------------------------------------ # ------------------------------------------------------------
# Retry helper for GitHub API rate-limit (HTTP 403) # Retry helper for GitHub API rate-limit (HTTP 403)
# ------------------------------------------------------------ # ------------------------------------------------------------
if [[ -f /src/scripts/nix/lib/retry_403.sh ]]; then if [[ -f /opt/src/pkgmgr/scripts/nix/lib/retry_403.sh ]]; then
# shellcheck source=./scripts/nix/lib/retry_403.sh # shellcheck source=./scripts/nix/lib/retry_403.sh
source /src/scripts/nix/lib/retry_403.sh source /opt/src/pkgmgr/scripts/nix/lib/retry_403.sh
elif [[ -f ./scripts/nix/lib/retry_403.sh ]]; then elif [[ -f ./scripts/nix/lib/retry_403.sh ]]; then
# shellcheck source=./scripts/nix/lib/retry_403.sh # shellcheck source=./scripts/nix/lib/retry_403.sh
source ./scripts/nix/lib/retry_403.sh source ./scripts/nix/lib/retry_403.sh

View File

@@ -17,8 +17,8 @@ echo
# ------------------------------------------------------------ # ------------------------------------------------------------
if OUTPUT=$(docker run --rm \ if OUTPUT=$(docker run --rm \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
-v "$(pwd):/src" \ -v "$(pwd):/opt/src/pkgmgr" \
-w /src \ -w /opt/src/pkgmgr \
"${IMAGE}" \ "${IMAGE}" \
bash -lc ' bash -lc '
set -euo pipefail set -euo pipefail

View File

@@ -6,19 +6,19 @@ echo ">>> Running INTEGRATION tests in ${PKGMGR_DISTRO} container"
echo "============================================================" echo "============================================================"
docker run --rm \ docker run --rm \
-v "$(pwd):/src" \ -v "$(pwd):/opt/src/pkgmgr" \
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \ -v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \ -v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
--workdir /src \ --workdir /opt/src/pkgmgr \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \ -e TEST_PATTERN="${TEST_PATTERN}" \
"pkgmgr-${PKGMGR_DISTRO}" \ "pkgmgr-${PKGMGR_DISTRO}" \
bash -lc ' bash -lc '
set -e; set -e;
git config --global --add safe.directory /src || true; git config --global --add safe.directory /opt/src/pkgmgr || true;
nix develop .#default --no-write-lock-file -c \ nix develop .#default --no-write-lock-file -c \
python3 -m unittest discover \ python3 -m unittest discover \
-s tests/integration \ -s tests/integration \
-t /src \ -t /opt/src/pkgmgr \
-p "$TEST_PATTERN"; -p "$TEST_PATTERN";
' '

View File

@@ -6,19 +6,19 @@ echo ">>> Running UNIT tests in ${PKGMGR_DISTRO} container"
echo "============================================================" echo "============================================================"
docker run --rm \ docker run --rm \
-v "$(pwd):/src" \ -v "$(pwd):/opt/src/pkgmgr" \
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \ -v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \ -v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
--workdir /src \ --workdir /opt/src/pkgmgr \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \ -e TEST_PATTERN="${TEST_PATTERN}" \
"pkgmgr-${PKGMGR_DISTRO}" \ "pkgmgr-${PKGMGR_DISTRO}" \
bash -lc ' bash -lc '
set -e; set -e;
git config --global --add safe.directory /src || true; git config --global --add safe.directory /opt/src/pkgmgr || true;
nix develop .#default --no-write-lock-file -c \ nix develop .#default --no-write-lock-file -c \
python3 -m unittest discover \ python3 -m unittest discover \
-s tests/unit \ -s tests/unit \
-t /src \ -t /opt/src/pkgmgr \
-p "$TEST_PATTERN"; -p "$TEST_PATTERN";
' '

View File

@@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
import os import os
from collections.abc import Iterable, Mapping
from typing import Union
from urllib.parse import urlparse from urllib.parse import urlparse
from typing import Mapping
from .types import MirrorMap, Repository from .types import MirrorMap, Repository
@@ -32,7 +33,7 @@ def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
""" """
Supports: Supports:
NAME URL NAME URL
URL auto name = hostname URL -> auto-generate name from hostname
""" """
path = os.path.join(repo_dir, filename) path = os.path.join(repo_dir, filename)
mirrors: MirrorMap = {} mirrors: MirrorMap = {}
@@ -52,7 +53,8 @@ def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
# Case 1: "name url" # Case 1: "name url"
if len(parts) == 2: if len(parts) == 2:
name, url = parts name, url = parts
# Case 2: "url" → auto-generate name
# Case 2: "url" -> auto name
elif len(parts) == 1: elif len(parts) == 1:
url = parts[0] url = parts[0]
parsed = urlparse(url) parsed = urlparse(url)
@@ -67,21 +69,56 @@ def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
continue continue
mirrors[name] = url mirrors[name] = url
except OSError as exc: except OSError as exc:
print(f"[WARN] Could not read MIRRORS file at {path}: {exc}") print(f"[WARN] Could not read MIRRORS file at {path}: {exc}")
return mirrors return mirrors
MirrorsInput = Union[Mapping[str, str], Iterable[str]]
def write_mirrors_file( def write_mirrors_file(
repo_dir: str, repo_dir: str,
mirrors: Mapping[str, str], mirrors: MirrorsInput,
filename: str = "MIRRORS", filename: str = "MIRRORS",
preview: bool = False, preview: bool = False,
) -> None: ) -> None:
"""
Write MIRRORS in one of two formats:
1) Mapping[str, str] -> "NAME URL" per line (legacy / compatible)
2) Iterable[str] -> "URL" per line (new preferred)
Strings are treated as a single URL (not iterated character-by-character).
"""
path = os.path.join(repo_dir, filename) path = os.path.join(repo_dir, filename)
lines = [f"{name} {url}" for name, url in sorted(mirrors.items())]
lines: list[str]
if isinstance(mirrors, Mapping):
items = [
(str(name), str(url))
for name, url in mirrors.items()
if url is not None and str(url).strip()
]
items.sort(key=lambda x: (x[0], x[1]))
lines = [f"{name} {url}" for name, url in items]
else:
if isinstance(mirrors, (str, bytes)):
urls = [str(mirrors).strip()]
else:
urls = [
str(url).strip()
for url in mirrors
if url is not None and str(url).strip()
]
urls = sorted(set(urls))
lines = urls
content = "\n".join(lines) + ("\n" if lines else "") content = "\n".join(lines) + ("\n" if lines else "")
if preview: if preview:
@@ -94,5 +131,6 @@ def write_mirrors_file(
with open(path, "w", encoding="utf-8") as fh: with open(path, "w", encoding="utf-8") as fh:
fh.write(content) fh.write(content)
print(f"[INFO] Wrote MIRRORS file at {path}") print(f"[INFO] Wrote MIRRORS file at {path}")
except OSError as exc: except OSError as exc:
print(f"[ERROR] Failed to write MIRRORS file at {path}: {exc}") print(f"[ERROR] Failed to write MIRRORS file at {path}: {exc}")

View File

@@ -12,8 +12,8 @@ class MirrorBootstrapper:
""" """
MIRRORS is the single source of truth. MIRRORS is the single source of truth.
We write defaults to MIRRORS and then call mirror setup which will Defaults are written to MIRRORS and mirror setup derives
configure git remotes based on MIRRORS content (but only for git URLs). git remotes exclusively from that file (git URLs only).
""" """
def write_defaults( def write_defaults(
@@ -25,10 +25,8 @@ class MirrorBootstrapper:
preview: bool, preview: bool,
) -> None: ) -> None:
mirrors = { mirrors = {
# preferred SSH url is supplied by CreateRepoPlanner.primary_remote primary,
"origin": primary, f"https://pypi.org/project/{name}/",
# metadata only: must NEVER be configured as a git remote
"pypi": f"https://pypi.org/project/{name}/",
} }
write_mirrors_file(repo_dir, mirrors, preview=preview) write_mirrors_file(repo_dir, mirrors, preview=preview)
@@ -41,7 +39,8 @@ class MirrorBootstrapper:
preview: bool, preview: bool,
remote: bool, remote: bool,
) -> None: ) -> None:
# IMPORTANT: do NOT set repo["mirrors"] here. # IMPORTANT:
# Do NOT set repo["mirrors"] here.
# MIRRORS file is the single source of truth. # MIRRORS file is the single source of truth.
setup_mirrors( setup_mirrors(
selected_repos=[repo], selected_repos=[repo],

View File

@@ -1,64 +1,27 @@
from __future__ import annotations from __future__ import annotations
import json
import os
from typing import Any, Dict, List from typing import Any, Dict, List
from pkgmgr.cli.context import CLIContext from pkgmgr.cli.context import CLIContext
from pkgmgr.cli.tools import open_vscode_workspace
from pkgmgr.cli.tools.paths import resolve_repository_path
from pkgmgr.core.command.run import run_command from pkgmgr.core.command.run import run_command
from pkgmgr .core .repository .identifier import get_repo_identifier
from pkgmgr .core .repository .dir import get_repo_dir
Repository = Dict[str, Any] Repository = Dict[str, Any]
def _resolve_repository_path(repository: Repository, ctx: CLIContext) -> str:
"""
Resolve the filesystem path for a repository.
Priority:
1. Use explicit keys if present (directory / path / workspace / workspace_dir).
2. Fallback to get_repo_dir(...) using the repositories base directory
from the CLI context.
"""
# 1) Explicit path-like keys on the repository object
for key in ("directory", "path", "workspace", "workspace_dir"):
value = repository.get(key)
if value:
return value
# 2) Fallback: compute from base dir + repository metadata
base_dir = (
getattr(ctx, "repositories_base_dir", None)
or getattr(ctx, "repositories_dir", None)
)
if not base_dir:
raise RuntimeError(
"Cannot resolve repositories base directory from context; "
"expected ctx.repositories_base_dir or ctx.repositories_dir."
)
return get_repo_dir(base_dir, repository)
def handle_tools_command( def handle_tools_command(
args, args,
ctx: CLIContext, ctx: CLIContext,
selected: List[Repository], selected: List[Repository],
) -> None: ) -> None:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# nautilus "explore" command # nautilus "explore" command
# ------------------------------------------------------------------ # ------------------------------------------------------------------
if args.command == "explore": if args.command == "explore":
for repository in selected: for repository in selected:
repo_path = _resolve_repository_path(repository, ctx) repo_path = resolve_repository_path(repository, ctx)
run_command( run_command(f'nautilus "{repo_path}" & disown')
f'nautilus "{repo_path}" & disown'
)
return return
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -66,50 +29,13 @@ def handle_tools_command(
# ------------------------------------------------------------------ # ------------------------------------------------------------------
if args.command == "terminal": if args.command == "terminal":
for repository in selected: for repository in selected:
repo_path = _resolve_repository_path(repository, ctx) repo_path = resolve_repository_path(repository, ctx)
run_command( run_command(f'gnome-terminal --tab --working-directory="{repo_path}"')
f'gnome-terminal --tab --working-directory="{repo_path}"'
)
return return
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# VS Code workspace command # VS Code workspace command
# ------------------------------------------------------------------ # ------------------------------------------------------------------
if args.command == "code": if args.command == "code":
if not selected: open_vscode_workspace(ctx, selected)
print("No repositories selected.")
return
identifiers = [
get_repo_identifier(repo, ctx.all_repositories)
for repo in selected
]
sorted_identifiers = sorted(identifiers)
workspace_name = "_".join(sorted_identifiers) + ".code-workspace"
directories_cfg = ctx.config_merged.get("directories") or {}
workspaces_dir = os.path.expanduser(
directories_cfg.get("workspaces", "~/Workspaces")
)
os.makedirs(workspaces_dir, exist_ok=True)
workspace_file = os.path.join(workspaces_dir, workspace_name)
folders = [
{"path": _resolve_repository_path(repository, ctx)}
for repository in selected
]
workspace_data = {
"folders": folders,
"settings": {},
}
if not os.path.exists(workspace_file):
with open(workspace_file, "w", encoding="utf-8") as f:
json.dump(workspace_data, f, indent=4)
print(f"Created workspace file: {workspace_file}")
else:
print(f"Using existing workspace file: {workspace_file}")
run_command(f'code "{workspace_file}"')
return return

View File

@@ -0,0 +1,5 @@
from __future__ import annotations
from .vscode import open_vscode_workspace
__all__ = ["open_vscode_workspace"]

View File

@@ -0,0 +1,35 @@
from __future__ import annotations
from typing import Any, Dict
from pkgmgr.cli.context import CLIContext
from pkgmgr.core.repository.dir import get_repo_dir
Repository = Dict[str, Any]
def resolve_repository_path(repository: Repository, ctx: CLIContext) -> str:
"""
Resolve the filesystem path for a repository.
Priority:
1. Use explicit keys if present (directory / path / workspace / workspace_dir).
2. Fallback to get_repo_dir(...) using the repositories base directory
from the CLI context.
"""
for key in ("directory", "path", "workspace", "workspace_dir"):
value = repository.get(key)
if value:
return value
base_dir = (
getattr(ctx, "repositories_base_dir", None)
or getattr(ctx, "repositories_dir", None)
)
if not base_dir:
raise RuntimeError(
"Cannot resolve repositories base directory from context; "
"expected ctx.repositories_base_dir or ctx.repositories_dir."
)
return get_repo_dir(base_dir, repository)

View File

@@ -0,0 +1,102 @@
from __future__ import annotations
import json
import os
import shutil
from typing import Any, Dict, List
from pkgmgr.cli.context import CLIContext
from pkgmgr.cli.tools.paths import resolve_repository_path
from pkgmgr.core.command.run import run_command
from pkgmgr.core.repository.identifier import get_repo_identifier
Repository = Dict[str, Any]
def _ensure_vscode_cli_available() -> None:
"""
Ensure that the VS Code CLI ('code') is available in PATH.
"""
if shutil.which("code") is None:
raise RuntimeError(
"VS Code CLI ('code') not found in PATH.\n\n"
"Hint:\n"
" Install Visual Studio Code and ensure the 'code' command is available.\n"
" VS Code → Command Palette → 'Shell Command: Install code command in PATH'\n"
)
def _ensure_identifiers_are_filename_safe(identifiers: List[str]) -> None:
"""
Ensure identifiers can be used in a filename.
If an identifier contains '/', it likely means the repository has not yet
been explicitly identified (no short identifier configured).
"""
invalid = [i for i in identifiers if "/" in i or os.sep in i]
if invalid:
raise RuntimeError(
"Cannot create VS Code workspace.\n\n"
"The following repositories are not yet identified "
"(identifier contains '/'): \n"
+ "\n".join(f" - {i}" for i in invalid)
+ "\n\n"
"Hint:\n"
" The repository has no short identifier yet.\n"
" Add an explicit identifier in your configuration before using `pkgmgr tools code`.\n"
)
def _resolve_workspaces_dir(ctx: CLIContext) -> str:
directories_cfg = ctx.config_merged.get("directories") or {}
return os.path.expanduser(directories_cfg.get("workspaces", "~/Workspaces"))
def _build_workspace_filename(identifiers: List[str]) -> str:
sorted_identifiers = sorted(identifiers)
return "_".join(sorted_identifiers) + ".code-workspace"
def _build_workspace_data(selected: List[Repository], ctx: CLIContext) -> Dict[str, Any]:
folders = [{"path": resolve_repository_path(repo, ctx)} for repo in selected]
return {
"folders": folders,
"settings": {},
}
def open_vscode_workspace(ctx: CLIContext, selected: List[Repository]) -> None:
"""
Create (if missing) and open a VS Code workspace for the selected repositories.
Policy:
- Fail with a clear error if VS Code CLI is missing.
- Fail with a clear error if any repository identifier contains '/', because that
indicates the repo has not been explicitly identified (no short identifier).
- Do NOT auto-sanitize identifiers and do NOT create subfolders under workspaces.
"""
if not selected:
print("No repositories selected.")
return
_ensure_vscode_cli_available()
identifiers = [get_repo_identifier(repo, ctx.all_repositories) for repo in selected]
_ensure_identifiers_are_filename_safe(identifiers)
workspaces_dir = _resolve_workspaces_dir(ctx)
os.makedirs(workspaces_dir, exist_ok=True)
workspace_name = _build_workspace_filename(identifiers)
workspace_file = os.path.join(workspaces_dir, workspace_name)
workspace_data = _build_workspace_data(selected, ctx)
if not os.path.exists(workspace_file):
with open(workspace_file, "w", encoding="utf-8") as f:
json.dump(workspace_data, f, indent=4)
print(f"Created workspace file: {workspace_file}")
else:
print(f"Using existing workspace file: {workspace_file}")
run_command(f'code "{workspace_file}"')

View File

@@ -75,12 +75,12 @@ class TestCreateRepoPypiNotInGitConfig(unittest.TestCase):
mirrors_content = mirrors_file.read_text(encoding="utf-8") mirrors_content = mirrors_file.read_text(encoding="utf-8")
self.assertIn( self.assertIn(
"pypi https://pypi.org/project/repo/", "https://pypi.org/project/repo/",
mirrors_content, mirrors_content,
"PyPI mirror entry must exist in MIRRORS", "PyPI mirror entry must exist in MIRRORS",
) )
self.assertIn( self.assertIn(
"origin git@github.com:acme/repo.git", "git@github.com:acme/repo.git",
mirrors_content, mirrors_content,
"origin SSH URL must exist in MIRRORS", "origin SSH URL must exist in MIRRORS",
) )

View File

@@ -1,11 +1,9 @@
from __future__ import annotations from __future__ import annotations
import json
import os
import tempfile
import unittest import unittest
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any, Dict, List from typing import Any, Dict, List
from unittest.mock import call, patch
from pkgmgr.cli.commands.tools import handle_tools_command from pkgmgr.cli.commands.tools import handle_tools_command
@@ -26,36 +24,23 @@ class TestHandleToolsCommand(unittest.TestCase):
Unit tests for pkgmgr.cli.commands.tools.handle_tools_command. Unit tests for pkgmgr.cli.commands.tools.handle_tools_command.
We focus on: We focus on:
- Correct path resolution for repositories that have a 'directory' key. - Correct path resolution and shell commands for 'explore' and 'terminal'.
- Correct shell commands for 'explore' and 'terminal'. - For 'code': delegation to pkgmgr.cli.tools.open_vscode_workspace.
- Proper workspace creation and invocation of 'code' for the 'code' command.
""" """
def setUp(self) -> None: def setUp(self) -> None:
# Two fake repositories with explicit 'directory' entries so that
# _resolve_repository_path() does not need to call get_repo_dir().
self.repos: List[Repository] = [ self.repos: List[Repository] = [
{"alias": "repo1", "directory": "/tmp/repo1"}, {"alias": "repo1", "directory": "/tmp/repo1"},
{"alias": "repo2", "directory": "/tmp/repo2"}, {"alias": "repo2", "directory": "/tmp/repo2"},
] ]
# Minimal CLI context; only attributes used in tools.py are provided.
self.ctx = SimpleNamespace( self.ctx = SimpleNamespace(
config_merged={"directories": {"workspaces": "~/Workspaces"}}, config_merged={"directories": {"workspaces": "~/Workspaces"}},
all_repositories=self.repos, all_repositories=self.repos,
repositories_base_dir="/base/dir", repositories_base_dir="/base/dir",
) )
# ------------------------------------------------------------------ #
# Helper
# ------------------------------------------------------------------ #
def _patch_run_command(self): def _patch_run_command(self):
"""
Convenience context manager for patching run_command in tools module.
"""
from unittest.mock import patch
return patch("pkgmgr.cli.commands.tools.run_command") return patch("pkgmgr.cli.commands.tools.run_command")
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@@ -63,12 +48,6 @@ class TestHandleToolsCommand(unittest.TestCase):
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def test_explore_uses_directory_paths(self) -> None: def test_explore_uses_directory_paths(self) -> None:
"""
The 'explore' command should call Nautilus with the resolved
repository paths and use '& disown' as in the implementation.
"""
from unittest.mock import call
args = _Args(command="explore") args = _Args(command="explore")
with self._patch_run_command() as mock_run_command: with self._patch_run_command() as mock_run_command:
@@ -85,12 +64,6 @@ class TestHandleToolsCommand(unittest.TestCase):
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def test_terminal_uses_directory_paths(self) -> None: def test_terminal_uses_directory_paths(self) -> None:
"""
The 'terminal' command should open a GNOME Terminal tab with the
repository as its working directory.
"""
from unittest.mock import call
args = _Args(command="terminal") args = _Args(command="terminal")
with self._patch_run_command() as mock_run_command: with self._patch_run_command() as mock_run_command:
@@ -106,63 +79,10 @@ class TestHandleToolsCommand(unittest.TestCase):
# Tests for 'code' # Tests for 'code'
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def test_code_creates_workspace_and_calls_code(self) -> None: def test_code_delegates_to_open_vscode_workspace(self) -> None:
"""
The 'code' command should:
- Build a workspace file name from sorted repository identifiers.
- Resolve the repository paths into VS Code 'folders'.
- Create the workspace file if it does not exist.
- Call 'code "<workspace_file>"' via run_command.
"""
from unittest.mock import patch
args = _Args(command="code") args = _Args(command="code")
with tempfile.TemporaryDirectory() as tmpdir: with patch("pkgmgr.cli.commands.tools.open_vscode_workspace") as m:
# Patch expanduser so that the configured '~/Workspaces'
# resolves into our temporary directory.
with patch(
"pkgmgr.cli.commands.tools.os.path.expanduser"
) as mock_expanduser:
mock_expanduser.return_value = tmpdir
# Patch get_repo_identifier so the resulting workspace file
# name is deterministic and easy to assert.
with patch(
"pkgmgr.cli.commands.tools.get_repo_identifier"
) as mock_get_identifier:
mock_get_identifier.side_effect = ["repo-b", "repo-a"]
with self._patch_run_command() as mock_run_command:
handle_tools_command(args, self.ctx, self.repos) handle_tools_command(args, self.ctx, self.repos)
# The identifiers are ['repo-b', 'repo-a'], which are m.assert_called_once_with(self.ctx, self.repos)
# sorted to ['repo-a', 'repo-b'] and joined with '_'.
expected_workspace_name = "repo-a_repo-b.code-workspace"
expected_workspace_file = os.path.join(
tmpdir, expected_workspace_name
)
# Workspace file should have been created.
self.assertTrue(
os.path.exists(expected_workspace_file),
"Workspace file was not created.",
)
# The content of the workspace must be valid JSON with
# the expected folder paths.
with open(expected_workspace_file, "r", encoding="utf-8") as f:
data = json.load(f)
self.assertIn("folders", data)
folder_paths = {f["path"] for f in data["folders"]}
self.assertEqual(
folder_paths,
{"/tmp/repo1", "/tmp/repo2"},
)
# And VS Code must have been invoked with that workspace.
mock_run_command.assert_called_once_with(
f'code "{expected_workspace_file}"'
)

View File

@@ -0,0 +1,38 @@
from __future__ import annotations
import unittest
from types import SimpleNamespace
from unittest.mock import patch
class TestResolveRepositoryPath(unittest.TestCase):
def test_explicit_directory_key_wins(self) -> None:
from pkgmgr.cli.tools.paths import resolve_repository_path
ctx = SimpleNamespace(repositories_base_dir="/base", repositories_dir="/base2")
repo = {"directory": "/explicit/repo"}
self.assertEqual(resolve_repository_path(repo, ctx), "/explicit/repo")
def test_fallback_uses_get_repo_dir_with_repositories_base_dir(self) -> None:
from pkgmgr.cli.tools.paths import resolve_repository_path
ctx = SimpleNamespace(repositories_base_dir="/base", repositories_dir="/base2")
repo = {"provider": "github.com", "account": "acme", "repository": "demo"}
with patch("pkgmgr.cli.tools.paths.get_repo_dir", return_value="/computed/repo") as m:
out = resolve_repository_path(repo, ctx)
self.assertEqual(out, "/computed/repo")
m.assert_called_once_with("/base", repo)
def test_raises_if_no_base_dir_in_context(self) -> None:
from pkgmgr.cli.tools.paths import resolve_repository_path
ctx = SimpleNamespace(repositories_base_dir=None, repositories_dir=None)
repo = {"provider": "github.com", "account": "acme", "repository": "demo"}
with self.assertRaises(RuntimeError) as cm:
resolve_repository_path(repo, ctx)
self.assertIn("Cannot resolve repositories base directory", str(cm.exception))

View File

@@ -0,0 +1,131 @@
from __future__ import annotations
import json
import os
import tempfile
import unittest
from types import SimpleNamespace
from typing import Any, Dict, List
from unittest.mock import patch
Repository = Dict[str, Any]
class TestOpenVSCodeWorkspace(unittest.TestCase):
def test_no_selected_repos_prints_message_and_returns(self) -> None:
from pkgmgr.cli.tools.vscode import open_vscode_workspace
ctx = SimpleNamespace(config_merged={}, all_repositories=[])
with patch("builtins.print") as p:
open_vscode_workspace(ctx, [])
p.assert_called_once()
self.assertIn("No repositories selected.", str(p.call_args[0][0]))
def test_raises_if_code_cli_missing(self) -> None:
from pkgmgr.cli.tools.vscode import open_vscode_workspace
ctx = SimpleNamespace(config_merged={}, all_repositories=[])
selected: List[Repository] = [{"provider": "github.com", "account": "x", "repository": "y"}]
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value=None):
with self.assertRaises(RuntimeError) as cm:
open_vscode_workspace(ctx, selected)
self.assertIn("VS Code CLI ('code') not found", str(cm.exception))
def test_raises_if_identifier_contains_slash(self) -> None:
from pkgmgr.cli.tools.vscode import open_vscode_workspace
ctx = SimpleNamespace(
config_merged={"directories": {"workspaces": "~/Workspaces"}},
all_repositories=[],
)
selected: List[Repository] = [{"provider": "github.com", "account": "x", "repository": "y"}]
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value="/usr/bin/code"), patch(
"pkgmgr.cli.tools.vscode.get_repo_identifier",
return_value="github.com/x/y",
):
with self.assertRaises(RuntimeError) as cm:
open_vscode_workspace(ctx, selected)
msg = str(cm.exception)
self.assertIn("not yet identified", msg)
self.assertIn("identifier contains '/'", msg)
def test_creates_workspace_file_and_calls_code(self) -> None:
from pkgmgr.cli.tools.vscode import open_vscode_workspace
with tempfile.TemporaryDirectory() as tmp:
workspaces_dir = os.path.join(tmp, "Workspaces")
repo_path = os.path.join(tmp, "Repos", "dotlinker")
ctx = SimpleNamespace(
config_merged={"directories": {"workspaces": workspaces_dir}},
all_repositories=[],
repositories_base_dir=os.path.join(tmp, "Repos"),
)
selected: List[Repository] = [
{"provider": "github.com", "account": "kevin", "repository": "dotlinker"}
]
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value="/usr/bin/code"), patch(
"pkgmgr.cli.tools.vscode.get_repo_identifier",
return_value="dotlinker",
), patch(
"pkgmgr.cli.tools.vscode.resolve_repository_path",
return_value=repo_path,
), patch(
"pkgmgr.cli.tools.vscode.run_command"
) as run_cmd:
open_vscode_workspace(ctx, selected)
workspace_file = os.path.join(workspaces_dir, "dotlinker.code-workspace")
self.assertTrue(os.path.exists(workspace_file))
with open(workspace_file, "r", encoding="utf-8") as f:
data = json.load(f)
self.assertEqual(data["folders"], [{"path": repo_path}])
self.assertEqual(data["settings"], {})
run_cmd.assert_called_once_with(f'code "{workspace_file}"')
def test_uses_existing_workspace_file_without_overwriting(self) -> None:
from pkgmgr.cli.tools.vscode import open_vscode_workspace
with tempfile.TemporaryDirectory() as tmp:
workspaces_dir = os.path.join(tmp, "Workspaces")
os.makedirs(workspaces_dir, exist_ok=True)
workspace_file = os.path.join(workspaces_dir, "dotlinker.code-workspace")
original = {"folders": [{"path": "/original"}], "settings": {"x": 1}}
with open(workspace_file, "w", encoding="utf-8") as f:
json.dump(original, f)
ctx = SimpleNamespace(
config_merged={"directories": {"workspaces": workspaces_dir}},
all_repositories=[],
)
selected: List[Repository] = [
{"provider": "github.com", "account": "kevin", "repository": "dotlinker"}
]
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value="/usr/bin/code"), patch(
"pkgmgr.cli.tools.vscode.get_repo_identifier",
return_value="dotlinker",
), patch(
"pkgmgr.cli.tools.vscode.resolve_repository_path",
return_value="/new/path",
), patch(
"pkgmgr.cli.tools.vscode.run_command"
) as run_cmd:
open_vscode_workspace(ctx, selected)
with open(workspace_file, "r", encoding="utf-8") as f:
data = json.load(f)
self.assertEqual(data, original)
run_cmd.assert_called_once_with(f'code "{workspace_file}"')