From dfc218bc11b0a9f071ea65ae0e30e92b4ac013bb Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 21 Oct 2026 11:18:40 +0200 Subject: [PATCH 1/2] Give the desktop app the kcap identity before its launch (#1761) Pack ID, bundle ID and notification ID move off the Capacitor names while no installed copy exists to strand: each of them is what an installed app updates or registers under, so changing one later would orphan every install. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 16 +- .github/workflows/release.yml | 28 ++- README.md | 9 +- scripts/assemble-app-bundle.test.sh | 2 +- scripts/desktop-baseline.test.sh | 4 +- scripts/install-windows-setup.sh | 2 +- scripts/install-windows-setup.test.sh | 8 +- scripts/promote-desktop-aliases.test.sh | 2 +- scripts/verify-desktop-immutables.sh | 4 +- scripts/verify-desktop-immutables.test.sh | 4 +- src/Capacitor.App/App.axaml.cs | 3 +- src/Capacitor.App/Packaging/Info.plist | 2 +- .../Services/ApplicationsInstallPlan.cs | 7 +- .../Services/ApplicationsMover.cs | 35 ++-- .../Services/InstalledAppProcesses.cs | 10 +- .../NativeDesktopNotificationSink.cs | 2 +- .../Services/SingleInstanceGuard.cs | 2 +- .../LocalEdition/LocalClientCopies.cs | 7 +- .../AppStartupTests.cs | 2 +- .../ApplicationIntegrationRefreshTests.cs | 17 +- .../ApplicationReferencesTests.cs | 15 +- .../ApplicationsMoverTests.cs | 168 +++--------------- .../CliResolverTests.cs | 2 +- .../PrereleaseFilteringSourceTests.cs | 2 +- .../SingleInstanceGuardTests.cs | 4 +- .../TerminalAdviceTests.cs | 4 +- .../WindowsDesktopNotificationAccessTests.cs | 4 +- ...aemonCommandsServiceUninstallUnderTests.cs | 16 +- .../Commands/DaemonShimCommandsTests.cs | 2 +- .../Commands/LocalSetupJsonLinesTests.cs | 4 +- .../Commands/PluginCommandClaudeTests.cs | 6 +- .../Commands/PluginCommandCodexTests.cs | 4 +- .../Commands/PluginCommandToolsOnlyTests.cs | 8 +- .../Commands/PluginPathTests.cs | 2 +- .../LocalEdition/LocalClientCopiesTests.cs | 19 +- .../LocalEdition/LocalPreflightTests.cs | 6 +- 36 files changed, 141 insertions(+), 291 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4129b4fee4..79c654d630 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -249,7 +249,7 @@ jobs: - name: Pack (unsigned) shell: bash run: > - vpk pack --packId KurrentCapacitor --packVersion "${{ steps.version.outputs.VERSION }}" + vpk pack --packId kcap --packVersion "${{ steps.version.outputs.VERSION }}" --packTitle "kcap" --packAuthors Kurrent --mainExe "kcap-desktop" --packDir "publish/kcap.app" --channel osx-arm64 --noInst --outputDir releases @@ -259,7 +259,7 @@ jobs: run: | set -euo pipefail mkdir -p extracted - ditto -x -k releases/KurrentCapacitor-osx-arm64-Portable.zip extracted + ditto -x -k releases/kcap-osx-arm64-Portable.zip extracted APP="extracted/kcap.app" bash scripts/assert-app-plugin.sh "$APP" "${{ steps.version.outputs.VERSION }}" bash scripts/assert-app-cli-version.sh "$APP/Contents/MacOS/kcap" "${{ steps.version.outputs.VERSION }}" @@ -290,7 +290,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: app-osx-arm64-unsigned - path: releases/KurrentCapacitor-osx-arm64-Portable.zip + path: releases/kcap-osx-arm64-Portable.zip app-bundle-windows: name: App bundle (win-x64) @@ -355,18 +355,18 @@ jobs: - name: Pack (unsigned) run: > - vpk pack --packId KurrentCapacitor --packVersion "${{ steps.version.outputs.VERSION }}" + vpk pack --packId kcap --packVersion "${{ steps.version.outputs.VERSION }}" --packTitle "kcap" --packAuthors Kurrent --mainExe "kcap-desktop.exe" --packDir publish/app --icon src/Capacitor.App/Assets/kcap-icon.ico --channel win-x64 --outputDir releases - name: Install the bundle - run: bash scripts/install-windows-setup.sh releases/KurrentCapacitor-win-x64-Setup.exe + run: bash scripts/install-windows-setup.sh releases/kcap-win-x64-Setup.exe - name: Assert the installed bundle run: | set -euo pipefail - install="$(cygpath -u "$LOCALAPPDATA")/KurrentCapacitor" + install="$(cygpath -u "$LOCALAPPDATA")/kcap" bash scripts/assert-app-plugin.sh "$install/current" "${{ steps.version.outputs.VERSION }}" bash scripts/assert-app-cli-version.sh "$install/current/kcap.exe" "${{ steps.version.outputs.VERSION }}" bash scripts/assert-bundle-digest.sh "$install" publish/daemon.sha256 @@ -382,7 +382,7 @@ jobs: $cfg = Join-Path $env:RUNNER_TEMP 'kcap-smoke' New-Item -ItemType Directory -Force $cfg | Out-Null $env:KCAP_CONFIG_DIR = $cfg; $env:KCAP_APP_UPDATE_URL = 'http://127.0.0.1:9/'; $env:DO_NOT_TRACK = '1' - $p = Start-Process (Join-Path $env:LOCALAPPDATA 'KurrentCapacitor\current\kcap-desktop.exe') -PassThru + $p = Start-Process (Join-Path $env:LOCALAPPDATA 'kcap\current\kcap-desktop.exe') -PassThru Start-Sleep 15 if ($p.HasExited) { Write-Error "the packed app exited within 15 s (exit $($p.ExitCode))"; exit 1 } Stop-Process -Id $p.Id -Force @@ -391,4 +391,4 @@ jobs: uses: actions/upload-artifact@v7 with: name: app-win-x64-unsigned - path: releases/KurrentCapacitor-win-x64-Setup.exe + path: releases/kcap-win-x64-Setup.exe diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13b1d685e3..2586d963a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -859,7 +859,7 @@ jobs: AWS_DEFAULT_REGION: auto run: | set -euo pipefail - KEY="desktop/osx-arm64/KurrentCapacitor-${{ steps.version.outputs.VERSION }}-osx-arm64-full.nupkg" + KEY="desktop/osx-arm64/kcap-${{ steps.version.outputs.VERSION }}-osx-arm64-full.nupkg" set +e ERR="$(aws s3api head-object --endpoint-url "${{ secrets.R2_ENDPOINT }}" --bucket "${{ secrets.R2_BUCKET }}" --key "$KEY" 2>&1 >/dev/null)" rc=$? @@ -910,7 +910,7 @@ jobs: - name: Pack, sign the bundle, notarize shell: bash run: > - vpk pack --packId KurrentCapacitor --packVersion "${{ steps.version.outputs.VERSION }}" + vpk pack --packId kcap --packVersion "${{ steps.version.outputs.VERSION }}" --packTitle "kcap" --packAuthors Kurrent --mainExe "kcap-desktop" --packDir "publish/kcap.app" --channel osx-arm64 --noInst --outputDir releases @@ -922,7 +922,7 @@ jobs: run: | set -euo pipefail mkdir -p extracted - ditto -x -k releases/KurrentCapacitor-osx-arm64-Portable.zip extracted + ditto -x -k releases/kcap-osx-arm64-Portable.zip extracted APP="extracted/kcap.app" bash scripts/assert-app-plugin.sh "$APP" "${{ steps.version.outputs.VERSION }}" bash scripts/assert-app-cli-version.sh "$APP/Contents/MacOS/kcap" "${{ steps.version.outputs.VERSION }}" @@ -1020,11 +1020,10 @@ jobs: DMG="releases/kcap-$V-osx-arm64.dmg" aws s3 cp $R2 "$DMG" "$DEST/kcap-$V-osx-arm64.dmg" --cache-control "public, max-age=31536000, immutable" aws s3 cp $R2 "$DEST/releases.osx-arm64.json" merged.json - # Keep legacy download links current while publishing the kcap names. for alias in $(bash scripts/promote-desktop-aliases.sh merged.json "$V"); do case "$alias" in - beta) aws s3 cp $R2 "$DMG" "$DEST/kcap-osx-arm64-beta.dmg" --cache-control "public, max-age=300"; aws s3 cp $R2 "$DMG" "$DEST/Kurrent-Capacitor-osx-arm64-beta.dmg" --cache-control "public, max-age=300" ;; - stable) aws s3 cp $R2 "$DMG" "$DEST/kcap-osx-arm64.dmg" --cache-control "public, max-age=300"; aws s3 cp $R2 "$DMG" "$DEST/Kurrent-Capacitor-osx-arm64.dmg" --cache-control "public, max-age=300" ;; + beta) aws s3 cp $R2 "$DMG" "$DEST/kcap-osx-arm64-beta.dmg" --cache-control "public, max-age=300" ;; + stable) aws s3 cp $R2 "$DMG" "$DEST/kcap-osx-arm64.dmg" --cache-control "public, max-age=300" ;; esac done @@ -1078,7 +1077,7 @@ jobs: AWS_DEFAULT_REGION: auto run: | set -euo pipefail - KEY="desktop/win-x64/KurrentCapacitor-${{ steps.version.outputs.VERSION }}-win-x64-full.nupkg" + KEY="desktop/win-x64/kcap-${{ steps.version.outputs.VERSION }}-win-x64-full.nupkg" set +e ERR="$(aws s3api head-object --endpoint-url "${{ secrets.R2_ENDPOINT }}" --bucket "${{ secrets.R2_BUCKET }}" --key "$KEY" 2>&1 >/dev/null)" rc=$? @@ -1127,7 +1126,7 @@ jobs: else echo "::warning::WINDOWS_CERTIFICATE_PFX is not configured; the Windows app ships unsigned" fi - vpk pack --packId KurrentCapacitor --packVersion "${{ steps.version.outputs.VERSION }}" \ + vpk pack --packId kcap --packVersion "${{ steps.version.outputs.VERSION }}" \ --packTitle "kcap" --packAuthors Kurrent --mainExe "kcap-desktop.exe" \ --packDir publish/app --icon src/Capacitor.App/Assets/kcap-icon.ico \ --channel win-x64 --outputDir releases "${sign[@]}" @@ -1136,17 +1135,17 @@ jobs: # The gates check the tree Setup.exe installs, which is what a user runs. A silent install # does not launch the app, so the smoke below starts it. - name: Install the packed app - run: bash scripts/install-windows-setup.sh releases/KurrentCapacitor-win-x64-Setup.exe + run: bash scripts/install-windows-setup.sh releases/kcap-win-x64-Setup.exe - name: Assert the installed app run: | set -euo pipefail - install="$(cygpath -u "$LOCALAPPDATA")/KurrentCapacitor" + install="$(cygpath -u "$LOCALAPPDATA")/kcap" bash scripts/assert-app-plugin.sh "$install/current" "${{ steps.version.outputs.VERSION }}" bash scripts/assert-app-cli-version.sh "$install/current/kcap.exe" "${{ steps.version.outputs.VERSION }}" bash scripts/assert-bundle-digest.sh "$install" trio/bin/daemon.sha256 "$install/current/kcap-daemon.exe" --version - mv releases/KurrentCapacitor-win-x64-Setup.exe "releases/kcap-${{ steps.version.outputs.VERSION }}-win-x64-Setup.exe" + mv releases/kcap-win-x64-Setup.exe "releases/kcap-${{ steps.version.outputs.VERSION }}-win-x64-Setup.exe" cp trio/bin/daemon.sha256 trio/bin/kcap.sha256 releases/ - name: Smoke the installed app @@ -1155,7 +1154,7 @@ jobs: $cfg = Join-Path $env:RUNNER_TEMP 'kcap-smoke' New-Item -ItemType Directory -Force $cfg | Out-Null $env:KCAP_CONFIG_DIR = $cfg; $env:KCAP_APP_UPDATE_URL = 'http://127.0.0.1:9/'; $env:DO_NOT_TRACK = '1' - $p = Start-Process (Join-Path $env:LOCALAPPDATA 'KurrentCapacitor\current\kcap-desktop.exe') -PassThru + $p = Start-Process (Join-Path $env:LOCALAPPDATA 'kcap\current\kcap-desktop.exe') -PassThru Start-Sleep 15 if ($p.HasExited) { Write-Error "the packed app exited within 15 s (exit $($p.ExitCode))"; exit 1 } Stop-Process -Id $p.Id -Force @@ -1239,11 +1238,10 @@ jobs: SETUP="releases/kcap-$V-win-x64-Setup.exe" aws s3 cp $R2 "$SETUP" "$DEST/kcap-$V-win-x64-Setup.exe" --cache-control "public, max-age=31536000, immutable" aws s3 cp $R2 "$DEST/releases.win-x64.json" merged.json - # Keep legacy download links current while publishing the kcap names. for alias in $(bash scripts/promote-desktop-aliases.sh merged.json "$V"); do case "$alias" in - beta) aws s3 cp $R2 "$SETUP" "$DEST/kcap-win-x64-beta-Setup.exe" --cache-control "public, max-age=300"; aws s3 cp $R2 "$SETUP" "$DEST/Kurrent-Capacitor-win-x64-beta-Setup.exe" --cache-control "public, max-age=300" ;; - stable) aws s3 cp $R2 "$SETUP" "$DEST/kcap-win-x64-Setup.exe" --cache-control "public, max-age=300"; aws s3 cp $R2 "$SETUP" "$DEST/Kurrent-Capacitor-win-x64-Setup.exe" --cache-control "public, max-age=300" ;; + beta) aws s3 cp $R2 "$SETUP" "$DEST/kcap-win-x64-beta-Setup.exe" --cache-control "public, max-age=300" ;; + stable) aws s3 cp $R2 "$SETUP" "$DEST/kcap-win-x64-Setup.exe" --cache-control "public, max-age=300" ;; esac done diff --git a/README.md b/README.md index 2330785b4a..8e7702009a 100644 --- a/README.md +++ b/README.md @@ -111,21 +111,18 @@ Updates arrive through the app: it checks a few times a day, downloads in the ba Help → Report a Bug… / Send Feedback… (or the help button in the session rail's footer) sends a report to Kurrent support; replies arrive by email. -**Upgrading an existing macOS installation:** use **Check for Updates…** in the installed app, or open **kcap** from the new disk image and choose **Quit and update**. Existing installations stay in their Applications folder and migrate from `Kurrent Capacitor.app` to `kcap.app`. The migration updates owned terminal links and background-service paths, then refreshes previously installed agent integrations with the bundled CLI to repair MCP commands, git hooks, and the Claude marketplace path. A failed integration refresh is reported and retried on the next launch. It preserves sign-in and settings, and keeps supported agent sessions for resumption unless `daemon.auto_resume` is off. An installer that finds valid installations in both Applications folders, or another app occupying the destination, asks for a destination first. Opening an installed `kcap.app` keeps that copy when its own folder needs no migration, even if a legacy copy remains in the other folder. An unrelated app is never replaced. Redundant copies are removed after the replacement opens successfully; a failed migration restores the installation or retains recovery backups and reports what needs attention. An equal or newer installed version offers **Open installed copy**. +**Upgrading an existing macOS installation:** use **Check for Updates…** in the installed app, or open **kcap** from the new disk image and choose **Quit and update**. The installation stays in its Applications folder. Moving the app to the other Applications folder updates owned terminal links and background-service paths, then refreshes previously installed agent integrations with the bundled CLI to repair MCP commands, git hooks, and the Claude marketplace path. A failed integration refresh is reported and retried on the next launch. An update preserves sign-in and settings, and keeps supported agent sessions for resumption unless `daemon.auto_resume` is off. An installer that finds valid installations in both Applications folders, or another app occupying the destination, asks for a destination first. An unrelated app is never replaced. Redundant copies are removed after the replacement opens successfully; a failed update restores the installation or retains recovery backups and reports what needs attention. An equal or newer installed version offers **Open installed copy**. ### Desktop app on Windows -Download `kcap--win-x64-Setup.exe` from the [GitHub release](https://github.com/kurrent-io/kcap-cli/releases) (Windows 10 or later, x64) and run it. It installs per user into `%LocalAppData%\KurrentCapacitor` with no administrator prompt and adds a Start-menu entry. Like the macOS app it bundles its own `kcap` CLI and daemon. The first run offers to add the app's folder to your user PATH, so `kcap` works from any new terminal. Signing in installs the daemon as a background service (a per-user Scheduled Task that starts at sign-in) and starts it, as on macOS. +Download `kcap--win-x64-Setup.exe` from the [GitHub release](https://github.com/kurrent-io/kcap-cli/releases) (Windows 10 or later, x64) and run it. It installs per user into `%LocalAppData%\kcap` with no administrator prompt and adds a Start-menu entry. Like the macOS app it bundles its own `kcap` CLI and daemon. The first run offers to add the app's folder to your user PATH, so `kcap` works from any new terminal. Signing in installs the daemon as a background service (a per-user Scheduled Task that starts at sign-in) and starts it, as on macOS. Windows draws no application menu, so **Settings…**, **Changelog** and the app version sit in the help button in the session rail's footer, next to Documentation and the report items. Closing the window keeps the app running in the notification area — quit from the tray icon's menu. Updates arrive through the app the same way as on macOS, from the Windows feed. ### Rebrand compatibility kcap keeps existing configuration filenames, data directories, worktree locations, -and branch prefixes. Internal app and package IDs also stay unchanged, including -`io.kurrent.capacitor` and `KurrentCapacitor`. These conventions apply to new -installations too; no data-directory migration or manual reinstall is required -for the rebrand. +and branch prefixes; no data-directory migration is required for the rebrand. The connection skill is now `connect-kcap` (`kcap-connect-kcap` in the shared agent skill tree), and the Muse recording skill is `kcap-recording`. Refreshing diff --git a/scripts/assemble-app-bundle.test.sh b/scripts/assemble-app-bundle.test.sh index 7524f39366..3ea5445e1b 100644 --- a/scripts/assemble-app-bundle.test.sh +++ b/scripts/assemble-app-bundle.test.sh @@ -15,7 +15,7 @@ cmp "$tmp/publish/kcap" "$bundle/Contents/MacOS/kcap" cmp "$tmp/publish/kcap-desktop" "$bundle/Contents/MacOS/kcap-desktop" [[ "$(/usr/libexec/PlistBuddy -c 'Print CFBundleExecutable' "$bundle/Contents/Info.plist")" == kcap-desktop ]] [[ "$(/usr/libexec/PlistBuddy -c 'Print CFBundleDisplayName' "$bundle/Contents/Info.plist")" == kcap ]] -[[ "$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$bundle/Contents/Info.plist")" == io.kurrent.capacitor ]] +[[ "$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$bundle/Contents/Info.plist")" == io.kurrent.kcap ]] test -f "$bundle/Contents/Resources/kcap-icon.icns" bash "$here/assert-app-plugin.sh" "$bundle" 1.3.0-beta.7 >/dev/null if bash "$here/assemble-app-bundle.sh" "$tmp/publish" "$bundle" 1.3.0-beta.8 >/dev/null 2>&1; then diff --git a/scripts/desktop-baseline.test.sh b/scripts/desktop-baseline.test.sh index df4bf920b2..f7df43ca97 100755 --- a/scripts/desktop-baseline.test.sh +++ b/scripts/desktop-baseline.test.sh @@ -6,8 +6,8 @@ tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT make_pkg() { # — the real download layout: one full nupkg, no manifest beside it local dir="$1" v="$2" work; work="$(mktemp -d)" - printf 'KurrentCapacitor%s' "$v" > "$work/KurrentCapacitor.nuspec" - (cd "$work" && zip -q "$dir/KurrentCapacitor-$v-osx-arm64-full.nupkg" KurrentCapacitor.nuspec) + printf 'kcap%s' "$v" > "$work/kcap.nuspec" + (cd "$work" && zip -q "$dir/kcap-$v-osx-arm64-full.nupkg" kcap.nuspec) rm -rf "$work" } diff --git a/scripts/install-windows-setup.sh b/scripts/install-windows-setup.sh index df69def231..8efb528898 100755 --- a/scripts/install-windows-setup.sh +++ b/scripts/install-windows-setup.sh @@ -7,7 +7,7 @@ set -euo pipefail setup="${1:?usage: install-windows-setup.sh }" unix() { if command -v cygpath >/dev/null 2>&1; then cygpath -u "$1"; else printf '%s' "$1"; fi; } -root="$(unix "${LOCALAPPDATA:?LOCALAPPDATA is not set}")/KurrentCapacitor" +root="$(unix "${LOCALAPPDATA:?LOCALAPPDATA is not set}")/kcap" shortcut="$(unix "${APPDATA:?APPDATA is not set}")/Microsoft/Windows/Start Menu/Programs/kcap.lnk" "$setup" --silent || { echo "Setup.exe failed (exit $?)" >&2; exit 1; } diff --git a/scripts/install-windows-setup.test.sh b/scripts/install-windows-setup.test.sh index 5bd80fc6f3..8ed3de26fa 100755 --- a/scripts/install-windows-setup.test.sh +++ b/scripts/install-windows-setup.test.sh @@ -5,10 +5,10 @@ sh="$here/install-windows-setup.sh" tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT layout=( - "Local/KurrentCapacitor/Update.exe" - "Local/KurrentCapacitor/current/kcap-desktop.exe" - "Local/KurrentCapacitor/current/kcap.exe" - "Local/KurrentCapacitor/current/kcap-daemon.exe" + "Local/kcap/Update.exe" + "Local/kcap/current/kcap-desktop.exe" + "Local/kcap/current/kcap.exe" + "Local/kcap/current/kcap-daemon.exe" "Roaming/Microsoft/Windows/Start Menu/Programs/kcap.lnk" ) diff --git a/scripts/promote-desktop-aliases.test.sh b/scripts/promote-desktop-aliases.test.sh index 74e772ca0d..5cc20cb61d 100755 --- a/scripts/promote-desktop-aliases.test.sh +++ b/scripts/promote-desktop-aliases.test.sh @@ -9,7 +9,7 @@ manifest() { # — Velopack's releases..json shap { printf '{"Assets":['; local first=1 for v in "$@"; do [ $first -eq 1 ] || printf ','; first=0 - printf '{"PackageId":"KurrentCapacitor","Version":"%s","Type":"Full","FileName":"KurrentCapacitor-%s-osx-arm64-full.nupkg","SHA1":"","SHA256":"","Size":1}' "$v" "$v" + printf '{"PackageId":"kcap","Version":"%s","Type":"Full","FileName":"kcap-%s-osx-arm64-full.nupkg","SHA1":"","SHA256":"","Size":1}' "$v" "$v" done printf ']}'; } > "$file" } diff --git a/scripts/verify-desktop-immutables.sh b/scripts/verify-desktop-immutables.sh index 9101c99967..4fa2b6bd92 100755 --- a/scripts/verify-desktop-immutables.sh +++ b/scripts/verify-desktop-immutables.sh @@ -18,8 +18,8 @@ fetch="${3:?usage: verify-desktop-immutables.sh "$local_dir/KurrentCapacitor-$v-osx-arm64-full.nupkg" +printf 'full' > "$local_dir/kcap-$v-osx-arm64-full.nupkg" printf 'dmg' > "$local_dir/kcap-$v-osx-arm64.dmg" fetch="$tmp/fetch.sh" printf '#!/usr/bin/env bash\n[ -f "%s/$1" ] || exit 44\ncp "%s/$1" "$2"\n' "$remote" "$remote" > "$fetch"; chmod +x "$fetch" @@ -15,7 +15,7 @@ assert() { local rc; set +e; bash "$sh" "$local_dir" "$v" "$fetch" >/dev/null 2> if [ "$rc" != "$2" ]; then echo "FAIL: $1 -> rc=$rc (want $2)"; fail=1; fi; } assert "nothing published yet passes" 0 -cp "$local_dir/KurrentCapacitor-$v-osx-arm64-full.nupkg" "$remote/" +cp "$local_dir/kcap-$v-osx-arm64-full.nupkg" "$remote/" assert "identical bytes already published passes (retry)" 0 printf 'other-bytes' > "$remote/kcap-$v-osx-arm64.dmg" assert "different bytes already published fails" 1 diff --git a/src/Capacitor.App/App.axaml.cs b/src/Capacitor.App/App.axaml.cs index 7db5ef6904..62cc525753 100644 --- a/src/Capacitor.App/App.axaml.cs +++ b/src/Capacitor.App/App.axaml.cs @@ -459,8 +459,7 @@ void Quit() { } internal static bool ShouldPromptToInstall(InstallLocationKind kind, string root, ApplicationsInstallPlan plan) => - !InstallLocation.Passes(kind) || plan.Migrates || Path.GetFullPath(root) != Path.GetFullPath(plan.Target) - || Path.GetFileName(root.TrimEnd('/')) == "Kurrent Capacitor.app"; + !InstallLocation.Passes(kind) || Path.GetFullPath(root) != Path.GetFullPath(plan.Target); Task QuitInstalledAppAsync(string root, CancellationToken ct) => InstalledAppProcesses.QuitAsync(root, _time, ct); diff --git a/src/Capacitor.App/Packaging/Info.plist b/src/Capacitor.App/Packaging/Info.plist index 8053388631..1eee229479 100644 --- a/src/Capacitor.App/Packaging/Info.plist +++ b/src/Capacitor.App/Packaging/Info.plist @@ -9,7 +9,7 @@ CFBundleExecutable kcap-desktop CFBundleIdentifier - io.kurrent.capacitor + io.kurrent.kcap CFBundlePackageType APPL CFBundleShortVersionString diff --git a/src/Capacitor.App/Services/ApplicationsInstallPlan.cs b/src/Capacitor.App/Services/ApplicationsInstallPlan.cs index 4689fe2785..26d9ec0e6a 100644 --- a/src/Capacitor.App/Services/ApplicationsInstallPlan.cs +++ b/src/Capacitor.App/Services/ApplicationsInstallPlan.cs @@ -5,9 +5,8 @@ public enum ApplicationsInstallAction { Install, Update, OpenInstalled, Blocked public sealed record ApplicationsInstallPlan( ApplicationsInstallAction Action, string Target, string? SourceVersion = null, string? InstalledVersion = null, string? Error = null, string? ExistingPath = null, string? PayloadPath = null) { - public bool Migrates => ExistingPath is not null && ExistingPath != Target; public string Title => Action switch { - ApplicationsInstallAction.Update => Migrates ? "Upgrade to kcap" : "Update kcap", + ApplicationsInstallAction.Update => "Update kcap", ApplicationsInstallAction.OpenInstalled => "kcap is already installed", ApplicationsInstallAction.Blocked => "Cannot install kcap", _ => "Install kcap", @@ -20,9 +19,7 @@ public sealed record ApplicationsInstallPlan( }; public string Detail => Action switch { - ApplicationsInstallAction.Update => (Migrates - ? "Kurrent Capacitor will be replaced with kcap, and the updated app will open." - : "kcap will be updated and reopened.") + ApplicationsInstallAction.Update => "kcap will be updated and reopened." + " Your settings and sign-in will stay intact.\n\n" + "The app will quit and its daemon will restart. This interrupts active turns; resumable agents restart unless auto-resume is off.", ApplicationsInstallAction.OpenInstalled => "The installed copy of kcap will open. Your settings and sign-in will stay intact.", diff --git a/src/Capacitor.App/Services/ApplicationsMover.cs b/src/Capacitor.App/Services/ApplicationsMover.cs index 1c237475e5..df30f1673a 100644 --- a/src/Capacitor.App/Services/ApplicationsMover.cs +++ b/src/Capacitor.App/Services/ApplicationsMover.cs @@ -14,50 +14,42 @@ public sealed partial class ApplicationsMover( Func>? quitRunning = null, Func? migrateReferences = null, Func>? activate = null, string? userApplicationsDir = null) { - static readonly string[] BundleNames = ["kcap.app", "Kurrent Capacitor.app"]; + const string BundleName = "kcap.app"; static readonly TimeSpan CopyTimeout = TimeSpan.FromMinutes(2); public static IReadOnlyList ApplicationDirectories(string bundleRoot, string userApplicationsDir, string systemApplicationsDir = "/Applications") { var source = Path.GetFullPath(bundleRoot.TrimEnd('/')); - bool Existing(string directory) => BundleNames - .Select(name => Path.Combine(directory, name)).Any(path => ReadVersion(path) is not null); + bool Existing(string directory) => ReadVersion(Path.Combine(directory, BundleName)) is not null; var directories = new[] { systemApplicationsDir, userApplicationsDir }; var existing = directories.Where(Existing).ToArray(); bool Conflicts(string directory) { - var target = Path.Combine(directory, "kcap.app"); + var target = Path.Combine(directory, BundleName); return Path.GetFullPath(target) != source && Exists(target) && ReadVersion(target) is null; } if (Conflicts(systemApplicationsDir) || existing.Any(Conflicts)) return directories; var home = Path.GetDirectoryName(source); - if (Path.GetFileName(source) == "kcap.app" && home is not null - && existing.Contains(home) && ReadVersion(source) is not null - && ReadVersion(Path.Combine(home, "Kurrent Capacitor.app")) is null) + if (Path.GetFileName(source) == BundleName && home is not null + && existing.Contains(home) && ReadVersion(source) is not null) return [home]; if (existing.Length > 0) return existing; return [Path.GetDirectoryName(source) == userApplicationsDir ? userApplicationsDir : systemApplicationsDir]; } public ApplicationsInstallPlan Inspect(string bundleRoot) { - var target = Path.Combine(applicationsDir, "kcap.app"); - var legacy = Path.Combine(applicationsDir, "Kurrent Capacitor.app"); + var target = Path.Combine(applicationsDir, BundleName); var source = ReadVersion(bundleRoot); if (source is null) return new(ApplicationsInstallAction.Blocked, target, Error: "This copy of kcap is incomplete or cannot be verified. Download it again."); if (Exists(target) && ReadVersion(target) is null) return new(ApplicationsInstallAction.Blocked, target, source, Error: $"{target} already exists, but is not a verified kcap bundle. Choose another Applications folder or move it aside in Finder."); - var existing = ReadVersion(legacy) is not null ? legacy : ReadVersion(target) is not null ? target : null; - var installed = existing is null ? null : ReadVersion(existing); - var payload = installed is not null && !PrereleaseSemver.IsNewer(source, installed) ? existing! : bundleRoot; + var installed = ReadVersion(target); + var existing = installed is null ? null : target; + var payload = installed is not null && !PrereleaseSemver.IsNewer(source, installed) ? target : bundleRoot; var payloadVersion = ReadVersion(payload)!; - // A dragged copy can be newer than both the disk image and the legacy install. - if (ReadVersion(target) is { } canonical && PrereleaseSemver.IsNewer(canonical, payloadVersion)) { - payload = target; - payloadVersion = canonical; - } - var action = existing is null ? ApplicationsInstallAction.Install - : existing != target || PrereleaseSemver.IsNewer(source, installed!) + var action = installed is null ? ApplicationsInstallAction.Install + : PrereleaseSemver.IsNewer(source, installed) ? ApplicationsInstallAction.Update : ApplicationsInstallAction.OpenInstalled; return new(action, target, payloadVersion, installed, ExistingPath: existing, PayloadPath: payload); } @@ -140,8 +132,7 @@ void RestoreBundles() { static string? ReadVersion(string root) { try { if (!Directory.Exists(root) || new DirectoryInfo(root).LinkTarget is not null || - (!File.Exists(Path.Combine(root, "Contents", "MacOS", "kcap-desktop")) && - !File.Exists(Path.Combine(root, "Contents", "MacOS", "Kurrent Capacitor")))) return null; + !File.Exists(Path.Combine(root, "Contents", "MacOS", "kcap-desktop"))) return null; using var reader = XmlReader.Create(Path.Combine(root, "Contents", "Info.plist"), new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, XmlResolver = null }); var entries = XDocument.Load(reader).Root?.Element("dict")?.Elements().ToArray(); @@ -152,7 +143,7 @@ void RestoreBundles() { return entries[i + 1].Value; return null; } - return Value("CFBundleIdentifier") == "io.kurrent.capacitor" + return Value("CFBundleIdentifier") == "io.kurrent.kcap" ? Value("CFBundleVersion") ?? Value("CFBundleShortVersionString") : null; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or XmlException) { return null; diff --git a/src/Capacitor.App/Services/InstalledAppProcesses.cs b/src/Capacitor.App/Services/InstalledAppProcesses.cs index 99d0ccc301..6e2a36b8ba 100644 --- a/src/Capacitor.App/Services/InstalledAppProcesses.cs +++ b/src/Capacitor.App/Services/InstalledAppProcesses.cs @@ -7,17 +7,17 @@ internal static partial class InstalledAppProcesses { const string ObjC = "/usr/lib/libobjc.A.dylib"; internal static int[]? Find(string root) { - string[] names = ["kcap-desktop", "Kurrent Capacitor"]; - var executables = names.Select(name => Path.Combine(root, "Contents", "MacOS", name)).ToArray(); + const string name = "kcap-desktop"; + var executable = Path.Combine(root, "Contents", "MacOS", name); var matches = new List(); var unknown = false; - foreach (var process in names.SelectMany(Process.GetProcessesByName)) { + foreach (var process in Process.GetProcessesByName(name)) { using (process) { if (process.Id == Environment.ProcessId) continue; try { if (process.HasExited) continue; - if (process.MainModule?.FileName is not { } executable) unknown = true; - else if (executables.Contains(executable)) matches.Add(process.Id); + if (process.MainModule?.FileName is not { } path) unknown = true; + else if (path == executable) matches.Add(process.Id); } catch (Exception ex) when (ex is System.ComponentModel.Win32Exception or InvalidOperationException or NotSupportedException) { unknown = true; } diff --git a/src/Capacitor.App/Services/Notifications/NativeDesktopNotificationSink.cs b/src/Capacitor.App/Services/Notifications/NativeDesktopNotificationSink.cs index b01d46b215..09196c1d4d 100644 --- a/src/Capacitor.App/Services/Notifications/NativeDesktopNotificationSink.cs +++ b/src/Capacitor.App/Services/Notifications/NativeDesktopNotificationSink.cs @@ -6,7 +6,7 @@ namespace Capacitor.App.Services.Notifications; public sealed class NativeDesktopNotificationSink : IDesktopNotificationSink, IDesktopNotificationAccess { - const string AppUserModelId = "io.kurrent.capacitor"; + const string AppUserModelId = "io.kurrent.kcap"; static bool _labsAvailable; readonly IDesktopNotificationSink _backend; diff --git a/src/Capacitor.App/Services/SingleInstanceGuard.cs b/src/Capacitor.App/Services/SingleInstanceGuard.cs index 72251a18e3..23f5bf646e 100644 --- a/src/Capacitor.App/Services/SingleInstanceGuard.cs +++ b/src/Capacitor.App/Services/SingleInstanceGuard.cs @@ -26,7 +26,7 @@ public sealed partial class SingleInstanceGuard : IDisposable { // The path is hashed because a kernel object name cannot contain a backslash. public static string NameFor(string executablePath) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(Path.GetFullPath(executablePath).ToUpperInvariant())); - return $"KurrentCapacitor-{Convert.ToHexStringLower(hash.AsSpan(0, 16))}"; + return $"kcap-{Convert.ToHexStringLower(hash.AsSpan(0, 16))}"; } /// Null when another copy kept the lock for the whole wait; that copy has been asked to show diff --git a/src/Capacitor.Cli/LocalEdition/LocalClientCopies.cs b/src/Capacitor.Cli/LocalEdition/LocalClientCopies.cs index 8d814abc54..15d4af5410 100644 --- a/src/Capacitor.Cli/LocalEdition/LocalClientCopies.cs +++ b/src/Capacitor.Cli/LocalEdition/LocalClientCopies.cs @@ -16,16 +16,13 @@ sealed class LocalClientCopies(IProcessRunner runner, string? searchPath, string public static LocalClientCopies ForCurrentProcess(UserHome home, TimeProvider time) { string[] apps = OperatingSystem.IsMacOS() - ? [.. AppClis("/Applications"), .. AppClis(Path.Combine(home.Path, "Applications"))] + ? [AppCli("/Applications"), AppCli(Path.Combine(home.Path, "Applications"))] : []; return new(new ProcessRunner(time), Environment.GetEnvironmentVariable("PATH"), Environment.ProcessPath, apps); } - internal static string[] AppClis(string applications) => [ - Path.Combine(applications, "kcap.app", "Contents", "MacOS", "kcap"), - Path.Combine(applications, "Kurrent Capacitor.app", "Contents", "MacOS", "kcap") - ]; + internal static string AppCli(string applications) => Path.Combine(applications, "kcap.app", "Contents", "MacOS", "kcap"); public IReadOnlyList Find() => _found ??= Locate().Select(c => c with { Version = VersionOf(c.Path) }).ToList(); diff --git a/test/Capacitor.App.Tests.Unit/AppStartupTests.cs b/test/Capacitor.App.Tests.Unit/AppStartupTests.cs index 4b150c0428..998f2ca490 100644 --- a/test/Capacitor.App.Tests.Unit/AppStartupTests.cs +++ b/test/Capacitor.App.Tests.Unit/AppStartupTests.cs @@ -732,7 +732,7 @@ public async Task Quit_lets_close_through() { await Assert.That(stillTracked).IsFalse(); } - /// Tray "Open Kurrent Capacitor" on a hidden window re-shows THAT window (no rebuild). + /// Tray "Open kcap" on a hidden window re-shows THAT window (no rebuild). [Test] [NotInParallel("AvaloniaSession")] public async Task ShowMainWindow_reshows_same_instance() { diff --git a/test/Capacitor.App.Tests.Unit/ApplicationIntegrationRefreshTests.cs b/test/Capacitor.App.Tests.Unit/ApplicationIntegrationRefreshTests.cs index 5a3daf5b01..099713f2c2 100644 --- a/test/Capacitor.App.Tests.Unit/ApplicationIntegrationRefreshTests.cs +++ b/test/Capacitor.App.Tests.Unit/ApplicationIntegrationRefreshTests.cs @@ -70,18 +70,17 @@ public async Task Another_bundle_does_not_consume_the_pending_repair() { } [Test] - public async Task Migration_handoff_repairs_an_owned_Codex_MCP_entry_after_removing_the_legacy_bundle() { + public async Task Migration_handoff_repairs_an_owned_Codex_MCP_entry_after_removing_the_user_Applications_copy() { var apps = Tmp.CreateDir("Applications"); - var old = Path.Combine(apps, "Kurrent Capacitor.app"); - var download = Tmp.CreateDir("Downloads/kcap.app"); + var userApps = Tmp.CreateDir("home/Applications"); + var old = Path.Combine(userApps, "kcap.app"); static void Bundle(string root, string version) { Directory.CreateDirectory(Path.Combine(root, "Contents/MacOS")); File.WriteAllText(Path.Combine(root, "Contents/MacOS/kcap-desktop"), "app"); File.WriteAllText(Path.Combine(root, "Contents/Info.plist"), - $"CFBundleIdentifierio.kurrent.capacitorCFBundleVersion{version}"); + $"CFBundleIdentifierio.kurrent.kcapCFBundleVersion{version}"); } - Bundle(old, "1.0.0"); - Bundle(download, "1.1.0"); + Bundle(old, "1.1.0"); var codex = Tmp.GetResolvedPath("home", ".codex", "config.toml"); CodexConfigToml.RegisterKcapMcpServers(codex, () => Path.Combine(old, "Contents/MacOS/kcap")); var runner = new Runner((file, args) => { @@ -95,15 +94,15 @@ static void Bundle(string root, string version) { }); var refresh = new ApplicationIntegrationRefresh(new ConfigRoot(Tmp.CreateDir("config")), runner); var mover = new ApplicationsMover(runner, (from, to) => { Directory.Move(from, to); return true; }, apps, - activate: (target, _) => { refresh.Schedule(target); return Task.FromResult(true); }); - var outcome = await mover.MoveAsync(download, CancellationToken.None); + activate: (target, _) => { refresh.Schedule(target); return Task.FromResult(true); }, userApplicationsDir: userApps); + var outcome = await mover.MoveAsync(old, CancellationToken.None); await Assert.That(outcome.Moved).IsTrue(); await Assert.That(Directory.Exists(old)).IsFalse(); await refresh.RunPendingAsync(outcome.InstalledPath, CancellationToken.None); var commands = CodexConfigToml.ReadMcpServerCommands(codex); await Assert.That(commands.Count).IsGreaterThan(0); await Assert.That(commands.All(command => command.Command == Path.Combine(apps, "kcap.app", "Contents", "MacOS", "kcap"))).IsTrue(); - await Assert.That(File.ReadAllText(codex)).DoesNotContain("Kurrent Capacitor.app"); + await Assert.That(File.ReadAllText(codex)).DoesNotContain(userApps); await Assert.That(File.Exists(Tmp.PathTo("home/.codex/hooks.json"))).IsFalse(); } } diff --git a/test/Capacitor.App.Tests.Unit/ApplicationReferencesTests.cs b/test/Capacitor.App.Tests.Unit/ApplicationReferencesTests.cs index 5d5b05fd7e..d6d03f0aa0 100644 --- a/test/Capacitor.App.Tests.Unit/ApplicationReferencesTests.cs +++ b/test/Capacitor.App.Tests.Unit/ApplicationReferencesTests.cs @@ -29,9 +29,9 @@ public Task RunStreamingAsync(string fileName, string[] args, R } (string Old, string Target, string Unit, string Link, DaemonStore Store, UserHome Home) Setup() { - var old = Tmp.CreateDir("Applications/Kurrent Capacitor.app"); + var old = Tmp.CreateDir("home/Applications/kcap.app"); var target = Tmp.CreateDir("Applications/kcap.app"); - Tmp.CreateFile("Applications/Kurrent Capacitor.app/Contents/MacOS/kcap"); + Tmp.CreateFile("home/Applications/kcap.app/Contents/MacOS/kcap"); Tmp.CreateFile("Applications/kcap.app/Contents/MacOS/kcap"); var unit = Tmp.CreateFile("home/Library/LaunchAgents/io.kurrent.kcap.daemon.work.plist"); var dict = new XElement("dict", new XElement("key", "Label"), new XElement("string", "io.kurrent.kcap.daemon.work"), @@ -53,21 +53,22 @@ public Task RunStreamingAsync(string fileName, string[] args, R } [Test] - public async Task A_dragged_bundle_migrates_real_files_and_references_before_opening_then_removes_the_legacy_copy() { + public async Task A_user_Applications_copy_moves_real_files_and_references_before_opening_then_is_removed() { if (!OperatingSystem.IsMacOS()) return; var paths = Setup(); static void Bundle(string root, string version) { File.WriteAllText(Path.Combine(root, "Contents", "Info.plist"), - $"CFBundleIdentifierio.kurrent.capacitorCFBundleVersion{version}"); + $"CFBundleIdentifierio.kurrent.kcapCFBundleVersion{version}"); File.WriteAllText(Path.Combine(root, "Contents", "MacOS", "kcap-desktop"), version); } - Bundle(paths.Old, "1.0.0"); - Bundle(paths.Target, "1.1.0"); + Bundle(paths.Old, "1.1.0"); + Directory.Delete(paths.Target, recursive: true); var ctl = new Launchctl(); var runner = new BundleRunner(ctl); var activated = false; var mover = new ApplicationsMover(runner, ApplicationsMover.PromoteExclusive, Path.GetDirectoryName(paths.Target)!, ApplicationsMover.SwapAtomic, + userApplicationsDir: Path.GetDirectoryName(paths.Old), migrateReferences: () => new ApplicationReferences(runner, paths.Home, paths.Store, TimeProvider.System, (_, _) => Task.FromResult(true), (_, _) => Task.FromResult(true), "gui/123", [Path.GetDirectoryName(paths.Link)!]), activate: (root, _) => { @@ -76,7 +77,7 @@ static void Bundle(string root, string version) { && new FileInfo(paths.Link).LinkTarget == paths.Target + "/Contents/MacOS/kcap"; return Task.FromResult(activated); }); - var outcome = await mover.MoveAsync(paths.Target, CancellationToken.None); + var outcome = await mover.MoveAsync(paths.Old, CancellationToken.None); await Assert.That(outcome.Moved).IsTrue(); await Assert.That(activated).IsTrue(); await Assert.That(File.ReadAllText(Path.Combine(paths.Target, "Contents", "MacOS", "kcap-desktop"))).IsEqualTo("1.1.0"); diff --git a/test/Capacitor.App.Tests.Unit/ApplicationsMoverTests.cs b/test/Capacitor.App.Tests.Unit/ApplicationsMoverTests.cs index 5ea7715c1b..90f928e859 100644 --- a/test/Capacitor.App.Tests.Unit/ApplicationsMoverTests.cs +++ b/test/Capacitor.App.Tests.Unit/ApplicationsMoverTests.cs @@ -33,13 +33,21 @@ static void CompleteBundle(string root) { CompleteBundle(root, "1.0.0"); } - static void CompleteBundle(string root, string version, string id = "io.kurrent.capacitor") { + static void CompleteBundle(string root, string version, string id = "io.kurrent.kcap") { Directory.CreateDirectory(Path.Combine(root, "Contents", "MacOS")); File.WriteAllText(Path.Combine(root, "Contents", "Info.plist"), $"CFBundleIdentifier{id}CFBundleVersion{version}"); File.WriteAllText(Path.Combine(root, "Contents", "MacOS", "kcap-desktop"), "exe"); } + static bool MoveSwap(string from, string to) { + var aside = to + ".aside"; + Directory.Move(to, aside); + Directory.Move(from, to); + Directory.Move(aside, from); + return true; + } + static bool MovePromote(string from, string to) { if (Directory.Exists(to)) return false; Directory.Move(from, to); @@ -61,105 +69,10 @@ public async Task Complete_copy_is_promoted_and_nothing_is_left_staged() { } [Test] - public async Task Rebrand_migrates_the_legacy_install_to_kcap_app() { - var apps = Tmp.CreateDir("Applications"); - var installed = Tmp.PathTo("Applications/Kurrent Capacitor.app"); - CompleteBundle(installed); - File.Move(Path.Combine(installed, "Contents", "MacOS", "kcap-desktop"), - Path.Combine(installed, "Contents", "MacOS", "Kurrent Capacitor")); - var source = Tmp.PathTo("Downloads/kcap.app"); - CompleteBundle(source, "1.1.0"); - var mover = new ApplicationsMover(new FakeDitto(root => CompleteBundle(root, "1.1.0")), - MovePromote, apps); - - await Assert.That(mover.Inspect(source).Action).IsEqualTo(ApplicationsInstallAction.Update); - var outcome = await mover.MoveAsync(source, CancellationToken.None); - - await Assert.That(outcome.Moved).IsTrue(); - await Assert.That(outcome.InstalledPath).IsEqualTo(Path.Combine(apps, "kcap.app")); - await Assert.That(File.Exists(Path.Combine(apps, "kcap.app", "Contents", "MacOS", "kcap-desktop"))).IsTrue(); - await Assert.That(Directory.Exists(installed)).IsFalse(); - await Assert.That(Directory.GetDirectories(apps).Length).IsEqualTo(1); - } - - [Test] - [Arguments("dmg", false)] - [Arguments("dmg", true)] - [Arguments("system", false)] - [Arguments("user", true)] - public async Task Legacy_upgrade_targets_the_existing_install_from_the_DMG_or_a_copy_in_the_same_folder(string sourceLocation, bool perUser) { - if (!OperatingSystem.IsMacOS()) return; - var systemApps = Tmp.CreateDir("SystemApplications"); - var userApps = Tmp.CreateDir("UserApplications"); - var apps = perUser ? userApps : systemApps; - var installed = Path.Combine(apps, "Kurrent Capacitor.app"); - CompleteBundle(installed); - File.Move(Path.Combine(installed, "Contents", "MacOS", "kcap-desktop"), - Path.Combine(installed, "Contents", "MacOS", "Kurrent Capacitor")); - var source = sourceLocation switch { - "system" => Path.Combine(systemApps, "kcap.app"), - "user" => Path.Combine(userApps, "kcap.app"), - _ => Tmp.PathTo("Downloads/kcap.app"), - }; - var kind = sourceLocation switch { - "system" => InstallLocationKind.Applications, - "user" => InstallLocationKind.UserApplications, - _ => InstallLocationKind.DmgVolume, - }; - CompleteBundle(source, "1.1.0"); - - var choices = ApplicationsMover.ApplicationDirectories(source, userApps, systemApps); - await Assert.That(choices).Contains(apps); - var selected = apps; - var mover = new ApplicationsMover(new FakeDitto(root => CompleteBundle(root, "1.1.0")), - MovePromote, selected, ApplicationsMover.SwapAtomic, userApplicationsDir: userApps); - var plan = mover.Inspect(source); - - await Assert.That(selected).IsEqualTo(apps); - await Assert.That(plan.Target).IsEqualTo(Path.Combine(apps, "kcap.app")); - await Assert.That(plan.Action).IsEqualTo(ApplicationsInstallAction.Update); - await Assert.That(App.ShouldPromptToInstall(kind, source, plan)).IsTrue(); - var outcome = await mover.MoveAsync(source, CancellationToken.None); - await Assert.That(outcome.Moved).IsTrue(); - await Assert.That(outcome.InstalledPath).IsEqualTo(Path.Combine(apps, "kcap.app")); - await Assert.That(File.ReadAllText(Path.Combine(apps, "kcap.app", "Contents", "Info.plist"))).Contains("1.1.0"); - await Assert.That(File.Exists(Path.Combine(apps, "kcap.app", "Contents", "MacOS", "kcap-desktop"))).IsTrue(); - await Assert.That(Directory.Exists(installed)).IsFalse(); - await Assert.That(File.Exists(Path.Combine(installed, "Contents", "MacOS", "Kurrent Capacitor"))).IsFalse(); - } - - [Test] - [Arguments(false)] - [Arguments(true)] - public async Task Opening_a_canonical_install_keeps_its_folder_when_only_a_legacy_copy_is_elsewhere(bool perUser) { - var system = Tmp.CreateDir("SystemApplications"); - var user = Tmp.CreateDir("UserApplications"); - var home = perUser ? user : system; - var other = perUser ? system : user; - var source = Path.Combine(home, "kcap.app"); - var legacy = Path.Combine(other, "Kurrent Capacitor.app"); - CompleteBundle(source, "1.1.0"); - CompleteBundle(legacy); - File.Move(Path.Combine(legacy, "Contents", "MacOS", "kcap-desktop"), - Path.Combine(legacy, "Contents", "MacOS", "Kurrent Capacitor")); - - var selected = ApplicationsMover.ApplicationDirectories(source, user, system).Single(); - var mover = new ApplicationsMover(new FakeDitto(CompleteBundle), MovePromote, selected); - var plan = mover.Inspect(source); - - await Assert.That(selected).IsEqualTo(home); - await Assert.That(plan.Action).IsEqualTo(ApplicationsInstallAction.OpenInstalled); - await Assert.That(App.ShouldPromptToInstall( - perUser ? InstallLocationKind.UserApplications : InstallLocationKind.Applications, - source, plan)).IsFalse(); - await Assert.That(File.Exists(Path.Combine(legacy, "Contents", "MacOS", "Kurrent Capacitor"))).IsTrue(); - } - - [Test] - public async Task An_unverified_user_legacy_bundle_is_not_selected_for_upgrade() { + public async Task An_unverified_user_bundle_is_not_selected_for_update() { var systemApps = Tmp.CreateDir("SystemApplications"); var userApps = Tmp.CreateDir("UserApplications"); - CompleteBundle(Path.Combine(userApps, "Kurrent Capacitor.app"), "1.0.0", "another.application"); + CompleteBundle(Path.Combine(userApps, "kcap.app"), "1.0.0", "another.application"); var source = Tmp.PathTo("Downloads/kcap.app"); CompleteBundle(source, "1.1.0"); @@ -171,7 +84,7 @@ public async Task An_unverified_user_legacy_bundle_is_not_selected_for_upgrade() } [Test] - public async Task An_installed_kcap_without_a_legacy_copy_starts_without_an_install_prompt() { + public async Task An_installed_kcap_starts_without_an_install_prompt() { var systemApps = Tmp.CreateDir("SystemApplications"); var userApps = Tmp.CreateDir("UserApplications"); var source = Path.Combine(userApps, "kcap.app"); @@ -205,33 +118,6 @@ await Assert.That(App.ShouldPromptToInstall( perUser ? InstallLocationKind.UserApplications : InstallLocationKind.Applications, source, mover.Inspect(source))).IsFalse(); await Assert.That(ApplicationsMover.ApplicationDirectories(Tmp.PathTo("Downloads/kcap.app"), user, system).Count).IsEqualTo(2); - - CompleteBundle(Path.Combine(other, "Kurrent Capacitor.app")); - await Assert.That(ApplicationsMover.ApplicationDirectories(source, user, system).Single()).IsEqualTo(home); - CompleteBundle(Path.Combine(home, "Kurrent Capacitor.app")); - await Assert.That(ApplicationsMover.ApplicationDirectories(source, user, system).Count).IsEqualTo(2); - } - - [Test] - [Arguments(false)] - [Arguments(true)] - public async Task Migrating_one_of_two_legacy_copies_does_not_prompt_again_on_ordinary_launch(bool perUser) { - var system = Tmp.CreateDir("SystemApplications"); - var user = Tmp.CreateDir("UserApplications"); - var selected = perUser ? user : system; - var other = perUser ? system : user; - var source = Tmp.PathTo("Downloads/kcap.app"); - CompleteBundle(source, "1.1.0"); - CompleteBundle(Path.Combine(system, "Kurrent Capacitor.app")); - CompleteBundle(Path.Combine(user, "Kurrent Capacitor.app")); - await Assert.That(ApplicationsMover.ApplicationDirectories(source, user, system).Count).IsEqualTo(2); - var mover = new ApplicationsMover(new FakeDitto(root => CompleteBundle(root, "1.1.0")), MovePromote, selected); - var outcome = await mover.MoveAsync(source, CancellationToken.None); - await Assert.That(outcome.Moved).IsTrue(); - await Assert.That(Directory.Exists(Path.Combine(other, "Kurrent Capacitor.app"))).IsTrue(); - await Assert.That(ApplicationsMover.ApplicationDirectories(outcome.InstalledPath!, user, system).Single()).IsEqualTo(selected); - await Assert.That(App.ShouldPromptToInstall(perUser ? InstallLocationKind.UserApplications : InstallLocationKind.Applications, - outcome.InstalledPath!, mover.Inspect(outcome.InstalledPath!))).IsFalse(); } [Test] @@ -257,45 +143,45 @@ public async Task Both_installed_directories_and_a_conflicting_system_app_requir var user = Tmp.CreateDir("UserApplications"); var source = Tmp.PathTo("Downloads/kcap.app"); CompleteBundle(source, "1.1.0"); - CompleteBundle(Path.Combine(user, "Kurrent Capacitor.app")); + CompleteBundle(Path.Combine(user, "kcap.app")); CompleteBundle(Path.Combine(system, "kcap.app")); await Assert.That(ApplicationsMover.ApplicationDirectories(source, user, system).Count).IsEqualTo(2); CompleteBundle(Path.Combine(system, "kcap.app"), "1.0.0", "another.application"); await Assert.That(ApplicationsMover.ApplicationDirectories(source, user, system).Count).IsEqualTo(2); var userMover = new ApplicationsMover(new FakeDitto(CompleteBundle), MovePromote, user); - await Assert.That(userMover.Inspect(source).Migrates).IsTrue(); + await Assert.That(userMover.Inspect(source).Action).IsEqualTo(ApplicationsInstallAction.Update); var systemMover = new ApplicationsMover(new FakeDitto(CompleteBundle), MovePromote, system); await Assert.That(systemMover.Inspect(source).Action).IsEqualTo(ApplicationsInstallAction.Blocked); } [Test] - public async Task An_older_download_migrates_the_newer_legacy_bundle_without_downgrading() { + public async Task An_older_download_opens_the_newer_install_without_downgrading() { var apps = Tmp.CreateDir("Applications"); - var legacy = Path.Combine(apps, "Kurrent Capacitor.app"); - CompleteBundle(legacy, "2.0.0"); + var installed = Path.Combine(apps, "kcap.app"); + CompleteBundle(installed, "2.0.0"); var source = Tmp.PathTo("Downloads/kcap.app"); CompleteBundle(source, "1.0.0"); - var mover = new ApplicationsMover(new FakeDitto(root => CompleteBundle(root, "2.0.0")), MovePromote, apps); - await Assert.That(mover.Inspect(source).PayloadPath).IsEqualTo(legacy); + var ditto = new FakeDitto(root => CompleteBundle(root, "1.0.0")); + var mover = new ApplicationsMover(ditto, MovePromote, apps); + await Assert.That(mover.Inspect(source).Action).IsEqualTo(ApplicationsInstallAction.OpenInstalled); var outcome = await mover.MoveAsync(source, CancellationToken.None); await Assert.That(outcome.Moved).IsTrue(); - await Assert.That(File.ReadAllText(Path.Combine(apps, "kcap.app", "Contents", "Info.plist"))).Contains("2.0.0"); - await Assert.That(Directory.Exists(legacy)).IsFalse(); + await Assert.That(ditto.Calls).IsEqualTo(0); + await Assert.That(File.ReadAllText(Path.Combine(installed, "Contents", "Info.plist"))).Contains("2.0.0"); } [Test] - public async Task A_failed_launch_restores_the_legacy_bundle_and_does_not_remove_the_download() { + public async Task A_failed_launch_restores_the_installed_bundle_and_does_not_remove_the_download() { var apps = Tmp.CreateDir("Applications"); - var legacy = Tmp.PathTo("Applications/Kurrent Capacitor.app"); - CompleteBundle(legacy, "1.0.0"); + var installed = Tmp.PathTo("Applications/kcap.app"); + CompleteBundle(installed, "1.0.0"); var source = Tmp.PathTo("Downloads/kcap.app"); CompleteBundle(source, "1.1.0"); var mover = new ApplicationsMover(new FakeDitto(root => CompleteBundle(root, "1.1.0")), MovePromote, apps, - activate: (_, _) => Task.FromResult(false)); + MoveSwap, activate: (_, _) => Task.FromResult(false)); var outcome = await mover.MoveAsync(source, CancellationToken.None); await Assert.That(outcome.Moved).IsFalse(); - await Assert.That(Directory.Exists(legacy)).IsTrue(); - await Assert.That(Directory.Exists(Path.Combine(apps, "kcap.app"))).IsFalse(); + await Assert.That(File.ReadAllText(Path.Combine(installed, "Contents", "Info.plist"))).Contains("1.0.0"); await Assert.That(Directory.Exists(source)).IsTrue(); await Assert.That(Directory.GetDirectories(apps).Length).IsEqualTo(1); } diff --git a/test/Capacitor.App.Tests.Unit/CliResolverTests.cs b/test/Capacitor.App.Tests.Unit/CliResolverTests.cs index 65f3d823bc..4a35a9e7ac 100644 --- a/test/Capacitor.App.Tests.Unit/CliResolverTests.cs +++ b/test/Capacitor.App.Tests.Unit/CliResolverTests.cs @@ -79,7 +79,7 @@ public async Task ResolvePath_override_wins_over_the_bundle_sibling() { [Test] public async Task ResolvePath_finds_the_windows_sibling_by_its_exe_name() { - const string dir = @"C:\Users\me\AppData\Local\KurrentCapacitor\current"; + const string dir = @"C:\Users\me\AppData\Local\kcap\current"; var sibling = Path.Combine(dir, "kcap.exe"); var path = CliResolver.ResolvePath(_ => null, p => p == sibling, dir, "kcap.exe"); diff --git a/test/Capacitor.App.Tests.Unit/PrereleaseFilteringSourceTests.cs b/test/Capacitor.App.Tests.Unit/PrereleaseFilteringSourceTests.cs index 52995f6a18..20182ba851 100644 --- a/test/Capacitor.App.Tests.Unit/PrereleaseFilteringSourceTests.cs +++ b/test/Capacitor.App.Tests.Unit/PrereleaseFilteringSourceTests.cs @@ -5,7 +5,7 @@ namespace Capacitor.App.Tests.Unit; public class PrereleaseFilteringSourceTests { static VelopackAsset Asset(string version) => - new() { PackageId = "KurrentCapacitor", Version = SemanticVersion.Parse(version), Type = VelopackAssetType.Full, FileName = $"KurrentCapacitor-{version}-osx-arm64-full.nupkg" }; + new() { PackageId = "kcap", Version = SemanticVersion.Parse(version), Type = VelopackAssetType.Full, FileName = $"kcap-{version}-osx-arm64-full.nupkg" }; [Test] public async Task Stable_install_drops_prereleases() { diff --git a/test/Capacitor.App.Tests.Unit/SingleInstanceGuardTests.cs b/test/Capacitor.App.Tests.Unit/SingleInstanceGuardTests.cs index 2c681f6c34..426163795d 100644 --- a/test/Capacitor.App.Tests.Unit/SingleInstanceGuardTests.cs +++ b/test/Capacitor.App.Tests.Unit/SingleInstanceGuardTests.cs @@ -72,8 +72,8 @@ public async Task A_relaunch_waits_for_the_old_copy_to_exit_and_then_takes_the_l [Test] public async Task The_lock_name_ignores_path_case_and_carries_no_backslash() { - var lower = SingleInstanceGuard.NameFor(@"C:\Users\someone\AppData\Local\KurrentCapacitor\current\kcap-desktop.exe"); - var upper = SingleInstanceGuard.NameFor(@"C:\USERS\SOMEONE\APPDATA\LOCAL\KURRENTCAPACITOR\CURRENT\KCAP-DESKTOP.EXE"); + var lower = SingleInstanceGuard.NameFor(@"C:\Users\someone\AppData\Local\kcap\current\kcap-desktop.exe"); + var upper = SingleInstanceGuard.NameFor(@"C:\USERS\SOMEONE\APPDATA\LOCAL\KCAP\CURRENT\KCAP-DESKTOP.EXE"); var other = SingleInstanceGuard.NameFor(@"C:\src\kcap-cli\src\Capacitor.App\bin\Debug\net10.0\kcap-desktop.exe"); await Assert.That(upper).IsEqualTo(lower); diff --git a/test/Capacitor.App.Tests.Unit/TerminalAdviceTests.cs b/test/Capacitor.App.Tests.Unit/TerminalAdviceTests.cs index d2a6a6d61c..389e662c0b 100644 --- a/test/Capacitor.App.Tests.Unit/TerminalAdviceTests.cs +++ b/test/Capacitor.App.Tests.Unit/TerminalAdviceTests.cs @@ -42,8 +42,8 @@ public class TerminalAdviceTests { "The kcap on PATH (/usr/local/bin/kcap) is 1.2.0, and a local installation needs a kcap newer than 1.3.0. Update or remove it before setting up a local installation.", "The kcap on PATH (/usr/local/bin/kcap) is 1.2.0, and a local installation needs a kcap newer than 1.3.0. Update or remove it before setting up a local installation."); yield return () => ( - "The desktop app's kcap (/Applications/Kurrent Capacitor.app/Contents/MacOS/kcap) is 1.2.0, and a local installation needs a kcap newer than 1.3.0. Update the desktop app before setting up a local installation.", - "The desktop app's kcap (/Applications/Kurrent Capacitor.app/Contents/MacOS/kcap) is 1.2.0, and a local installation needs a kcap newer than 1.3.0. Update the desktop app before setting up a local installation."); + "The desktop app's kcap (/Applications/kcap.app/Contents/MacOS/kcap) is 1.2.0, and a local installation needs a kcap newer than 1.3.0. Update the desktop app before setting up a local installation.", + "The desktop app's kcap (/Applications/kcap.app/Contents/MacOS/kcap) is 1.2.0, and a local installation needs a kcap newer than 1.3.0. Update the desktop app before setting up a local installation."); yield return () => ( "The kcap on PATH (/usr/local/bin/kcap) did not report its version, and a local installation needs a kcap newer than 1.3.0. Update or remove it before setting up a local installation.", "The kcap on PATH (/usr/local/bin/kcap) did not report its version, and a local installation needs a kcap newer than 1.3.0. Update or remove it before setting up a local installation."); diff --git a/test/Capacitor.App.Tests.Unit/WindowsDesktopNotificationAccessTests.cs b/test/Capacitor.App.Tests.Unit/WindowsDesktopNotificationAccessTests.cs index 7fcfd94236..86d24ad255 100644 --- a/test/Capacitor.App.Tests.Unit/WindowsDesktopNotificationAccessTests.cs +++ b/test/Capacitor.App.Tests.Unit/WindowsDesktopNotificationAccessTests.cs @@ -7,8 +7,8 @@ namespace Capacitor.App.Tests.Unit; [RunOn(OS.Windows)] [SupportedOSPlatform("windows")] public class WindowsDesktopNotificationAccessTests { - const string App = "io.kurrent.capacitor"; - const string AppKey = @"Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\io.kurrent.capacitor"; + const string App = "io.kurrent.kcap"; + const string AppKey = @"Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\io.kurrent.kcap"; const string GlobalKey = @"Software\Microsoft\Windows\CurrentVersion\PushNotifications"; static WindowsDesktopNotificationAccess With(Dictionary<(string, string), object> values) => diff --git a/test/Capacitor.Cli.Tests.Unit/Commands/DaemonCommandsServiceUninstallUnderTests.cs b/test/Capacitor.Cli.Tests.Unit/Commands/DaemonCommandsServiceUninstallUnderTests.cs index f354034202..def14a242a 100644 --- a/test/Capacitor.Cli.Tests.Unit/Commands/DaemonCommandsServiceUninstallUnderTests.cs +++ b/test/Capacitor.Cli.Tests.Unit/Commands/DaemonCommandsServiceUninstallUnderTests.cs @@ -21,11 +21,11 @@ DaemonServiceCommands Commands(IServiceManager manager, string id = "unused") => [Test] public async Task Removes_only_units_whose_daemon_is_inside_the_folder() { - var app = Path.GetFullPath("/apps/KurrentCapacitor/current"); + var app = Path.GetFullPath("/apps/kcap/current"); var manager = new RecordingServiceManager(new() { ["laptop"] = Path.Combine(app, "kcap-daemon.exe"), ["npm"] = Path.GetFullPath("/npm/node_modules/@kurrent/kcap/bin/kcap-daemon.exe"), - ["near"] = Path.GetFullPath("/apps/KurrentCapacitor/current-old/kcap-daemon.exe"), + ["near"] = Path.GetFullPath("/apps/kcap/current-old/kcap-daemon.exe"), ["blank"] = null, }); using var console = ConsoleOutput.StartFullCapture(); @@ -41,7 +41,7 @@ public async Task Removes_only_units_whose_daemon_is_inside_the_folder() { [Test] public async Task A_running_daemon_is_asked_to_end_its_agents_before_its_unit_is_removed() { Skip.When(OperatingSystem.IsWindows(), "the control socket is a unix socket"); - var app = Path.GetFullPath("/apps/KurrentCapacitor/current"); + var app = Path.GetFullPath("/apps/kcap/current"); using var child = DummyProcess.StartSleep(120); File.WriteAllText(Daemons.Store.PidPath("laptop"), $"{child.Pid}\n{ProcessStartToken.ForPid(child.Pid)}\n"); // Set before the stand-in dies: a bootout that found it gone came after the request. @@ -124,7 +124,7 @@ public async Task Uninstall_of_a_daemon_that_does_not_answer_marks_its_ledger_in [Test] public async Task UninstallUnder_of_a_daemon_that_does_not_answer_marks_its_ledger_interrupted() { Skip.When(OperatingSystem.IsWindows(), "the control socket is a unix socket"); - var app = Path.GetFullPath("/apps/KurrentCapacitor/current"); + var app = Path.GetFullPath("/apps/kcap/current"); using var child = DummyProcess.StartSleep(120); File.WriteAllText(Daemons.Store.PidPath("laptop"), $"{child.Pid}\n{ProcessStartToken.ForPid(child.Pid)}\n"); var ledger = ResumeLedgerStore.For(Daemons.Store, "laptop"); @@ -140,7 +140,7 @@ public async Task UninstallUnder_of_a_daemon_that_does_not_answer_marks_its_ledg [Test] public async Task A_unit_another_operation_holds_is_left_and_reported() { - var app = Path.GetFullPath("/apps/KurrentCapacitor/current"); + var app = Path.GetFullPath("/apps/kcap/current"); var manager = new RecordingServiceManager(new() { ["laptop"] = Path.Combine(app, "kcap-daemon.exe") }); using var held = await ServiceTxnLock.TryAcquireAsync(Daemons.Store, "laptop", TimeSpan.Zero, TimeProvider.System); using var console = ConsoleOutput.StartFullCapture(); @@ -154,7 +154,7 @@ public async Task A_unit_another_operation_holds_is_left_and_reported() { [Test] public async Task A_unit_repointed_while_the_lock_was_held_is_left_alone() { - var app = Path.GetFullPath("/apps/KurrentCapacitor/current"); + var app = Path.GetFullPath("/apps/kcap/current"); var binaries = new Dictionary { ["laptop"] = Path.Combine(app, "kcap-daemon.exe") }; var manager = new RecordingServiceManager(binaries); var held = await ServiceTxnLock.TryAcquireAsync(Daemons.Store, "laptop", TimeSpan.Zero, TimeProvider.System); @@ -172,7 +172,7 @@ public async Task A_unit_repointed_while_the_lock_was_held_is_left_alone() { [Test] public async Task A_unit_that_cannot_be_read_does_not_stop_the_others() { - var app = Path.GetFullPath("/apps/KurrentCapacitor/current"); + var app = Path.GetFullPath("/apps/kcap/current"); var manager = new RecordingServiceManager(new() { ["broken"] = Unreadable, ["laptop"] = Path.Combine(app, "kcap-daemon.exe"), @@ -190,7 +190,7 @@ public async Task A_unit_that_cannot_be_read_does_not_stop_the_others() { /// retrying its deleted daemon at every sign-in. [Test] public async Task A_listing_that_fails_is_reported_not_taken_as_no_units() { - var app = Path.GetFullPath("/apps/KurrentCapacitor/current"); + var app = Path.GetFullPath("/apps/kcap/current"); var manager = new RecordingServiceManager(new() { ["laptop"] = Path.Combine(app, "kcap-daemon.exe") }) { ListingFails = true }; using var console = ConsoleOutput.StartFullCapture(); diff --git a/test/Capacitor.Cli.Tests.Unit/Commands/DaemonShimCommandsTests.cs b/test/Capacitor.Cli.Tests.Unit/Commands/DaemonShimCommandsTests.cs index b7151d44f3..046a7cc60c 100644 --- a/test/Capacitor.Cli.Tests.Unit/Commands/DaemonShimCommandsTests.cs +++ b/test/Capacitor.Cli.Tests.Unit/Commands/DaemonShimCommandsTests.cs @@ -88,7 +88,7 @@ public async Task ResolveLinkTarget_on_Windows_uses_the_npm_prefix_shim_for_a_fl [Test] public async Task ResolveLinkTarget_on_Windows_uses_the_running_binary_outside_npm() { - var native = Path.GetFullPath("/Users/someone/AppData/Local/KurrentCapacitor/current/kcap.exe"); + var native = Path.GetFullPath("/Users/someone/AppData/Local/kcap/current/kcap.exe"); var target = DaemonShimCommands.ResolveLinkTarget(() => native, _ => false, windows: true); await Assert.That(target).IsEqualTo(native); } diff --git a/test/Capacitor.Cli.Tests.Unit/Commands/LocalSetupJsonLinesTests.cs b/test/Capacitor.Cli.Tests.Unit/Commands/LocalSetupJsonLinesTests.cs index fb8d1052c2..7ae100eed2 100644 --- a/test/Capacitor.Cli.Tests.Unit/Commands/LocalSetupJsonLinesTests.cs +++ b/test/Capacitor.Cli.Tests.Unit/Commands/LocalSetupJsonLinesTests.cs @@ -26,8 +26,8 @@ internal sealed class LocalSetupJsonLinesTests { """{"v":1,"event":"verified","sha256":"ab12"}"""); yield return () => (new LocalInstallEvent.Unpacked("/r/bin/1.3.0"), """{"v":1,"event":"unpacked","path":"/r/bin/1.3.0"}"""); - yield return () => (new LocalInstallEvent.Starting("io.kurrent.capacitor.local"), - """{"v":1,"event":"starting","unit":"io.kurrent.capacitor.local"}"""); + yield return () => (new LocalInstallEvent.Starting("io.kurrent.kcap.local"), + """{"v":1,"event":"starting","unit":"io.kurrent.kcap.local"}"""); yield return () => (new LocalInstallEvent.Ready("http://localhost:9473"), """{"v":1,"event":"ready","server_url":"http://localhost:9473"}"""); yield return () => (new LocalInstallEvent.Committing(), diff --git a/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandClaudeTests.cs b/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandClaudeTests.cs index 953619b5ef..8b55f42d68 100644 --- a/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandClaudeTests.cs +++ b/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandClaudeTests.cs @@ -105,8 +105,8 @@ public async Task Same_version_refresh_relocates_the_marketplace_and_an_existing using var home = new TempDir(); using var plugin = new TempDir(); var settings = home.PathTo(".claude", "settings.json"); - SetupCommand.InstallPlugin(settings, "/Applications/Kurrent Capacitor.app/Contents/Resources/kcap"); - var old = new GitHookInstaller(new UserHome(home.Path), () => "/Applications/Kurrent Capacitor.app/Contents/MacOS/kcap"); + SetupCommand.InstallPlugin(settings, "/Users/someone/Applications/kcap.app/Contents/Resources/kcap"); + var old = new GitHookInstaller(new UserHome(home.Path), () => "/Users/someone/Applications/kcap.app/Contents/MacOS/kcap"); old.Install(); var env = TestEnv(home.Path, plugin.Path) with { ResolveMcpBinaryPath = () => "/Applications/kcap.app/Contents/MacOS/kcap" }; var exit = await new PluginCommand(env, new WorkingDirectory(home.Path)).HandleAsync( @@ -114,7 +114,7 @@ public async Task Same_version_refresh_relocates_the_marketplace_and_an_existing await Assert.That(exit).IsEqualTo(0); await Assert.That(JsonNode.Parse(File.ReadAllText(settings))!["extraKnownMarketplaces"]!["kcap"]!["source"]!["path"]!.GetValue()) .IsEqualTo(plugin.Path); - await Assert.That(File.ReadAllText(home.PathTo(".gitconfig"))).DoesNotContain("Kurrent Capacitor.app"); + await Assert.That(File.ReadAllText(home.PathTo(".gitconfig"))).DoesNotContain("/Users/someone/Applications/kcap.app"); await Assert.That(File.ReadAllText(home.PathTo(".gitconfig"))).Contains("/Applications/kcap.app/Contents/MacOS/kcap"); } diff --git a/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandCodexTests.cs b/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandCodexTests.cs index f5c853daa9..58a166e8ab 100644 --- a/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandCodexTests.cs +++ b/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandCodexTests.cs @@ -674,7 +674,7 @@ public async Task InstallCodex_if_installed_leaves_config_toml_alone_when_never_ public async Task Refresh_relocates_owned_MCP_without_adding_recording_hooks_or_changing_custom_entries() { using var home = new TempDir(); var config = home.GetResolvedPath(".codex", "config.toml"); - CodexConfigToml.RegisterKcapMcpServers(config, () => "/Applications/Kurrent Capacitor.app/Contents/MacOS/kcap"); + CodexConfigToml.RegisterKcapMcpServers(config, () => "/Users/someone/Applications/kcap.app/Contents/MacOS/kcap"); File.AppendAllText(config, "\n[mcp_servers.user-tool]\ncommand = \"custom\"\n"); var env = TestEnv(home.GetResolvedPath()) with { ResolveMcpBinaryPath = () => "/Applications/kcap.app/Contents/MacOS/kcap" }; var exit = await new PluginCommand(env, new WorkingDirectory(home.GetResolvedPath())).HandleAsync( @@ -682,7 +682,7 @@ public async Task Refresh_relocates_owned_MCP_without_adding_recording_hooks_or_ await Assert.That(exit).IsEqualTo(0); var text = File.ReadAllText(config); await Assert.That(text).Contains("/Applications/kcap.app/Contents/MacOS/kcap"); - await Assert.That(text).DoesNotContain("Kurrent Capacitor.app"); + await Assert.That(text).DoesNotContain("/Users/someone/Applications/kcap.app"); await Assert.That(text).Contains("custom"); await Assert.That(File.Exists(home.GetResolvedPath(".codex", "hooks.json"))).IsFalse(); } diff --git a/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandToolsOnlyTests.cs b/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandToolsOnlyTests.cs index ed8df95999..ed4e0e85c4 100644 --- a/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandToolsOnlyTests.cs +++ b/test/Capacitor.Cli.Tests.Unit/Commands/PluginCommandToolsOnlyTests.cs @@ -79,11 +79,11 @@ public async Task Cursor_refresh_relocates_MCP_even_when_recording_hooks_are_alr var cursor = env.Harnesses.Of().Paths; PluginCommand.InstallCursorHooks(cursor.UserHooksJson); HarnessMcpProjections.Cursor.Register(cursor.UserMcpJson, env.Home, - resolveBinaryPath: () => "/Applications/Kurrent Capacitor.app/Contents/MacOS/kcap"); + resolveBinaryPath: () => "/Users/someone/Applications/kcap.app/Contents/MacOS/kcap"); var hooks = File.ReadAllText(cursor.UserHooksJson); await Assert.That(await Run(env, "plugin", "install", "--cursor", "--if-installed", "--strict-refresh")).IsEqualTo(0); await Assert.That(File.ReadAllText(cursor.UserMcpJson)).Contains(TestBinaryPath); - await Assert.That(File.ReadAllText(cursor.UserMcpJson)).DoesNotContain("Kurrent Capacitor.app"); + await Assert.That(File.ReadAllText(cursor.UserMcpJson)).DoesNotContain("/Users/someone/Applications/kcap.app"); await Assert.That(File.ReadAllText(cursor.UserHooksJson)).IsEqualTo(hooks); } @@ -237,7 +237,7 @@ public async Task Vibe_tools_only_writes_mcp_and_skills_but_no_hooks() { public async Task Plain_refresh_of_a_tools_only_vibe_install_heals_mcp_without_adding_hooks() { var env = Env(); var vibe = env.Harnesses.Of().Paths; - await Run(env with { ResolveMcpBinaryPath = () => "/Applications/Kurrent Capacitor.app/Contents/MacOS/kcap" }, + await Run(env with { ResolveMcpBinaryPath = () => "/Users/someone/Applications/kcap.app/Contents/MacOS/kcap" }, "plugin", "install", "--mistral-vibe", "--tools-only"); var config = TomlSerializer.Deserialize(await File.ReadAllTextAsync(vibe.ConfigToml))!; var servers = (TomlTableArray)config["mcp_servers"]; @@ -247,7 +247,7 @@ await Run(env with { ResolveMcpBinaryPath = () => "/Applications/Kurrent Capacit await Assert.That(await Run(env, "plugin", "install", "--mistral-vibe", "--if-installed", "--strict-refresh")).IsEqualTo(0); await Assert.That(await File.ReadAllTextAsync(vibe.ConfigToml)).Contains("kcap-sessions"); - await Assert.That(await File.ReadAllTextAsync(vibe.ConfigToml)).DoesNotContain("Kurrent Capacitor.app"); + await Assert.That(await File.ReadAllTextAsync(vibe.ConfigToml)).DoesNotContain("/Users/someone/Applications/kcap.app"); await Assert.That(await File.ReadAllTextAsync(vibe.ConfigToml)).Contains(TestBinaryPath); await Assert.That(File.Exists(vibe.HooksToml)).IsFalse(); } diff --git a/test/Capacitor.Cli.Tests.Unit/Commands/PluginPathTests.cs b/test/Capacitor.Cli.Tests.Unit/Commands/PluginPathTests.cs index 80ccd86093..6087ec954d 100644 --- a/test/Capacitor.Cli.Tests.Unit/Commands/PluginPathTests.cs +++ b/test/Capacitor.Cli.Tests.Unit/Commands/PluginPathTests.cs @@ -11,7 +11,7 @@ public class PluginPathTests { [Test] [Arguments("Capacitor.app/Contents/MacOS/kcap", "Capacitor.app/Contents/Resources/kcap")] - [Arguments("KurrentCapacitor/current/kcap.exe", "KurrentCapacitor/current/kcap")] + [Arguments("kcap/current/kcap.exe", "kcap/current/kcap")] public async Task Bundled_CLI_finds_and_installs_the_app_plugin_without_npm(string executablePath, string pluginPath) { var plugin = Tmp.CreateDir(pluginPath.Split('/')); plugin.CreateFile([".claude-plugin", "marketplace.json"], """{"name":"kcap","plugins":[{"name":"kcap","source":"./"}]}"""); diff --git a/test/Capacitor.Cli.Tests.Unit/LocalEdition/LocalClientCopiesTests.cs b/test/Capacitor.Cli.Tests.Unit/LocalEdition/LocalClientCopiesTests.cs index e18b0d86c7..b1452958bf 100644 --- a/test/Capacitor.Cli.Tests.Unit/LocalEdition/LocalClientCopiesTests.cs +++ b/test/Capacitor.Cli.Tests.Unit/LocalEdition/LocalClientCopiesTests.cs @@ -28,7 +28,7 @@ public Task RunStreamingAsync(string fileName, string[] args, R [Test] public async Task Finds_the_kcap_on_path_and_the_desktop_apps_with_their_versions() { var onPath = Kcap("bin"); - var app = Kcap("Kurrent Capacitor.app/Contents/MacOS"); + var app = Kcap("kcap.app/Contents/MacOS"); var runner = new FakeRunner(new() { [onPath] = Prints("1.2.0"), [app] = Prints("1.3.0+abc") }); var copies = new LocalClientCopies(runner, Tmp.PathTo("bin"), self: null, [app, Tmp.PathTo("Elsewhere.app/Contents/MacOS/kcap")]).Find(); @@ -40,21 +40,6 @@ await Assert.That(copies).IsEquivalentTo([ await Assert.That(runner.Ran).Contains($"{onPath} --version --no-update-check"); } - [Test] - public async Task Finds_both_rebranded_and_legacy_desktop_copies() { - var current = Kcap("Applications/kcap.app/Contents/MacOS"); - var legacy = Kcap("Applications/Kurrent Capacitor.app/Contents/MacOS"); - var runner = new FakeRunner(new() { [current] = Prints("1.3.0"), [legacy] = Prints("1.2.0") }); - - var copies = new LocalClientCopies(runner, searchPath: null, self: null, - LocalClientCopies.AppClis(Tmp.PathTo("Applications"))).Find(); - - await Assert.That(copies).IsEquivalentTo([ - new ClientCopy(ClientCopyKind.DesktopApp, current, "1.3.0"), - new ClientCopy(ClientCopyKind.DesktopApp, legacy, "1.2.0") - ]); - } - [Test] public async Task The_running_kcap_is_not_asked_even_through_a_link() { var self = Kcap("real"); @@ -70,7 +55,7 @@ public async Task The_running_kcap_is_not_asked_even_through_a_link() { [Test] public async Task A_path_link_to_the_desktop_apps_kcap_is_one_copy_of_the_app() { - var app = Kcap("Kurrent Capacitor.app/Contents/MacOS"); + var app = Kcap("kcap.app/Contents/MacOS"); Tmp.CreateDir("bin"); File.CreateSymbolicLink(Tmp.PathTo("bin", "kcap"), app); var runner = new FakeRunner(new() { [app] = Prints("1.2.0") }); diff --git a/test/Capacitor.Cli.Tests.Unit/LocalEdition/LocalPreflightTests.cs b/test/Capacitor.Cli.Tests.Unit/LocalEdition/LocalPreflightTests.cs index f9e0aefc33..845b631b5f 100644 --- a/test/Capacitor.Cli.Tests.Unit/LocalEdition/LocalPreflightTests.cs +++ b/test/Capacitor.Cli.Tests.Unit/LocalEdition/LocalPreflightTests.cs @@ -296,7 +296,7 @@ public async Task Only_an_Apple_silicon_Mac_supports_the_local_edition(bool mac, public async Task Copies_newer_than_the_floor_pass() { var plan = Preflight(copies: [ new(ClientCopyKind.OnPath, "/usr/local/bin/kcap", "1.3.1-alpha.0.1"), - new(ClientCopyKind.DesktopApp, "/Applications/Kurrent Capacitor.app/Contents/MacOS/kcap", "9.0.0"), + new(ClientCopyKind.DesktopApp, "/Applications/kcap.app/Contents/MacOS/kcap", "9.0.0"), ]).Check(Version); await Assert.That(plan.Hint).IsEqualTo(PlanHint.Install); @@ -314,11 +314,11 @@ await Assert.That(failure.Message).IsEqualTo( [Test] public async Task The_desktop_apps_kcap_below_the_floor_is_refused_naming_the_app() { var failure = await Refusal(() => Preflight(copies: - new ClientCopy(ClientCopyKind.DesktopApp, "/Applications/Kurrent Capacitor.app/Contents/MacOS/kcap", "1.3.0")).Check(Version)); + new ClientCopy(ClientCopyKind.DesktopApp, "/Applications/kcap.app/Contents/MacOS/kcap", "1.3.0")).Check(Version)); await Assert.That(failure.Code).IsEqualTo(LocalInstallFailureCode.ClientBelowFloor); await Assert.That(failure.Message).IsEqualTo( - "The desktop app's kcap (/Applications/Kurrent Capacitor.app/Contents/MacOS/kcap) is 1.3.0, and a local installation needs a kcap newer than 1.3.0. Update the desktop app before setting up a local installation."); + "The desktop app's kcap (/Applications/kcap.app/Contents/MacOS/kcap) is 1.3.0, and a local installation needs a kcap newer than 1.3.0. Update the desktop app before setting up a local installation."); } [Test] From a7041db3a033f2e5aeb2d790c1a02a1c05be57a4 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 21 Oct 2026 11:39:15 +0200 Subject: [PATCH 2/2] Pack full-only when the delta baseline carries another pack ID (#1761) Co-Authored-By: Claude Opus 5.5 --- scripts/desktop-baseline.sh | 10 ++++++++-- scripts/desktop-baseline.test.sh | 12 ++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/desktop-baseline.sh b/scripts/desktop-baseline.sh index e9617a3c56..2fdf716c07 100755 --- a/scripts/desktop-baseline.sh +++ b/scripts/desktop-baseline.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # `vpk download` leaves the channel's highest full package in the releases dir (no manifest), and -# `vpk pack` refuses a version at or below that baseline. Keep the package only when it is strictly -# below the candidate; otherwise delete it so the pack runs full-only, without a delta. +# `vpk pack` refuses a version at or below that baseline. Keep the package only when it carries the +# kcap pack ID and is strictly below the candidate; otherwise delete it so the pack runs full-only. # Usage: desktop-baseline.sh set -euo pipefail here="$(cd "$(dirname "$0")" && pwd)" @@ -18,6 +18,12 @@ if [ "${#pkgs[@]}" -eq 0 ]; then echo "no baseline package; packing full-only"; [ "${#pkgs[@]}" -eq 1 ] || { echo "expected one full package in $dir, found ${#pkgs[@]}" >&2; exit 1; } pkg="${pkgs[0]}" +id="$(unzip -p "$pkg" '*.nuspec' | grep -oE '[^<]+' | head -1 | sed -E 's###g')" +if [ "$id" != kcap ]; then + rm -f "$pkg" + echo "baseline package id '$id' is not kcap; discarded, packing full-only" + exit 0 +fi base="$(unzip -p "$pkg" '*.nuspec' | grep -oE '[^<]+' | head -1 | sed -E 's###g')" [ -n "$base" ] || { echo "no in the nuspec of $pkg" >&2; exit 1; } diff --git a/scripts/desktop-baseline.test.sh b/scripts/desktop-baseline.test.sh index f7df43ca97..ad358a520e 100755 --- a/scripts/desktop-baseline.test.sh +++ b/scripts/desktop-baseline.test.sh @@ -4,10 +4,10 @@ here="$(cd "$(dirname "$0")" && pwd)" sh="$here/desktop-baseline.sh" tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT -make_pkg() { # — the real download layout: one full nupkg, no manifest beside it - local dir="$1" v="$2" work; work="$(mktemp -d)" - printf 'kcap%s' "$v" > "$work/kcap.nuspec" - (cd "$work" && zip -q "$dir/kcap-$v-osx-arm64-full.nupkg" kcap.nuspec) +make_pkg() { # [id] — the real download layout: one full nupkg, no manifest beside it + local dir="$1" v="$2" id="${3:-kcap}" work; work="$(mktemp -d)" + printf '%s%s' "$id" "$v" > "$work/$id.nuspec" + (cd "$work" && zip -q "$dir/$id-$v-osx-arm64-full.nupkg" "$id.nuspec") rm -rf "$work" } @@ -26,6 +26,10 @@ d="$tmp/equal"; mkdir -p "$d"; make_pkg "$d" "0.12.2" bash "$sh" "$d" "0.12.2" >/dev/null [ "$(count "$d")" = "0" ] || { echo "FAIL: equal baseline should be deleted"; fail=1; } +d="$tmp/other-id"; mkdir -p "$d"; make_pkg "$d" "0.12.1" "SomeOtherApp" +bash "$sh" "$d" "0.12.2" >/dev/null +[ "$(count "$d")" = "0" ] || { echo "FAIL: a baseline under another pack id should be deleted"; fail=1; } + d="$tmp/empty"; mkdir -p "$d" bash "$sh" "$d" "0.12.2" >/dev/null || { echo "FAIL: empty download should be a no-op"; fail=1; }