@kurrent/kcap 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,7 @@ kcap status Show server, auth, and daemon status
24
24
  kcap daemon start [-d] Start the daemon
25
25
  kcap daemon stop Stop the daemon
26
26
  kcap review <pr> Launch Claude Code with PR review context
27
- kcap update Check for updates
27
+ kcap update Upgrade the CLI and refresh agent plugins
28
28
  kcap --version Show version
29
29
  kcap --help Show all commands
30
30
  ```
package/bin/kcap.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // Resolves and exec's the native kcap binary for the current platform.
4
4
 
5
- const { execFileSync } = require("child_process");
5
+ const { execFileSync, spawnSync } = require("child_process");
6
6
  const path = require("path");
7
7
  const fs = require("fs");
8
8
 
@@ -63,6 +63,21 @@ try {
63
63
  try { fs.chmodSync(binaryPath, 0o755); } catch {}
64
64
  }
65
65
 
66
+ // `kcap update` for npm-global installs is driven HERE, in the Node launcher,
67
+ // not by the native binary. The OS locks an executable image for the whole
68
+ // process lifetime but a script only during load — so by driving the upgrade
69
+ // from this script (with the native binary not running) npm can overwrite the
70
+ // binary even on Windows, with no temp-file/rename/detached-helper dance.
71
+ // `--check` (JSON probe) and `--help` fall through to the native binary.
72
+ {
73
+ const updArgs = process.argv.slice(3);
74
+ const checkOnly = updArgs.includes("--check");
75
+ const wantsHelp = updArgs.some((a) => a === "--help" || a === "-h");
76
+ if (process.argv[2] === "update" && !checkOnly && !wantsHelp) {
77
+ runUpdate(binaryPath); // never returns
78
+ }
79
+ }
80
+
66
81
  // Exec the native binary, replacing this process
67
82
  try {
68
83
  execFileSync(binaryPath, process.argv.slice(2), {
@@ -76,3 +91,87 @@ try {
76
91
  }
77
92
  process.exit(1);
78
93
  }
94
+
95
+ // Performs `kcap update`: upgrade the global npm package, then refresh
96
+ // user-scope skills/hooks. Always exits the process; never returns.
97
+ function runUpdate(binaryPath) {
98
+ // On Windows, `npm` is a .cmd shim; Node refuses to spawn .cmd/.bat directly
99
+ // (2024 command-injection fix), so route npm through a shell there. Our npm
100
+ // args are static, so shell use is safe.
101
+ const npmOpts = process.platform === "win32" ? { shell: true } : {};
102
+
103
+ // Resolve npm's global root so we can (a) confirm this is a global install we
104
+ // own and (b) check we can write to it before starting.
105
+ let globalRoot = null;
106
+ try {
107
+ globalRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf8", ...npmOpts }).trim();
108
+ } catch {}
109
+
110
+ let isGlobalNpm = false;
111
+ try {
112
+ if (globalRoot) {
113
+ const root = fs.realpathSync(globalRoot) + path.sep;
114
+ isGlobalNpm = fs.realpathSync(__filename).startsWith(root);
115
+ }
116
+ } catch {}
117
+
118
+ if (!isGlobalNpm) {
119
+ console.error("kcap was not installed via a global npm install, so it can't");
120
+ console.error("self-update. Update it the way you installed it, e.g.:");
121
+ console.error(" npm install -g @kurrent/kcap@latest (npm)");
122
+ console.error(" brew upgrade kcap (Homebrew)");
123
+ process.exit(1);
124
+ }
125
+
126
+ // Ask the native binary whether a newer version exists. This short-lived
127
+ // child fully EXITS before we run npm, so the binary file is unlocked when
128
+ // npm overwrites it (matters on Windows). `--no-update-check` keeps the
129
+ // background nudge from racing onto stderr.
130
+ try {
131
+ const out = execFileSync(binaryPath, ["update", "--check", "--no-update-check"], { encoding: "utf8" });
132
+ // The probe prints one JSON line; take the last {...} line in case anything
133
+ // else (e.g. a git-remote warning) landed on stdout first.
134
+ const line = out.split(/\r?\n/).reverse().find((l) => l.trim().startsWith("{"));
135
+ const info = JSON.parse(line);
136
+ // Only short-circuit when the probe is CONFIDENT we're up to date: `newer`
137
+ // explicitly false AND a known current version. `newer: null` (version
138
+ // unknown / check failed) falls through to the upgrade rather than stranding
139
+ // the user on a stale CLI.
140
+ if (info && info.newer === false && info.current) {
141
+ console.log(`Already up to date: ${info.current}`);
142
+ process.exit(0);
143
+ }
144
+ if (info && info.newer) {
145
+ console.log(`Updating kcap: ${info.current} → ${info.latest}`);
146
+ }
147
+ } catch {
148
+ // Couldn't determine — fall through and let npm decide (it's idempotent).
149
+ }
150
+
151
+ // Fail clearly instead of half-installing under a root-owned prefix.
152
+ try {
153
+ fs.accessSync(globalRoot, fs.constants.W_OK);
154
+ } catch {
155
+ console.error(`Cannot write to the global npm directory (${globalRoot}).`);
156
+ console.error("Re-run with the permissions you installed kcap with, or reinstall");
157
+ console.error("to a user-owned prefix to avoid needing elevated rights on updates.");
158
+ process.exit(1);
159
+ }
160
+
161
+ const res = spawnSync("npm", ["install", "-g", "@kurrent/kcap@latest"], {
162
+ stdio: "inherit",
163
+ windowsHide: true,
164
+ ...npmOpts,
165
+ });
166
+ if (res.status !== 0) {
167
+ console.error("npm install failed; kcap was not updated.");
168
+ process.exit(res.status == null ? 1 : res.status);
169
+ }
170
+
171
+ // npm has now overwritten kcap.js, refresh.js, and the binary. require()
172
+ // reads the NEW refresh.js, which spawns the NEW launcher -> NEW binary.
173
+ console.log("Refreshing hooks and skills…");
174
+ require("./refresh").runRefreshes(fs.realpathSync(__filename));
175
+ console.log("kcap updated.");
176
+ process.exit(0);
177
+ }
@@ -2,58 +2,30 @@
2
2
 
3
3
  // Runs after `npm install -g @kurrent/kcap` (including upgrades).
4
4
  //
5
- // Refreshes user-scope kcap agent installations so users pick up
6
- // new or updated skills, Codex hook commands, Cursor hook commands, and
7
- // Claude plugin registration without manually re-running `kcap setup`.
5
+ // Refreshes user-scope kcap agent installations so users pick up new or updated
6
+ // skills, hook commands, and Claude plugin registration without manually
7
+ // re-running `kcap setup`. The actual refresh logic lives in refresh.js, shared
8
+ // with the `kcap update` path.
8
9
  //
9
10
  // Contract:
10
- // - Only runs on global installs. Skipping non-global installs avoids
11
- // touching ~/.agents/, ~/.codex/, ~/.cursor/, or ~/.claude/ during unrelated
11
+ // - Only runs on global installs. Skipping non-global installs avoids touching
12
+ // ~/.agents/, ~/.codex/, ~/.cursor/, or ~/.claude/ during unrelated
12
13
  // local/transitive installs on already-opted-in machines.
13
- // - Each refresh uses `--if-installed`, which no-ops unless the user
14
- // has previously opted in (marker file present OR pre-marker install
15
- // detected via existing kcap entries in the target file).
16
- // - Each refresh runs independently. A failure, timeout, or unexpected
17
- // exit code from one does not prevent the others. The script always
18
- // exits 0 — a failed refresh must never break `npm install`.
14
+ // - Always exits 0 a failed refresh must never break `npm install`.
15
+ //
16
+ // Note: modern package managers gate install scripts behind an "allowed
17
+ // scripts" allowlist, so this hook may not run on upgrade. `kcap update` runs
18
+ // the same refresh and is not subject to that gate (the user invokes it
19
+ // explicitly).
19
20
 
20
- const { spawnSync } = require("child_process");
21
21
  const path = require("path");
22
22
 
23
23
  const isGlobal =
24
24
  process.env.npm_config_global === "true" ||
25
25
  process.env.npm_config_location === "global";
26
26
 
27
- if (!isGlobal) {
28
- process.exit(0);
29
- }
30
-
31
- const launcher = path.join(__dirname, "kcap.js");
32
-
33
- // One entry per agent. Order is independent — each refresh is gated by
34
- // its own marker.
35
- const refreshes = [
36
- ["plugin", "install", "--skills", "--if-installed"],
37
- ["plugin", "install", "--codex", "--if-installed"],
38
- ["plugin", "install", "--cursor", "--if-installed"],
39
- ["plugin", "install", "--copilot", "--if-installed"],
40
- ["plugin", "install", "--if-installed"], // Claude
41
- ];
42
-
43
- for (const argv of refreshes) {
44
- try {
45
- spawnSync(process.execPath, [launcher, ...argv], {
46
- stdio: "ignore",
47
- env: process.env,
48
- // Hard ceiling so a stalled child can never hang `npm install`.
49
- // Each refresh is bounded independently.
50
- timeout: 60_000,
51
- killSignal: "SIGKILL",
52
- windowsHide: true,
53
- });
54
- } catch {
55
- // Never fail npm install.
56
- }
27
+ if (isGlobal) {
28
+ require("./refresh").runRefreshes(path.join(__dirname, "kcap.js"));
57
29
  }
58
30
 
59
31
  process.exit(0);
package/bin/refresh.js ADDED
@@ -0,0 +1,44 @@
1
+ // Refreshes user-scope kcap agent installations so users pick up new or updated
2
+ // skills, Codex/Cursor/Copilot hook commands, and Claude plugin registration.
3
+ //
4
+ // Shared by:
5
+ // - postinstall.js — runs after `npm install -g @kurrent/kcap` (incl. upgrades),
6
+ // when the package manager allows install scripts to run.
7
+ // - kcap.js `update` — runs after a user-initiated `kcap update`, which works
8
+ // even when the package manager blocks postinstall scripts.
9
+
10
+ const { spawnSync } = require("child_process");
11
+
12
+ // One entry per agent. Order is independent — each refresh is gated by its own
13
+ // marker via `--if-installed`, which no-ops unless the user has previously
14
+ // opted in (marker file present OR pre-marker install detected).
15
+ const REFRESHES = [
16
+ ["plugin", "install", "--skills", "--if-installed"],
17
+ ["plugin", "install", "--codex", "--if-installed"],
18
+ ["plugin", "install", "--cursor", "--if-installed"],
19
+ ["plugin", "install", "--copilot", "--if-installed"],
20
+ ["plugin", "install", "--if-installed"], // Claude
21
+ ];
22
+
23
+ // Runs each refresh via the given launcher (an absolute path to kcap.js).
24
+ // Each refresh runs independently: a failure, timeout, or unexpected exit code
25
+ // from one never prevents the others and never throws to the caller — a failed
26
+ // refresh must never break `npm install` or report `kcap update` as failed.
27
+ function runRefreshes(launcherPath) {
28
+ for (const argv of REFRESHES) {
29
+ try {
30
+ spawnSync(process.execPath, [launcherPath, ...argv], {
31
+ stdio: "ignore",
32
+ env: process.env,
33
+ // Hard ceiling so a stalled child can never hang the caller.
34
+ timeout: 60_000,
35
+ killSignal: "SIGKILL",
36
+ windowsHide: true,
37
+ });
38
+ } catch {
39
+ // Never fail the caller.
40
+ }
41
+ }
42
+ }
43
+
44
+ module.exports = { runRefreshes };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kcap",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Records and visualizes Claude Code sessions via kcap CLI hooks"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kurrent/kcap",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "CLI companion for Kurrent Capacitor — records and visualizes Claude Code sessions",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "repository": {
@@ -14,12 +14,12 @@
14
14
  "postinstall": "node bin/postinstall.js"
15
15
  },
16
16
  "optionalDependencies": {
17
- "@kurrent/kcap-darwin-arm64": "0.7.0",
18
- "@kurrent/kcap-linux-x64": "0.7.0",
19
- "@kurrent/kcap-linux-arm64": "0.7.0",
20
- "@kurrent/kcap-linux-musl-x64": "0.7.0",
21
- "@kurrent/kcap-linux-musl-arm64": "0.7.0",
22
- "@kurrent/kcap-win-x64": "0.7.0"
17
+ "@kurrent/kcap-darwin-arm64": "0.7.2",
18
+ "@kurrent/kcap-linux-x64": "0.7.2",
19
+ "@kurrent/kcap-linux-arm64": "0.7.2",
20
+ "@kurrent/kcap-linux-musl-x64": "0.7.2",
21
+ "@kurrent/kcap-linux-musl-arm64": "0.7.2",
22
+ "@kurrent/kcap-win-x64": "0.7.2"
23
23
  },
24
24
  "files": [
25
25
  "bin/",