Skip to content

[AI-866] feat: make kcap update perform the upgrade and refresh hooks/skills#159

Merged
alexeyzimarev merged 2 commits into
mainfrom
alexeyzimarev/ai-866-kcap-update-self-update
Jun 15, 2026
Merged

[AI-866] feat: make kcap update perform the upgrade and refresh hooks/skills#159
alexeyzimarev merged 2 commits into
mainfrom
alexeyzimarev/ai-866-kcap-update-self-update

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

What

kcap update now performs a one-step upgrade and refreshes agent hooks/skills, instead of only printing npm install -g ….

Closes AI-866.

Why

The skills/hooks refresh lived only in the npm postinstall hook, which modern package managers gate behind an "allowed scripts" allowlist. When that gate blocks the postinstall, an upgraded CLI ships stale skills/hooks. A user-initiated kcap update runs the refresh regardless of that gate — the same pattern Claude Code / Codex use.

How

The upgrade is driven from the Node launcher (kcap.js), never the native binary. The OS locks an executable image for the whole process lifetime, but a script only during load — so with the native binary not running, npm can overwrite it even on Windows, with no temp-file/rename/detached-helper dance.

kcap update:

  1. Detects an npm-global install (launcher under npm root -g); otherwise prints install-method guidance (e.g. Homebrew) and exits.
  2. Probes the native update --check (short-lived → binary unlocked before npm runs) to skip when already latest.
  3. Pre-checks write access to the global root (clear message instead of a half-failed sudo install).
  4. Runs npm install -g @kurrent/kcap@latest, then refreshes opted-in agent plugins via the new launcher. npm is routed through a shell on Windows (.cmd-spawn fix).

update --check / update --help fall through to the native binary.

Changes

  • npm/kcap/bin/refresh.js (new): shared per-agent plugin install --if-installed loop.
  • postinstall.js: slimmed to call refresh.runRefreshes().
  • kcap.js: update intercept (above).
  • UpdateCommand.cs: --check JSON probe ({current, latest, newer}); messages retargeted to kcap update.
  • VersionNudgeEmitter.cs / Program.cs: in-agent nudge retargeted; pass args.
  • .gitignore: track npm/kcap/bin/ (the blanket bin/ rule would silently drop refresh.js; platform binary dirs stay ignored).
  • Docs (README, help-update.txt, npm README) + 3 test assertions updated.

Verification

  • ✅ All JS files pass node --check.
  • ⚠️ C# not built locally (no .NET SDK on the dev machine) — relying on CI for build / AOT IL-warning check / unit + integration tests.

Notes

  • No automated test added for --check: CheckForUpdateAsync is network/cache-coupled (needs injection to test cleanly); the newer gating reuses already-tested SemverCompare.

🤖 Generated with Claude Code

…ks/skills

Previously `kcap update` only checked the registry and printed a hint to run
`npm install -g`. The skills/hooks refresh lived only in the npm postinstall
hook, which modern package managers gate behind an "allowed scripts" allowlist
— so an upgrade could ship stale skills/hooks.

`kcap update` now performs a one-step, user-initiated upgrade and refresh.
Because the user invokes it explicitly, the refresh runs regardless of the
postinstall gate.

The upgrade is driven from the Node launcher (kcap.js), never the native
binary: the OS locks an executable image for the whole process lifetime but a
script only during load, so with the native binary not running, npm can
overwrite it even on Windows — no temp-file/rename/detached-helper dance.

- npm/kcap/bin/refresh.js: extract the per-agent `plugin install --if-installed`
  loop, shared by postinstall and update.
- npm/kcap/bin/postinstall.js: slim to call refresh.runRefreshes().
- npm/kcap/bin/kcap.js: intercept `update` — detect npm-global install, probe
  the native `update --check`, pre-check write access to the global root, run
  `npm install -g @kurrent/kcap@latest`, then refresh. Route npm through a shell
  on Windows. `--check`/`--help` fall through to the binary.
- UpdateCommand.cs: add `--check` JSON probe; retarget messages to `kcap update`.
- VersionNudgeEmitter.cs / Program.cs: retarget nudge, pass args.
- .gitignore: track npm/kcap/bin/ (blanket `bin/` rule would drop refresh.js).
- Docs + tests updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 15, 2026

Copy link
Copy Markdown

AI-866

@qodo-code-review

qodo-code-review Bot commented Jun 15, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used
✅ Tickets: AI-866

Grey Divider


Action required

1. Unknown version skips update ✓ Resolved 🐞 Bug ≡ Correctness
Description
UpdateCommand.HandleAsync(... --check) can emit current: null and newer: false with a success
exit code, which makes the Node launcher treat the CLI as already up to date and exit without
running the npm upgrade. This can strand users on an outdated CLI when the native binary cannot
determine its current version (e.g., reports "unknown").
Code

