Skip to content

Update script installs in place and add kcap refresh - #1340

Merged
alexeyzimarev merged 2 commits into
mainfrom
script-install-self-update
Oct 7, 2026
Merged

alexeyzimarev merged 2 commits into
mainfrom
script-install-self-update

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Part of #1227 — AI-3354

What & why

A script install can update itself: kcap update reads the installer's channel manifest, checks the archive's sha256 against it (a mismatch installs nothing), extracts it into versions/<v> and switches current. One kcap refresh replaces the refresh lists the npm launcher and the install scripts each carried; refresh.js now just calls it.

kcap-web follow-up: in install.sh and install.ps1, replace the TODO(#1227) plugin install --if-installed loop with "$kcap" refresh, then point the quickstart and setup-cli docs at the script installer.

Where to look

  • A daemon unit installed from a script install pins versions/<v>/bin/kcap-daemon. daemon service refresh moves it onto current: launchd reloads only an idle daemon, systemd only rewrites the unit and runs daemon-reload, so the new path applies on the next start.
  • RestartCoordinator watches the binary through current, so a link switch triggers the idle restart.

Verification

  • dotnet build Capacitor.slnx: 0 warnings. dotnet publish -c Release of kcap and kcap-daemon: no IL2026/IL3050.
  • New classes green, including a checksum mismatch, a malformed digest and a missing RID each leaving current untouched, and the launchd/systemd repoint.
  • Full Core and CLI unit suites: 6 local failures (NudgeLeaseTests, RepositoryDetectionTrackedBranchTests, one Core test), all green alone.
  • node npm/kcap/bin/{refresh,kcap,resolve}.test.js: ok.
  • Not run: a real script-install update end to end, or install.ps1 on Windows.

🤖 Generated with Claude Code

A daemon unit written from a script install pins its version directory, so service refresh moves it onto the current link; otherwise the daemon would never follow an update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T10:06:28.405237Z d98d518 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add verified self-updates for script installs and unify integration refresh

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Let script installs update from verified release archives without npm, preserving running
 versions.
• Centralize agent-integration refresh in kcap refresh for installers and updates.
• Move daemon units and persisted binary paths onto current so they follow subsequent updates.
Diagram

graph TD
  CLI["kcap update"] --> Feed["Release feed"] --> Archive["Verified archive"] --> Store[("Version store")] --> Refresh["kcap refresh"] --> Agents["Agent integrations"]
  Refresh --> Units["Daemon units"]
  Store --> Units
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Re-run the install script for updates
  • ➕ Reuses the existing archive-install procedure.
  • ➕ Avoids implementing archive activation in the CLI.
  • ➖ Requires invoking an external installer from the CLI.
  • ➖ Makes update checks, server version caps, and refresh behavior harder to keep consistent.
2. Keep refresh logic in each installer
  • ➕ Requires fewer changes to the native CLI.
  • ➖ Integration lists can drift.
  • ➖ Script installs and npm installs retain separate refresh behavior.

Recommendation: Keep the native updater and single refresh command: they give script installs an npm-free update path and one integration list. Review the Windows junction switch and archive activation especially carefully, since neither received a real end-to-end verification in the PR.

Files changed (51) +1852 / -456

Enhancement (23) +834 / -71
CapacitorHttpServices.csRegister release-download HTTP client +7/-0

Register release-download HTTP client

• Adds a typed release client with an archive-sized timeout and CLI user-agent.

src/Capacitor.Cli.Core/Http/CapacitorHttpServices.cs

IReleaseFeed.csAbstract channel release checks +7/-0

Abstract channel release checks

• Defines a shared feed contract for npm dist-tags and script-installer channel manifests.

src/Capacitor.Cli.Core/Http/IReleaseFeed.cs

KcapReleaseClient.csFetch installer manifests and release archives +64/-0

Fetch installer manifests and release archives

• Reads channel and version manifests from the installer host and streams platform archives from GitHub Releases.

src/Capacitor.Cli.Core/Http/KcapReleaseClient.cs

ReleaseManifest.csParse and validate release manifests +44/-0

Parse and validate release manifests

• Extracts release versions and per-RID checksums, validating version and SHA-256 formats before installation.

src/Capacitor.Cli.Core/Install/ReleaseManifest.cs

ScriptInstallLayout.csRecognize and stabilize script-install paths +76/-0

Recognize and stabilize script-install paths

• Detects marked script-install layouts and maps version-pinned binary paths to their 'current/bin' equivalents when present.

src/Capacitor.Cli.Core/Install/ScriptInstallLayout.cs

KcapBinaryCommand.csKeep MCP binary references on current +7/-3

Keep MCP binary references on current

• Stabilizes resolved script-install binary paths used in MCP registrations.

src/Capacitor.Cli.Core/Mcp/KcapBinaryCommand.cs

PluginMcpConfigPatcher.csMove Claude MCP patching into the native CLI +82/-0

Move Claude MCP patching into the native CLI

• Atomically rewrites canonical plugin MCP commands while preserving customized entries and reporting failures.

src/Capacitor.Cli.Core/Mcp/PluginMcpConfigPatcher.cs

CodingAgentsStep.csTailor setup reinstall advice to install source +9/-9

Tailor setup reinstall advice to install source

• Replaces npm-only recovery hints with commands selected from install provenance.

src/Capacitor.Cli/Commands/CodingAgentsStep.cs

DaemonCommands.csShow stable daemon binary in diagnostics +2/-1

Show stable daemon binary in diagnostics

• Passes a 'current'-based script-install path to daemon diagnostics.

src/Capacitor.Cli/Commands/DaemonCommands.cs

DaemonServiceCommands.csRefresh launchd and systemd unit paths +31/-10

Refresh launchd and systemd unit paths

• Extends service refresh to repoint installed systemd units and refresh launchd plists without disrupting busy daemons.

src/Capacitor.Cli/Commands/DaemonServiceCommands.cs

PluginCommand.csUse install-aware plugin recovery hints +14/-14

Use install-aware plugin recovery hints

• Replaces npm-only reinstall advice across plugin-install error paths.

src/Capacitor.Cli/Commands/PluginCommand.cs

RefreshCommand.csAdd shared integration refresh command +89/-0

Add shared integration refresh command

• Runs each opted-in plugin or daemon refresh independently with a timeout and failure summary, and patches the shipped Claude MCP config.

src/Capacitor.Cli/Commands/RefreshCommand.cs

ScriptUpdater.csInstall verified script releases in place +175/-0

Install verified script releases in place

• Downloads and hashes an archive, stages and validates its contents, activates a version directory, and records the selected channel.

src/Capacitor.Cli/Commands/ScriptUpdater.cs

StatusCommand.csUse selected feed for status checks +1/-1

Use selected feed for status checks

• Accepts the shared release-feed interface so status checks can follow install provenance.

src/Capacitor.Cli/Commands/StatusCommand.cs

UpdateCommand.csEnable native script-install updates +54/-6

Enable native script-install updates

• Checks the installer channel feed for script installs, invokes the verified updater, and runs refresh using the newly activated binary.

src/Capacitor.Cli/Commands/UpdateCommand.cs

InstallKind.csDefine install provenance categories +16/-0

Define install provenance categories

• Distinguishes app, script, npm, and unknown installs for update behavior.

src/Capacitor.Cli/InstallKind.cs

InstallProvenance.csDetect script and npm install layouts +35/-4

Detect script and npm install layouts

• Classifies the running binary and chooses a reinstall hint appropriate to its source.

src/Capacitor.Cli/InstallProvenance.cs

Program.csExpose refresh and install-aware update notices +4/-2

Expose refresh and install-aware update notices

• Registers 'refresh' as an offline command and routes passive update checks through the selected release feed.

src/Capacitor.Cli/Program.cs

LaunchdServiceManager.csRepoint installed launchd jobs safely +30/-18

Repoint installed launchd jobs safely

• Expands unit refresh beyond process type to stable binary paths, deferring reloads until an idle restart is accepted.

src/Capacitor.Cli/Services/LaunchdServiceManager.cs

LaunchdUnit.csRewrite launchd program paths +36/-0

Rewrite launchd program paths

• Adds targeted plist binary replacement and inspection of the program currently loaded by launchd.

src/Capacitor.Cli/Services/LaunchdUnit.cs

SystemdServiceManager.csRepoint installed systemd units +25/-1

Repoint installed systemd units

• Rewrites stable daemon paths and runs 'daemon-reload' without restarting the running service.

src/Capacitor.Cli/Services/SystemdServiceManager.cs

SystemdUnit.csRewrite systemd ExecStart binary +24/-0

Rewrite systemd ExecStart binary

• Changes only the executable in a compatible unit's 'ExecStart' line, preserving its remaining content.

src/Capacitor.Cli/Services/SystemdUnit.cs

UpdateNotice.csUse install-aware feed for update notices +2/-2

Use install-aware feed for update notices

• Accepts 'IReleaseFeed' in shared passive checks and exit-time notices.

src/Capacitor.Cli/UpdateNotice.cs

Bug fix (5) +19 / -4
DetachedRespawnStrategy.csRespawn script-installed daemons through current +2/-1

Respawn script-installed daemons through current

• Uses the stable binary path so detached restarts can pick up an activated release.

src/Capacitor.Cli.Daemon/Services/DetachedRespawnStrategy.cs

RestartCoordinator.csWatch the active daemon binary +3/-1

Watch the active daemon binary

• Stats the binary through 'current' so switching the link can trigger the existing idle-restart mechanism.

src/Capacitor.Cli.Daemon/Services/RestartCoordinator.cs

DaemonShimCommands.csTarget stable daemon shim paths +3/-1

Target stable daemon shim paths

• Stabilizes script-install paths when resolving a shim target.

src/Capacitor.Cli/Commands/DaemonShimCommands.cs

SetupCommand.csKeep Claude plugin registration on current +9/-0

Keep Claude plugin registration on current

• Resolves script-installed plugin directories through 'current' instead of a version-pinned directory.

src/Capacitor.Cli/Commands/SetupCommand.cs

UnitIdentity.csWrite stable daemon binary identities +2/-1

Write stable daemon binary identities

• Uses 'current' when resolving a script-installed daemon sibling for new units.

src/Capacitor.Cli/Services/UnitIdentity.cs

Refactor (5) +69 / -160
refresh.jsDelegate npm refresh to the native command +19/-143

Delegate npm refresh to the native command

• Replaces the JavaScript integration list and MCP patcher with one bounded, best-effort 'kcap refresh' invocation.

npm/kcap/bin/refresh.js

NpmRegistryClient.csImplement shared release-feed contract +1/-1

Implement shared release-feed contract

• Makes the existing npm registry client usable through 'IReleaseFeed'.

src/Capacitor.Cli.Core/Http/NpmRegistryClient.cs

SqliteNativeResolver.csReuse canonical runtime RID detection +1/-16

Reuse canonical runtime RID detection

• Replaces local OS and architecture detection with the RID helper shared by script updates.

src/Capacitor.Cli/Commands/SqliteNativeResolver.cs

RuntimeRid.csShare release asset RID selection +25/-0

Share release asset RID selection

• Provides canonical OS, architecture, and musl identifiers for release archives and native SQLite resolution.

src/Capacitor.Cli/RuntimeRid.cs

UnitRefresh.csGeneralize launchd refresh outcomes +23/-0

Generalize launchd refresh outcomes

• Names refresh results for both process-type and binary-path changes, replacing the deleted process-type-specific enum.

src/Capacitor.Cli/Services/UnitRefresh.cs

Tests (12) +872 / -209
refresh.test.jsTest native refresh delegation +19/-181

Test native refresh delegation

• Tests launcher arguments and failure isolation, replacing tests for the removed JavaScript MCP patcher.

npm/kcap/bin/refresh.test.js

ReleaseManifestTests.csTest release manifest validation +42/-0

Test release manifest validation

• Covers the published manifest shape, invalid versions, and malformed digests.

test/Capacitor.Cli.Core.Tests.Unit/Install/ReleaseManifestTests.cs

ScriptInstallLayoutTests.csTest script layout recognition and stable paths +78/-0

Test script layout recognition and stable paths

• Covers marked layouts, versioned and current binaries, and paths that must remain unchanged.

test/Capacitor.Cli.Core.Tests.Unit/Install/ScriptInstallLayoutTests.cs

PluginMcpConfigPatcherTests.csTest native Claude MCP patcher +95/-0

Test native Claude MCP patcher

• Checks shipped entries, idempotence, customization preservation, and malformed-config handling.

test/Capacitor.Cli.Core.Tests.Unit/Mcp/PluginMcpConfigPatcherTests.cs

RefreshCommandTests.csTest shared refresh execution +88/-0

Test shared refresh execution

• Covers the integration list, per-step failure isolation, and provenance-gated MCP patching.

test/Capacitor.Cli.Tests.Unit/Commands/RefreshCommandTests.cs

ScriptReleaseFixture.csProvide stubbed script release fixture +78/-0

Provide stubbed script release fixture

• Builds a versioned test install and serves manifests and archives for updater tests.

test/Capacitor.Cli.Tests.Unit/Commands/ScriptReleaseFixture.cs

ScriptUpdaterTests.csTest verified archive activation +143/-0

Test verified archive activation

• Covers successful activation and refusal of mismatched checksums, malformed manifests, missing RIDs, and unsafe current paths.

test/Capacitor.Cli.Tests.Unit/Commands/ScriptUpdaterTests.cs

UpdateCommandScriptInstallTests.csTest script update command flow +105/-0

Test script update command flow

• Checks channel-feed selection, activation, refresh through the new binary, and failure behavior.

test/Capacitor.Cli.Tests.Unit/Commands/UpdateCommandScriptInstallTests.cs

UpdateCommandServerRefreshTests.csAdapt server-check test to install kinds +1/-1

Adapt server-check test to install kinds

• Explicitly selects npm provenance after the update command constructor change.

test/Capacitor.Cli.Tests.Unit/Commands/UpdateCommandServerRefreshTests.cs

InstallKindDetectionTests.csTest install source detection +52/-0

Test install source detection

• Covers script, flat and nested npm, app, and unknown layouts plus reinstall hints.

test/Capacitor.Cli.Tests.Unit/InstallKindDetectionTests.cs

LaunchdUnitRefreshTests.csGeneralize launchd refresh tests +27/-27

Generalize launchd refresh tests

• Renames the existing process-type refresh suite and adapts it to the unit-refresh API and outcomes.

test/Capacitor.Cli.Tests.Unit/Services/LaunchdUnitRefreshTests.cs

ServiceRepointTests.csTest daemon service path migration +144/-0

Test daemon service path migration

• Covers launchd idle and deferred reloads and systemd unit rewriting without a daemon restart.

test/Capacitor.Cli.Tests.Unit/Services/ServiceRepointTests.cs

Documentation (5) +52 / -12
README.mdDocument script updates and refresh +20/-3

Document script updates and refresh

• Explains verified script-install updates, the 'current' link, daemon-unit behavior, and the new refresh command.

README.md

resolve.jsClarify binary resolver ownership +2/-7

Clarify binary resolver ownership

• Updates comments now that refresh no longer uses this module.

npm/kcap/bin/resolve.js

help-refresh.txtAdd refresh command help +17/-0

Add refresh command help

• Describes refreshed integrations, installer usage, and failure reporting.

src/Capacitor.Cli.Core/Resources/help-refresh.txt

help-update.txtExplain script-install updates in help +12/-2

Explain script-install updates in help

• Documents manifest checks, versioned activation, refresh, and non-script install behavior.

src/Capacitor.Cli.Core/Resources/help-update.txt

help-usage.txtList refresh in command usage +1/-0

List refresh in command usage

• Adds 'refresh' to the maintenance command summary.

src/Capacitor.Cli.Core/Resources/help-usage.txt

Other (1) +6 / -0
CommandServices.csWire release feeds, updater, and refresh +6/-0

Wire release feeds, updater, and refresh

• Registers new command services and selects the installer manifest feed for script installs.

src/Capacitor.Cli/Commands/CommandServices.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d98d5189ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// A leftover directory of this version is not current (only an older one is), so nothing runs from it.
var dest = layout.VersionDir(version);
try {
if (Directory.Exists(dest)) Directory.Delete(dest, recursive: true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid deleting the directory targeted by current

If an older versioned binary runs kcap update after current already points at the requested version—or two updates overlap—dest can be the active installation despite the comment above. This recursive deletion then removes files used by the current CLI, daemon, and integrations before activation completes; on Windows, locked executables can also leave the active directory partially deleted. Resolve and reject the active target or serialize activation before removing an existing version directory.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c847708. Requesting the active version now installs nothing and only rewrites the marker. Any other existing versions/<v> is renamed to versions/.replaced-<v>-<guid> and kept, never deleted in place, so a process still running from it keeps its files. A rename that fails (Windows, locked binary) installs nothing.

if (binary is null || SystemdUnit.WithBinary(text, stabilize(binary)) is not { } rewritten) return false;

_writeUnit(path, rewritten, null);
_runProcess("systemctl", SystemdUnit.DaemonReloadArgs());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check daemon-reload before reporting the repoint

When systemctl --user daemon-reload exits nonzero, _runProcess returns that failure but it is discarded, so refresh reports success even though systemd still has the old ExecStart cached. A later refresh sees the already-rewritten file and skips daemon-reload, allowing the next daemon start to keep using the old version indefinitely. The inspected systemctl --help describes daemon-reload as “Reload systemd manager configuration,” so its exit status must be checked and surfaced for retry.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c847708. Repoint throws when daemon-reload exits non-zero, and daemon service refresh reports that unit as failed. A unit already on the stable path is reloaded when systemctl show --property=NeedDaemonReload says yes, so the next refresh retries a reload that failed after the rewrite.

@qodo-code-review

qodo-code-review Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Windows script updates are rejected ✓ Resolved
Description
ScriptUpdater.InstallAsync checks FileInfo.LinkTarget to decide whether current is an
installer-created link, which rejects the Windows junction layout. kcap-web’s PowerShell installer
creates current as a junction, so an ordinary Windows script installation cannot update through
kcap update.
Code

src/Capacitor.Cli/Commands/ScriptUpdater.cs[R50-52]

+        var current = new FileInfo(layout.Current);
+        if (current.LinkTarget is null && (current.Exists || Directory.Exists(layout.Current))) {
+            await stderr.WriteLineAsync($"{layout.Current} is not a link the installer made. Nothing was installed.");
Relevance

●●● Strong

Windows junction handling is a concrete cross-platform installation bug, matching accepted path and
symlink robustness fixes.

PR-#191
PR-#657

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PowerShell installer recognizes reparse points and creates current as a junction, while the
new updater requires a non-null file link target before it will proceed.

kcap-cli -> kcap-web
src/Capacitor.Cli/Commands/ScriptUpdater.cs[49-53]
External repo: kurrent-io/kcap-web, src/server/install/install.ps1 [53-58]
External repo: kurrent-io/kcap-web, src/server/install/install.ps1 [119-123]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The updater rejects the `current` junction created by the official Windows installer.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/ScriptUpdater.cs[50-53]
- /cross_repos/kcap-web/src/server/install/install.ps1[53-58]
- /cross_repos/kcap-web/src/server/install/install.ps1[119-123]

## Recommended Fix
On Windows, validate `current` as the installer’s directory junction using its reparse-point attributes instead of requiring `FileInfo.LinkTarget`. Add a Windows test using the junction layout.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Windows updates can lose the active install ✓ Resolved
Description
On Windows, DefaultSwitch deletes the existing current junction before creating its replacement
with mklink /J, without preserving a rollback path. If mklink fails or cannot start,
InstallAsync reports activation failure and leaves current\bin unavailable to the PATH entry,
MCP registrations, and service units until the installation is repaired.
Code

src/Capacitor.Cli/Commands/ScriptUpdater.cs[R143-148]

+    static void DefaultSwitch(ScriptInstallLayout layout, string versionDir) {
+        if (OperatingSystem.IsWindows()) {
+            if (Directory.Exists(layout.Current) || File.Exists(layout.Current)) Directory.Delete(layout.Current);
+            using var mklink = Process.Start(new ProcessStartInfo("cmd.exe") {
+                ArgumentList = { "/d", "/c", "mklink", "/J", layout.Current, versionDir },
+                UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true,
Relevance

●●● Strong

Deleting the active Windows junction before replacement can strand installations; accepted
path-installation precedents favor preserving recovery.

PR-#191
PR-#657

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Directory.Delete(layout.Current) runs before the replacement mklink command. A non-zero mklink
exit throws an IOException, while the activation catch at lines 98–100 returns false without
recreating the previous junction; the same lack of restoration applies if the command cannot start.

Implement verified native updates for script installations
src/Capacitor.Cli/Commands/ScriptUpdater.cs[143-148]
src/Capacitor.Cli/Commands/ScriptUpdater.cs[143-158]
src/Capacitor.Cli/Commands/ScriptUpdater.cs[94-101]
src/Capacitor.Cli/Commands/ScriptUpdater.cs[93-100]
src/Capacitor.Cli/Commands/ScriptUpdater.cs[143-153]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Windows activation path removes `current` before creating its replacement junction. If junction creation fails, the staged version is not activated and the previous active path is not restored.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/ScriptUpdater.cs[143-158]
- src/Capacitor.Cli/Commands/ScriptUpdater.cs[93-100]

## Recommended Fix
Create the replacement junction under a temporary name and verify that it points to the staged version before replacing `current`. Preserve the old junction target (for example, through `new DirectoryInfo(layout.Current).LinkTarget`) and, if the switch requires deleting `current`, recreate the old junction whenever activation fails before reporting the failure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Script upgrades leave daemons outdated ✗ Dismissed
Description
RefreshCommand.Steps adds daemon service refresh, but the upgrade loops in kcap-web still run
only plugin install --if-installed. When either web installer upgrades a script installation whose
daemon unit points into a version directory, that unit is not repointed to current and continues
using the old binary.
Code

src/Capacitor.Cli/Commands/RefreshCommand.cs[R29-30]

+        ["plugin", "install",                  "--if-installed"],
+        ["daemon", "service", "refresh"],
Relevance

●●● Strong

The PR explicitly intends every installer upgrade to refresh daemon units, so missing web-installer
invocation conflicts with stated scope.

PR-#1269

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI’s new refresh list includes the service-repointing step, but neither web installer invokes
that list during an upgrade.

kcap-cli -> kcap-web
src/Capacitor.Cli/Commands/RefreshCommand.cs[19-30]
src/Capacitor.Cli/Commands/DaemonServiceCommands.cs[99-113]
External repo: kurrent-io/kcap-web, src/server/install/install.sh [115-123]
External repo: kurrent-io/kcap-web, src/server/install/install.sh [219-224]
External repo: kurrent-io/kcap-web, src/server/install/install.ps1 [128-135]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The web installers omit the daemon-service step now provided by `kcap refresh` when upgrading script installations.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/RefreshCommand.cs[19-30]
- /cross_repos/kcap-web/src/server/install/install.sh[115-123]
- /cross_repos/kcap-web/src/server/install/install.sh[219-224]
- /cross_repos/kcap-web/src/server/install/install.ps1[128-135]

## Recommended Fix
Update both kcap-web installers to invoke the newly installed binary’s `refresh` command on upgrade, replacing their per-plugin loops. Coordinate deployment so installer upgrades receive the daemon-service repointing step.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (1)
4. Daemon restarts can keep the old binary ✓ Resolved
Description
SystemdServiceManager.Repoint ignores the exit code from systemctl --user daemon-reload and
reports success after rewriting the unit file. If reload fails, systemd can retain the old unit
definition while the refresh command claims the new path will apply on the next start; subsequent
refreshes see the already-rewritten file and may not retry the reload.
Code

src/Capacitor.Cli/Services/SystemdServiceManager.cs[R64-68]

+        if (binary is null || SystemdUnit.WithBinary(text, stabilize(binary)) is not { } rewritten) return false;
+
+        _writeUnit(path, rewritten, null);
+        _runProcess("systemctl", SystemdUnit.DaemonReloadArgs());
+
Relevance

●●● Strong

Recent daemon refresh reviews accept fail-closed handling when reload evidence or process outcomes
are ignored.

PR-#1269
PR-#1176

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed systemd path rewrites the unit and invokes daemon-reload, but Repoint ignores its exit
code even though the process runner returns nonzero exits as values. Because
DaemonServiceCommands.Refresh counts only exceptions as failures, it treats a failed reload as
successful while systemd can still hold the old binary path, contrary to compliance rule 3209115's
requirement that a refreshed daemon use the updated version when running or subsequently started.

Implement verified native updates for script installations
src/Capacitor.Cli/Services/SystemdServiceManager.cs[64-68]
src/Capacitor.Cli/Services/SystemdServiceManager.cs[58-69]
src/Capacitor.Cli/Services/ServiceProcess.cs[11-39]
src/Capacitor.Cli/Commands/DaemonServiceCommands.cs[107-120]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Repoint` rewrites the systemd unit but ignores a failed `systemctl --user daemon-reload`. The refresh command consequently reports success while systemd may retain the old binary path, and a later refresh may not retry the reload because the unit file already contains the desired path.

## Fix Focus Areas
- src/Capacitor.Cli/Services/SystemdServiceManager.cs[58-69]
- src/Capacitor.Cli/Commands/DaemonServiceCommands.cs[107-120]

## Recommended Fix
Capture and check the daemon-reload process exit code, throwing or returning failure when it is nonzero. Propagate that failure so `DaemonServiceCommands.Refresh` reports the service as failed rather than repointed, and ensure a later refresh can retry daemon-reload even when the on-disk unit already has the desired path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. A test edits its temp root directly ✓ Resolved
Description
A_current_that_is_not_a_link_is_refused calls Directory.Delete and Directory.CreateDirectory
directly on paths below its injected temporary directory. When the test resets current, it
bypasses the repository's temporary-directory helpers, so future cleanup and platform-specific path
handling are harder to maintain consistently.
Code

test/Capacitor.Cli.Tests.Unit/Commands/ScriptUpdaterTests.cs[R118-119]

+        Directory.Delete(f.Layout.Current);
+        Directory.CreateDirectory(f.Layout.Current);
Relevance

●●● Strong

Recent test reviews consistently require TempDir helper APIs for filesystem state beneath temporary
roots.

PR-#1318
PR-#980

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The test operates on f.Layout.Current, which is located beneath the ScriptReleaseFixture's
TempDir, but directly invokes Directory.Delete and Directory.CreateDirectory. The checklist
requires temporary filesystem state under a TempDir to use its helper members instead of direct
filesystem calls.

Rule 2767472: Use TempDir helper for all temporary filesystem state in tests
test/Capacitor.Cli.Tests.Unit/Commands/ScriptUpdaterTests.cs[118-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test directly deletes and creates a directory beneath the injected temporary root instead of using the repository's temporary filesystem helpers.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/Commands/ScriptUpdaterTests.cs[118-119]

## Recommended Fix
Use the appropriate `TempDir` helper APIs for resetting the `current` test state, or add a helper operation if symlink replacement requires behavior not currently exposed. Keep all temporary-root filesystem mutations routed through the helper so cleanup and platform-specific path handling remain centralized.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. npm users never see which refresh steps failed ✓ Resolved
Description
runRefreshes now spawns kcap refresh with stdio: "ignore", so the native command's per-step
failure list and the .mcp.json patch warning go nowhere. The old JS patcher printed its warning
with console.warn in the npm process itself. During npm install -g and the launcher's `kcap
update, a failed refresh step or patch is silent, even though the README and help-refresh.txt`
tell users to run kcap refresh when an update reports a failed step.
Code

npm/kcap/bin/refresh.js[R25-31]

+    spawnSync(process.execPath, [launcherPath, "refresh"], {
+      stdio: "ignore",
+      env: process.env,
+      timeout: REFRESH_TIMEOUT_MS,
+      killSignal: "SIGKILL",
+      windowsHide: true,
+    });
Relevance

●●● Strong

Accepted precedent requires surfacing failures instead of silently discarding diagnostics from
installation workflows.

PR-#280

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
RefreshCommand reports failures only on stderr ('Some agent integrations were not refreshed:' plus
each step, and the MCP patch failure line). refresh.js discards both streams and ignores the exit
status. The removed JS patchPluginMcpConfig emitted a visible console.warn. kcap.js prints 'kcap
updated.' unconditionally after runRefreshes.

src/Capacitor.Cli/Commands/RefreshCommand.cs[49-54]
src/Capacitor.Cli/Commands/RefreshCommand.cs[79-82]
npm/kcap/bin/kcap.js[374-378]
src/Capacitor.Cli.Core/Resources/help-refresh.txt[14-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
refresh.js runs `kcap refresh` with stdio ignored, so the failures and warnings it reports never reach npm users.

## Fix Focus Areas
- npm/kcap/bin/refresh.js[23-35]

## Recommended Fix
Use `stdio: ["ignore", "ignore", "inherit"]` (or pipe stderr and re-emit it with console.warn) so failure lines reach the user. When `result.status` is non-zero, print one hint: `kcap: some integrations were not refreshed; run \`kcap refresh\` to retry.` Keep the call non-throwing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Npm migrations leave daemons outdated ✓ Resolved
Description
The new installation guidance describes script archives, the current link and daemon follow-up but
never tells users switching from npm to re-register the daemon with kcap daemon install. A daemon
service created by the npm installation retains its old executable path after migration, so the
documented update flow does not cover the required service transition.
Code

README.md[R2443-2451]

+> **Script installs** (`curl -fsSL https://www.kurrent.io/install | bash`, or
+> `install.ps1` on Windows) update without npm: `kcap update` reads the
+> installer's channel manifest, downloads this platform's archive from GitHub
+> Releases and checks its sha256 against the manifest, installing nothing on a
+> mismatch. Each release gets its own `versions/<version>` directory and the
+> `current` link is switched to it, so running processes keep the files they
+> started from; then the new binary runs `kcap refresh`. A daemon service
+> installed from a script install is moved onto the `current` path, so it
+> follows updates.
Relevance

●●● Strong

Explicit migration documentation requirement and daemon path transition make this a clear accepted
documentation gap.

PR-#1277

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 3209117 requires documentation for script installation and updates to retain npm
support and explicitly instruct users migrating from npm to re-register the daemon with `kcap daemon
install`. The added README section explains script updates and daemon behavior but contains no
migration or daemon re-registration instruction; the corresponding update help text has the same
omission.

Document installation, updates, and npm migration
README.md[2443-2451]
src/Capacitor.Cli/Resources/help-update.txt[11-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The script-install documentation does not explain how users migrating from npm should re-register an existing daemon service, even though daemon units persist the installation path.

## Fix Focus Areas
- README.md[2443-2451]
- src/Capacitor.Cli/Resources/help-update.txt[11-18]

## Recommended Fix
Add explicit migration guidance stating that users switching from npm to the script installer must run `kcap daemon install` to register the daemon against the new script installation before relying on updates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (4)
8. Beta script installs switch to stable ✓ Resolved
Description
UpdateCommand selects the update channel from the active profile and flags, without reading the
channel that ScriptUpdater.WriteMarker records in install.json. For a beta script install whose
profile has no beta setting, an unqualified kcap update checks latest.json instead of the
installed channel.
Code

src/Capacitor.Cli/Commands/UpdateCommand.cs[102]

+        var checkResult       = await CheckForUpdateAsync(forceCheck: true, channel, root, Feed, time);
Relevance

●●● Strong

A recorded beta installation channel is ignored during update checks, directly violating channel
continuity and the new marker design.

PR-#892
PR-#267

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The update selects a channel using profile configuration, passes it to the release feed, and never
reads the marker written with the script install's channel.

src/Capacitor.Cli/Commands/UpdateCommand.cs[54-65]
src/Capacitor.Cli/Commands/UpdateCommand.cs[99-103]
src/Capacitor.Cli.Core/Http/KcapReleaseClient.cs[22-32]
src/Capacitor.Cli/Commands/ScriptUpdater.cs[130-135]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Script updates ignore the channel recorded by the installer and can follow a different release stream.
## Fix Focus Areas
- src/Capacitor.Cli/Commands/UpdateCommand.cs[64-65]
- src/Capacitor.Cli/Commands/UpdateCommand.cs[99-103]
- src/Capacitor.Cli/Commands/ScriptUpdater.cs[130-135]
## Recommended Fix
Read and validate the script install's recorded channel when selecting its default update feed channel, while preserving explicit channel-switch flags.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Stalled archive download hangs kcap update ✓ Resolved
Description
KcapReleaseClient.DownloadAsync calls GetAsync with HttpCompletionOption.ResponseHeadersRead
and then streams the body with CopyToAsync(file, ct). HttpClient.Timeout (the 10-minute cap) only
covers the request up to the response headers in that mode, and UpdateScriptInstallAsync passes
CancellationToken.None. A connection that stalls after the headers (common with redirected GitHub
asset storage on flaky networks) blocks kcap update forever, contrary to the registration comment
that the whole request is capped.
Code

src/Capacitor.Cli.Core/Http/KcapReleaseClient.cs[R56-62]

+        using var resp = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
+        if (!resp.IsSuccessStatusCode)
+            throw new HttpRequestException($"{url} answered {(int)resp.StatusCode}.");
+
+        await using var body = await resp.Content.ReadAsStreamAsync(ct);
+        await using var file = new FileStream(destination, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+        await body.CopyToAsync(file, ct);
Relevance

●●● Strong

The whole-request timeout does not cover streamed bodies; HTTP reliability findings receive
acceptance when bounded behavior is missing.

PR-#1232

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The typed client sets Timeout = 10 minutes and the comment says 'the whole-request cap is
generous', but DownloadAsync uses ResponseHeadersRead, so the body copy is not bounded by that
timeout. The only token is the one passed in, and UpdateCommand passes CancellationToken.None.

src/Capacitor.Cli.Core/Http/CapacitorHttpServices.cs[173-178]
src/Capacitor.Cli.Core/Http/KcapReleaseClient.cs[55-63]
src/Capacitor.Cli/Commands/UpdateCommand.cs[150-151]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The archive download's body stream isn't covered by HttpClient.Timeout when ResponseHeadersRead is used, and callers pass CancellationToken.None, so a stalled transfer hangs forever.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Http/KcapReleaseClient.cs[55-63]

## Recommended Fix
Inside DownloadAsync, create a `CancellationTokenSource(TimeSpan.FromMinutes(10), time)` linked with `ct` and use it for both GetAsync and CopyToAsync. Better still, add an idle-read timeout that cancels when no bytes arrive for e.g. 60s. Surface the cancellation as a download failure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Every script update leaves old versions on disk ✗ Dismissed
Description
ScriptUpdater.InstallAsync extracts each release into a new versions/<v> directory and switches
current to it, but nothing ever deletes older version directories. The same goes for .update-*
work directories left behind when an update is killed mid-download. Each archive carries the CLI,
the daemon, native libs and the plugin directory, so on a script install every kcap update adds a
full release to ~/.local/share/kcap/versions and that space is never reclaimed.
Code

src/Capacitor.Cli/Commands/ScriptUpdater.cs[R93-97]

+            var dest = layout.VersionDir(version);
+            try {
+                if (Directory.Exists(dest)) Directory.Delete(dest, recursive: true);
+                Directory.Move(staging, dest);
+                SwitchCurrent(layout, dest);
Relevance

●● Moderate

Unbounded release retention is a credible reliability issue, but cleanup policy and process safety
require architectural judgment.

PR-#670

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The only directory ScriptUpdater deletes is the work dir in finally, plus a leftover copy of the
*target* version (lines 95, 108). No code anywhere enumerates layout.Versions to prune old
versions: a grep for prune/versions cleanup in ScriptUpdater finds nothing. The release archive
bundles both AOT binaries, the pty shim and the plugin dir (release.yml 488-517).

src/Capacitor.Cli/Commands/ScriptUpdater.cs[56-58]
src/Capacitor.Cli/Commands/ScriptUpdater.cs[92-108]
.github/workflows/release.yml[488-517]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Script-install updates add a new `versions/<v>` directory each time and never remove old ones (or stale `.update-*` work dirs), so disk usage grows without bound.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/ScriptUpdater.cs[92-108]

## Recommended Fix
Once `SwitchCurrent` and `WriteMarker` succeed, enumerate `layout.Versions`. Keep the new version and the previously active one (a running daemon may still use it), and best-effort delete every other version directory and any `.update-*` directory older than a threshold. Swallow IO errors, because a version in use on Windows cannot be deleted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Returning to a release deletes its files ✓ Resolved
Description
ScriptUpdater.InstallAsync recursively deletes an existing target version directory before moving
the downloaded release into place. If an install returns to a previously installed version after
switching away, processes still running from that version's directory lose its ancillary files, and
Windows may refuse the deletion because its executable is locked.
Code

src/Capacitor.Cli/Commands/ScriptUpdater.cs[R92-96]

+            // A leftover directory of this version is not current (only an older one is), so nothing runs from it.
+            var dest = layout.VersionDir(version);
+            try {
+                if (Directory.Exists(dest)) Directory.Delete(dest, recursive: true);
+                Directory.Move(staging, dest);
Relevance

●● Moderate

Deleting a previously installed version can disrupt still-running processes, but cleanup semantics
are architectural and precedent is limited.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The updater always deletes an existing destination, despite the layout explicitly accommodating
running processes and persisted paths into old version directories.

src/Capacitor.Cli/Commands/ScriptUpdater.cs[13-16]
src/Capacitor.Cli/Commands/ScriptUpdater.cs[92-97]
src/Capacitor.Cli.Core/Install/ScriptInstallLayout.cs[5-9]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reinstalling a version that remains on disk deletes files that older running processes may still use.
## Fix Focus Areas
- src/Capacitor.Cli/Commands/ScriptUpdater.cs[92-97]
## Recommended Fix
Do not recursively replace an existing version directory. Verify and reuse it when appropriate, or install into a distinct immutable directory and switch `current` without deleting the old one.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

12. Marker write failure crashes an applied update ✓ Resolved
Description
WriteMarker(layout, channel) runs after SwitchCurrent has already activated the new version,
outside every try/catch in InstallAsync. An IOException or UnauthorizedAccessException
(read-only or full disk, a permissions change on install.json) then escapes through
UpdateScriptInstallAsync as an unhandled exception: kcap update crashes with a stack trace and
never runs kcap refresh, even though current already points at the new release.
Code

src/Capacitor.Cli/Commands/ScriptUpdater.cs[R103-104]

+            WriteMarker(layout, channel);
+            await stdout.WriteLineAsync($"Installed kcap {version} to {layout.Root}");
Relevance

●●● Strong

Marker persistence occurs after activation and can crash the update; accepted precedents favor
atomic marker and rollback handling.

PR-#165

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Lines 94-101 catch only around Delete/Move/SwitchCurrent. WriteMarker (lines 131-136) does
File.WriteAllText plus File.Move with no handling, and UpdateCommand calls InstallAsync without a
try.

src/Capacitor.Cli/Commands/ScriptUpdater.cs[93-106]
src/Capacitor.Cli/Commands/ScriptUpdater.cs[131-136]
src/Capacitor.Cli/Commands/UpdateCommand.cs[150-156]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The install.json rewrite runs after activation and is unguarded, so an IO failure crashes `kcap update` and skips the refresh.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/ScriptUpdater.cs[103-104]
- src/Capacitor.Cli/Commands/ScriptUpdater.cs[131-136]

## Recommended Fix
Wrap `WriteMarker` in a try/catch for IOException and UnauthorizedAccessException. On failure, write a warning to stderr, delete the temp file, and keep returning true, since the version is already active.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
  Explored: repo: kurrent-io/cloud-meta (sha: 107e7601) — View relationship
  Explored: repo: kurrent-io/kcap-web (sha: ea602810) — View relationship
Review mode: Auto: 🧠 Deep: Many independent installer, updater, daemon, and refresh paths changed.

Grey Divider

Tip of the day
💡 Did you know, you can route each severity your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread README.md Resolved
Comment thread npm/kcap/bin/refresh.js Resolved
The version being installed can already be on disk and running, so it is moved aside, and the active one is left alone. A script install's channel comes from install.json: the profile defaults to latest, so it cannot say whether stable was chosen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant