claudeup 4.18.0 → 4.19.0

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.
@@ -295,6 +295,65 @@ export function extractGoBinaryName(pkg: string): string {
295
295
  return withoutVersion.split("/").pop() || pkg;
296
296
  }
297
297
 
298
+ /**
299
+ * Extract the pinned version from a Go module path, or null when unpinned.
300
+ * e.g., "github.com/MadAppGang/tmux-mcp@v1.6.2" → "v1.6.2"
301
+ * "github.com/MadAppGang/tmux-mcp@latest" → null (a moving target)
302
+ * "github.com/user/tool" → null
303
+ */
304
+ export function extractGoVersion(pkg: string): string | null {
305
+ const at = pkg.lastIndexOf("@");
306
+ if (at === -1) return null;
307
+ const version = pkg.slice(at + 1);
308
+ if (version === "" || version === "latest") return null;
309
+ return version;
310
+ }
311
+
312
+ /** Normalize a semver-ish string for comparison: trim and drop a leading "v". */
313
+ function normalizeVersion(v: string): string {
314
+ return v.trim().replace(/^v/, "");
315
+ }
316
+
317
+ /**
318
+ * Read an installed binary's version via `<binary> --version`, normalized, or
319
+ * null if it produced no usable version string. Used to tell whether a pinned Go
320
+ * dependency is already at the right version.
321
+ */
322
+ async function getInstalledBinaryVersion(
323
+ binaryName: string,
324
+ ): Promise<string | null> {
325
+ const { ok, stdout } = await run(binaryName, ["--version"], 10000);
326
+ if (!ok || !stdout) return null;
327
+ const match = stdout.split("\n")[0].match(/v?\d+\.\d+\.\d+[^\s]*/);
328
+ return match ? normalizeVersion(match[0]) : null;
329
+ }
330
+
331
+ /**
332
+ * Decide whether a Go dependency needs (re)installing. This is what makes a
333
+ * plugin *update* actually update its binary: without a version check an
334
+ * already-present binary is skipped forever, so a bumped plugin never pulls the
335
+ * new binary.
336
+ *
337
+ * - binary absent → yes
338
+ * - pinned version, installed reports the same one → no
339
+ * - pinned version, installed reports a different one → yes
340
+ * - pinned version, installed version can't be read → yes (can't confirm the
341
+ * pin, so reinstall to be safe)
342
+ * - unpinned (@latest) and present → no (a moving target
343
+ * can't be verified without a network round-trip)
344
+ */
345
+ export async function goDepNeedsInstall(pkg: string): Promise<boolean> {
346
+ const binaryName = extractGoBinaryName(pkg);
347
+ if (!(await isBinaryAvailable(binaryName))) return true;
348
+
349
+ const pinned = extractGoVersion(pkg);
350
+ if (!pinned) return false;
351
+
352
+ const installed = await getInstalledBinaryVersion(binaryName);
353
+ if (installed === null) return true;
354
+ return normalizeVersion(pinned) !== installed;
355
+ }
356
+
298
357
  /**
299
358
  * Install Go packages via `go install`
300
359
  */
@@ -311,8 +370,7 @@ async function installGoPackages(
311
370
  }
312
371
 