src/Capacitor.Cli/Commands/UpdateCommand.cs[R16-30]

+        if (checkOnly) {
+            // Machine-readable probe consumed by the npm launcher (kcap.js).
+            // One JSON line on stdout; exit 1 only when the check itself failed.
+            var newer = latest is not null && current is not null && IsNewer(latest, current);
+
+            var obj = new JsonObject {
+                ["current"] = current,
+                ["latest"]  = latest,
+                ["newer"]   = newer,
+            };
+
+            await Console.Out.WriteLineAsync(obj.ToJsonString());
+
+            return latest is null ? 1 : 0;
+        }
Evidence
The C# probe can produce current = null (when the build/version is "unknown"), and in that case
newer is computed as false. The Node launcher uses only newer === false to decide it is
already up to date and exits immediately, skipping the upgrade.

src/Capacitor.Cli/Commands/UpdateCommand.cs[11-30]
src/Capacitor.Cli/Commands/UpdateCommand.cs[140-145]
src/Capacitor.Cli.Core/CapacitorVersion.cs[11-30]
npm/kcap/bin/kcap.js[126-145]

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 `kcap update --check` probe can return `current: null` while setting `newer: false` and returning exit code `0`. The Node launcher (`npm/kcap/bin/kcap.js`) treats `newer === false` as a definitive “already up to date” signal and exits early, so no upgrade is performed.

### Issue Context
- `GetCurrentVersion()` maps `CapacitorVersion.CurrentDisplay()` == "unknown" to `null`.
- The probe currently computes `newer` as `latest != null && current != null && IsNewer(...)`, which yields `false` when `current` is null.
- The launcher exits early when it sees `newer === false`.

### Fix Focus Areas
- src/Capacitor.Cli/Commands/UpdateCommand.cs[11-30]
- src/Capacitor.Cli/Commands/UpdateCommand.cs[142-145]
- npm/kcap/bin/kcap.js[126-143]

### Suggested fix
Option A (preferred): In `--check` mode, if `current` is null/empty, return exit code `1` (and/or omit `newer` / set it to null) so the launcher will fall back to running `npm install -g ...`.

Option B (additional hardening): In `kcap.js`, only early-exit on `newer === false` when both `current` and `latest` are non-empty strings; otherwise continue to the npm upgrade path.

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


Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Make kcap update self-upgrade and refresh agent hooks/skills (npm-global)
✨ Enhancement 📝 Documentation 🧪 Tests 🕐 20-40 Minutes

Grey Divider

Walkthroughs

