Skip to content

Commit 803e0f1

Browse files
ithiria894claude
andcommitted
feat: update check — CLI + UI sidebar banner with copy-to-clipboard
- /api/version endpoint compares local vs npm registry latest - CLI shows update notice after server starts (non-blocking) - UI sidebar footer shows yellow banner when outdated - Hover: "Click to copy update command — paste into Claude Code" - Click: copies prompt to clipboard, user pastes into CC to update Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c18bf8c commit 803e0f1

6 files changed

Lines changed: 92 additions & 3 deletions

File tree

bin/cli.mjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,26 @@ if (!isMcpMode) {
6060
}
6161
}
6262

63+
// ── Update check (non-blocking) ──
64+
async function checkForUpdate() {
65+
try {
66+
const { createRequire } = await import('node:module');
67+
const require = createRequire(import.meta.url);
68+
const pkg = require('../package.json');
69+
const localVersion = pkg.version;
70+
71+
const resp = await fetch('https://registry.npmjs.org/@mcpware/claude-code-organizer/latest', { signal: AbortSignal.timeout(3000) });
72+
if (!resp.ok) return null;
73+
const data = await resp.json();
74+
const latestVersion = data.version;
75+
76+
if (localVersion !== latestVersion) {
77+
return { local: localVersion, latest: latestVersion };
78+
}
79+
} catch { /* silent — don't block startup */ }
80+
return null;
81+
}
82+
6383
if (isMcpMode) {
6484
// MCP server mode — AI clients connect via stdio
6585
await import('../src/mcp-server.mjs');
@@ -71,8 +91,20 @@ if (isMcpMode) {
7191
const portIdx = args.indexOf('--port');
7292
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 3847;
7393

94+
// Check for update in background (don't block server start)
95+
const updatePromise = checkForUpdate();
96+
7497
startServer(port);
7598

99+
// Show update notice after server starts (CLI users)
100+
updatePromise.then(update => {
101+
if (update) {
102+
console.log(`\n 📦 New version available! You're not on the latest.`);
103+
console.log(` Run: npx @mcpware/claude-code-organizer@latest`);
104+
console.log(` Or: npm update -g @mcpware/claude-code-organizer\n`);
105+
}
106+
});
107+
76108
try {
77109
const openCmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
78110
execSync(`${openCmd} http://localhost:${port}`, { stdio: 'ignore' });

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mcpware/claude-code-organizer",
3-
"version": "0.5.9",
3+
"version": "0.5.10",
44
"description": "Organize all your Claude Code memories, skills, MCP servers, commands, agents, rules, and hooks — view by scope hierarchy, move between scopes via drag-and-drop",
55
"type": "module",
66
"bin": {

src/server.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,27 @@ async function handleRequest(req, res) {
104104

105105
// ── API routes ──
106106

107+
// GET /api/version — check for updates (UI calls this)
108+
if (path === "/api/version" && req.method === "GET") {
109+
const require = createRequire(import.meta.url);
110+
const { version: local } = require("../package.json");
111+
try {
112+
const data = await new Promise((resolve, reject) => {
113+
const req = https.get("https://registry.npmjs.org/@mcpware/claude-code-organizer/latest", { timeout: 3000 }, (res) => {
114+
let body = "";
115+
res.on("data", (c) => (body += c));
116+
res.on("end", () => resolve(body));
117+
});
118+
req.on("error", reject);
119+
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
120+
});
121+
const { version: latest } = JSON.parse(data);
122+
return json(res, { local, latest, updateAvailable: latest !== local });
123+
} catch {
124+
return json(res, { local, latest: null, updateAvailable: false });
125+
}
126+
}
127+
107128
// GET /api/scan — full scan of all customizations
108129
if (path === "/api/scan" && req.method === "GET") {
109130
const data = await freshScan();

src/ui/app.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,34 @@ async function init() {
9292
initializeScopeState();
9393
setupUi();
9494
renderAll();
95+
checkForUpdate();
9596
} catch (error) {
9697
document.getElementById("loading").textContent = "Failed to load inventory";
9798
toast(error?.message || "Failed to load inventory", true);
9899
}
99100
}
100101

102+
async function checkForUpdate() {
103+
try {
104+
const { updateAvailable } = await fetchJson("/api/version");
105+
if (!updateAvailable) return;
106+
const footer = document.querySelector(".sidebar-footer");
107+
if (!footer) return;
108+
const banner = document.createElement("div");
109+
banner.className = "update-banner";
110+
banner.innerHTML = "🔄 New version available — <code>npx @mcpware/claude-code-organizer@latest</code>";
111+
banner.addEventListener("click", () => {
112+
navigator.clipboard.writeText("Run npx @mcpware/claude-code-organizer@latest to update Claude Code Organizer to the latest version.").then(() => {
113+
banner.innerHTML = "✅ Copied! Paste into Claude Code";
114+
setTimeout(() => {
115+
banner.innerHTML = "🔄 New version available — <code>npx @mcpware/claude-code-organizer@latest</code>";
116+
}, 2000);
117+
});
118+
});
119+
footer.prepend(banner);
120+
} catch { /* silent */ }
121+
}
122+
101123
async function fetchJson(url) {
102124
const res = await fetch(url);
103125
return res.json();

src/ui/style.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,20 @@ body {
254254
padding: 1px 5px;
255255
border-radius: 3px;
256256
}
257+
.update-banner {
258+
font-size: 0.65rem;
259+
color: #f0c040 !important;
260+
background: oklch(0.25 0.06 80);
261+
padding: 6px 8px !important;
262+
border-radius: 6px;
263+
margin-bottom: 4px;
264+
display: block;
265+
text-align: center;
266+
}
267+
.update-banner:hover { background: oklch(0.30 0.07 80) !important; cursor: pointer; }
268+
.update-banner::after { content: "Click to copy update command — paste into Claude Code"; display: block; font-size: 0.55rem; opacity: 0; transition: opacity 150ms; margin-top: 2px; color: #d4c080; }
269+
.update-banner:hover::after { opacity: 1; }
270+
.update-banner code { background: oklch(0.20 0.04 80); padding: 1px 5px; border-radius: 3px; font-size: 0.6rem; }
257271
.s-cat-ico { font-size: 0.78rem; }
258272
.s-cat-nm { font-weight: 400; flex: 1; }
259273
.s-cat-cnt { font-size: 0.65rem; }

0 commit comments

Comments
 (0)