313
372
  for (const pkg of packages) {
314
- const binaryName = extractGoBinaryName(pkg);
315
- if (await isBinaryAvailable(binaryName)) {
373
+ if (!(await goDepNeedsInstall(pkg))) {
316
374
  result.skipped.push(`go:${pkg}`);
317
375
  continue;
318
376
  }
@@ -515,8 +573,7 @@ export async function checkMissingDeps(
515
573
  if (setup.go?.length) {
516
574
  const missingGo: string[] = [];
517
575
  for (const pkg of setup.go) {
518
- const bin = extractGoBinaryName(pkg);
519
- if (!(await isBinaryAvailable(bin))) {
576
+ if (await goDepNeedsInstall(pkg)) {
520
577
  missingGo.push(pkg);
521
578
  }
522
579
  }
package/src/ui/App.js CHANGED
@@ -53,7 +53,7 @@ function Router() {
53
53
  }
54
54
  /**
55
55
  * GlobalKeyHandler Component
56
- * Handles global keyboard shortcuts (1-5, Tab, Escape, q, ?, Shift+D)
56
+ * Handles global keyboard shortcuts (1-8, Tab, Escape, q, Ctrl+C, ?, Shift+D)
57
57
  * Does not render anything (returns null)
58
58
  */
59
59
  function GlobalKeyHandler({ onDebugToggle, onExit, }) {
@@ -139,18 +139,26 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
139
139
  }
140
140
  }
141
141
  }
142
- // Escape/q to go back or exit
143
- if (key.escape || input === "q") {
142
+ // q or Ctrl+C quits from anywhere — no need to bounce to home first.
143
+ if (input === "q" || (key.ctrl && input === "c")) {
144
+ onExit();
145
+ return;
146
+ }
147
+ // Escape goes back: registry → mcp, any other tab → plugins home,
148
+ // already-home → exit. But when a screen has a focused sub-view open
149
+ // (e.g. the Alias flag-detail editor), that sub-view owns Escape and
150
+ // backs out to itself — don't let the global handler jump to home.
151
+ // Scoped to the Escape branch ONLY so q/Ctrl+C still quit from anywhere.
152
+ if (key.escape) {
153
+ if (state.detailActive)
154
+ return;
144
155
  if (state.currentRoute.screen === "plugins") {
145
- // On home screen, exit immediately
146
156
  onExit();
147
157
  }
148
158
  else if (state.currentRoute.screen === "mcp-registry") {
149
- // Go back to MCP from registry
150
159
  navigateToScreen("mcp");
151
160
  }
152
161
  else {
153
- // Go back to plugins (home)
154
162
  navigateToScreen("plugins");
155
163
  }
156
164
  }
@@ -159,7 +167,8 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
159
167
  modal.message("claudeup Help", `Navigation
160
168
  ↑/↓ or j/k Move selection
161
169
  Enter Select / Toggle
162
- Escape or q Back / Quit
170
+ Escape Back (to home)
171
+ q / Ctrl+C Quit (from anywhere)
163
172
  ? This help
164
173
 
165
174
  Quick Navigation
package/src/ui/App.tsx CHANGED
@@ -89,7 +89,7 @@ function Router() {
89
89
 
90
90
  /**
91
91
  * GlobalKeyHandler Component
92
- * Handles global keyboard shortcuts (1-5, Tab, Escape, q, ?, Shift+D)
92
+ * Handles global keyboard shortcuts (1-8, Tab, Escape, q, Ctrl+C, ?, Shift+D)
93
93
  * Does not render anything (returns null)
94
94
  */
95
95
  function GlobalKeyHandler({
@@ -180,16 +180,24 @@ function GlobalKeyHandler({
180
180
  }
181
181
  }
182
182
 
183
- // Escape/q to go back or exit
184
- if (key.escape || input === "q") {
183
+ // q or Ctrl+C quits from anywhere — no need to bounce to home first.
184
+ if (input === "q" || (key.ctrl && input === "c")) {
185
+ onExit();
186
+ return;
187
+ }
188
+
189
+ // Escape goes back: registry → mcp, any other tab → plugins home,
190
+ // already-home → exit. But when a screen has a focused sub-view open
191
+ // (e.g. the Alias flag-detail editor), that sub-view owns Escape and
192
+ // backs out to itself — don't let the global handler jump to home.
193
+ // Scoped to the Escape branch ONLY so q/Ctrl+C still quit from anywhere.
194
+ if (key.escape) {
195
+ if (state.detailActive) return;
185
196
  if (state.currentRoute.screen === "plugins") {
186
- // On home screen, exit immediately
187
197
  onExit();
188
198
  } else if (state.currentRoute.screen === "mcp-registry") {
189
- // Go back to MCP from registry
190
199
  navigateToScreen("mcp");
191
200
  } else {
192
- // Go back to plugins (home)
193
201
  navigateToScreen("plugins");
194
202
  }
195
203
  }
@@ -201,7 +209,8 @@ function GlobalKeyHandler({
201
209
  `Navigation
202
210
  ↑/↓ or j/k Move selection
203
211
  Enter Select / Toggle
204
- Escape or q Back / Quit
212
+ Escape Back (to home)
213
+ q / Ctrl+C Quit (from anywhere)
205
214
  ? This help
206
215
 
207
216
  Quick Navigation
@@ -9,6 +9,6 @@ export function ConfirmModal({ title, message, onConfirm, onCancel, }) {
9
9
  onCancel();
10
10
  }
11
11
  });
12
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsxs("box", { marginTop: 1, children: [_jsx("text", { fg: "#71717A", children: "Press " }), _jsx("text", { fg: "#EDEDED", children: "Y" }), _jsx("text", { fg: "#71717A", children: " to confirm \u2022 " }), _jsx("text", { fg: "#EDEDED", children: "N" }), _jsx("text", { fg: "#71717A", children: " to cancel" })] })] }));
12
+ return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsxs("box", { marginTop: 1, flexDirection: "row", children: [_jsx("text", { fg: "#71717A", children: "Press " }), _jsx("text", { fg: "#EDEDED", children: "Y" }), _jsx("text", { fg: "#71717A", children: " to confirm \u2022 " }), _jsx("text", { fg: "#EDEDED", children: "N" }), _jsx("text", { fg: "#71717A", children: " to cancel" })] })] }));
13
13
  }
14
14
  export default ConfirmModal;
@@ -47,7 +47,7 @@ export function ConfirmModal({
47
47
  <box marginBottom={1}>
48
48
  <text fg="#A1A1AA">{message}</text>
49
49
  </box>
50
- <box marginTop={1}>
50
+ <box marginTop={1} flexDirection="row">
51
51
  <text fg="#71717A">Press </text>
52
52
  <text fg="#EDEDED">Y</text>
53
53
  <text fg="#71717A"> to confirm • </text>