Description
• Make kcap update run the npm global upgrade and then refresh opted-in plugins.
• Add a --check JSON probe for launcher-driven update gating.
• Retarget docs, help text, and nudge messaging from npm install -g to kcap update.
Diagram
graph TD
  U["User runs kcap update"] --> L["Node launcher (kcap.js)"] --> D{"Global npm install?"}
  D -->|"no"| G["Print install guidance"]
  D -->|"yes"| C["Native binary: update --check"] --> N["npm install -g @kurrent/kcap@latest"] --> R["refresh.js: runRefreshes"] --> P["plugin install --if-installed (agents)"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Implement self-update inside the native binary
  • ➕ Single implementation language/runtime for update logic
  • ➕ Could reuse existing update-check code paths directly
  • ➖ Harder to update in-place due to OS executable locking (especially Windows)
  • ➖ Likely requires temp-file/rename/detached helper complexity and higher failure modes
2. Keep `kcap update` as “check-only” and rely on postinstall refresh
  • ➕ Minimal code and less platform-specific process handling
  • ➖ Still fails when package managers gate install scripts; users can end up with stale hooks/skills
  • ➖ Does not provide a reliable, user-invoked recovery path
3. Add a dedicated `kcap refresh` command and keep upgrade separate
  • ➕ Clear separation of concerns; refresh can be run after any install method
  • ➖ Two-step UX; users may still forget to refresh after upgrading
  • ➖ Doesn’t solve the core ‘one-step upgrade’ goal for npm-global installs

Recommendation: Keep the PR’s approach: driving the upgrade from the Node launcher is the most robust way to avoid native binary file-lock issues while still providing a one-step UX. The added update --check probe is a pragmatic bridge between the launcher and native update logic, and factoring refresh into a shared module reduces divergence between postinstall and manual update flows.

Grey Divider

File Changes

Enhancement (4)
kcap.js Intercept 'kcap update' in the Node launcher to self-upgrade + refresh +96/-1

Intercept 'kcap update' in the Node launcher to self-upgrade + refresh

• Adds a launcher-driven update flow that (1) verifies global npm install, (2) probes the native binary via 'update --check', (3) checks write access to the global root, (4) runs 'npm install -g @kurrent/kcap@latest', then (5) refreshes agent plugins via the shared refresher. Includes a Windows fix by spawning npm through a shell due to '.cmd' shims.

npm/kcap/bin/kcap.js


refresh.js Add shared agent plugin refresh runner for postinstall and update +44/-0

Add shared agent plugin refresh runner for postinstall and update

• Introduces 'runRefreshes()' which runs 'plugin install --if-installed' for skills/Codex/Cursor/Copilot/Claude with per-invocation timeouts and failure isolation. Designed to never throw so it can safely run in postinstall and update flows.

npm/kcap/bin/refresh.js


VersionNudgeEmitter.cs Retarget in-agent upgrade nudge to 'kcap update' +1/-1

Retarget in-agent upgrade nudge to 'kcap update'

• Changes the upgrade suggestion injected into agent sessions to recommend 'kcap update' instead of 'npm install -g @kurrent/kcap'.

src/Capacitor.Cli.Core/VersionNudgeEmitter.cs


UpdateCommand.cs Add 'update --check' JSON probe and retarget update messaging +25/-4

Add 'update --check' JSON probe and retarget update messaging

• Extends the update command to accept args and support '--check', emitting a single JSON line '{current, latest, newer}' for consumption by the Node launcher. Updates console/stderr messaging to recommend 'kcap update' (and 'npm install -g ...@latest' as a fallback).

src/Capacitor.Cli/Commands/UpdateCommand.cs


Refactor (2)
postinstall.js Refactor postinstall refresh logic to call shared refresher +14/-42

Refactor postinstall refresh logic to call shared refresher

• Replaces the inline per-agent refresh loop with a call into 'refresh.js' when the install is global. Clarifies that 'kcap update' exists to bypass install-script gating.

npm/kcap/bin/postinstall.js


Program.cs Pass argv through to UpdateCommand for '--check' handling +1/-1

Pass argv through to UpdateCommand for '--check' handling

• Adjusts the update command dispatch to pass the original args so 'UpdateCommand' can interpret '--check'.

src/Capacitor.Cli/Program.cs


Tests (2)
ClaudeHookStdoutTests.cs Update integration tests to expect 'kcap update' in nudge envelope +2/-2

Update integration tests to expect 'kcap update' in nudge envelope

• Updates assertions to match the new upgrade wording emitted into Claude hook additionalContext.

test/Capacitor.Cli.Tests.Integration/ClaudeHookStdoutTests.cs


VersionNudgeEmitterTests.cs Update unit tests for new upgrade command text +2/-2

Update unit tests for new upgrade command text

• Renames the test and updates expectations to ensure the version nudge includes 'kcap update' instead of the npm install command.

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


Documentation (3)
README.md Document 'kcap update' as the recommended upgrade + refresh path +14/-6

Document 'kcap update' as the recommended upgrade + refresh path

• Updates installation/upgrade guidance to prefer 'kcap update' when install scripts are gated, while keeping manual plugin-refresh alternatives. Retargets in-agent upgrade prompt text and command list to reflect the new behavior.

README.md


README.md Update npm package README to describe 'kcap update' upgrade behavior +1/-1

Update npm package README to describe 'kcap update' upgrade behavior

• Changes the command summary so 'kcap update' is described as upgrading the CLI and refreshing agent plugins, not just checking for updates.

npm/kcap/README.md


help-update.txt Rewrite 'kcap update' help text and add '--check' option +13/-2

Rewrite 'kcap update' help text and add '--check' option

• Updates the command description to reflect launcher-driven upgrades and plugin refresh behavior for npm-global installs. Documents '--check' as a machine-readable JSON probe.

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


Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli/Commands/UpdateCommand.cs
…inate

`update --check` emitted `newer:false` whenever it couldn't compare versions —
including when the native binary reports its version as "unknown" (current
=> null). The launcher treated that as "already up to date" and exited without
upgrading, stranding the user on a stale CLI (and printing "Already up to
date: null").

Make `newer` tri-state: true (upgrade), false (confidently up to date), null
(can't tell — current unknown or registry check failed). The launcher now only
takes the up-to-date fast-path on `newer:false` with a known current version;
null falls through to the idempotent `npm install -g @latest`.

Reported by qodo on #159.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alexeyzimarev alexeyzimarev merged commit 2f5dd79 into main Jun 15, 2026
5 checks passed
@alexeyzimarev alexeyzimarev deleted the alexeyzimarev/ai-866-kcap-update-self-update branch June 15, 2026 08:04
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