Skip to content

Commit 1267c8d

Browse files
catlog22claude
andcommitted
feat(dashboard): add npm version update notification
- Add /api/version-check endpoint to check npm registry for updates - Create version-check.js component with update banner UI - Add CSS styles for version update banner - Fix hook manager button event handling (use e.currentTarget) - Bump version to 6.1.2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent eb10931 commit 1267c8d

File tree

6 files changed

+425
-10
lines changed

6 files changed

+425
-10
lines changed

ccw/src/core/server.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ const MODULE_FILES = [
8686
'components/carousel.js',
8787
'components/notifications.js',
8888
'components/global-notifications.js',
89+
'components/version-check.js',
8990
'components/mcp-manager.js',
9091
'components/hook-manager.js',
9192
'components/_exp_helpers.js',
@@ -191,6 +192,15 @@ export async function startServer(options = {}) {
191192
return;
192193
}
193194

195+
// API: Version check (check for npm updates)
196+
if (pathname === '/api/version-check') {
197+
const versionData = await checkNpmVersion();
198+
res.writeHead(200, { 'Content-Type': 'application/json' });
199+
res.end(JSON.stringify(versionData));
200+
return;
201+
}
202+
203+
194204
// API: Shutdown server (for ccw stop command)
195205
if (pathname === '/api/shutdown' && req.method === 'POST') {
196206
res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1946,3 +1956,108 @@ async function triggerUpdateClaudeMd(targetPath, tool, strategy) {
19461956
}, 300000);
19471957
});
19481958
}
1959+
1960+
1961+
// ========================================
1962+
// Version Check Functions
1963+
// ========================================
1964+
1965+
// Package name on npm registry
1966+
const NPM_PACKAGE_NAME = 'claude-code-workflow';
1967+
1968+
// Cache for version check (avoid too frequent requests)
1969+
let versionCheckCache = null;
1970+
let versionCheckTime = 0;
1971+
const VERSION_CHECK_CACHE_TTL = 3600000; // 1 hour
1972+
1973+
/**
1974+
* Get current package version from package.json
1975+
* @returns {string}
1976+
*/
1977+
function getCurrentVersion() {
1978+
try {
1979+
const packageJsonPath = join(import.meta.dirname, '../../../package.json');
1980+
if (existsSync(packageJsonPath)) {
1981+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
1982+
return pkg.version || '0.0.0';
1983+
}
1984+
} catch (e) {
1985+
console.error('Error reading package.json:', e);
1986+
}
1987+
return '0.0.0';
1988+
}
1989+
1990+
/**
1991+
* Check npm registry for latest version
1992+
* @returns {Promise<Object>}
1993+
*/
1994+
async function checkNpmVersion() {
1995+
// Return cached result if still valid
1996+
const now = Date.now();
1997+
if (versionCheckCache && (now - versionCheckTime) < VERSION_CHECK_CACHE_TTL) {
1998+
return versionCheckCache;
1999+
}
2000+
2001+
const currentVersion = getCurrentVersion();
2002+
2003+
try {
2004+
// Fetch latest version from npm registry
2005+
const npmUrl = 'https://registry.npmjs.org/' + encodeURIComponent(NPM_PACKAGE_NAME) + '/latest';
2006+
const response = await fetch(npmUrl, {
2007+
headers: { 'Accept': 'application/json' }
2008+
});
2009+
2010+
if (!response.ok) {
2011+
throw new Error('HTTP ' + response.status);
2012+
}
2013+
2014+
const data = await response.json();
2015+
const latestVersion = data.version;
2016+
2017+
// Compare versions
2018+
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
2019+
2020+
const result = {
2021+
currentVersion,
2022+
latestVersion,
2023+
hasUpdate,
2024+
packageName: NPM_PACKAGE_NAME,
2025+
updateCommand: 'npm update -g ' + NPM_PACKAGE_NAME,
2026+
checkedAt: new Date().toISOString()
2027+
};
2028+
2029+
// Cache the result
2030+
versionCheckCache = result;
2031+
versionCheckTime = now;
2032+
2033+
return result;
2034+
} catch (error) {
2035+
console.error('Version check failed:', error.message);
2036+
return {
2037+
currentVersion,
2038+
latestVersion: null,
2039+
hasUpdate: false,
2040+
error: error.message,
2041+
checkedAt: new Date().toISOString()
2042+
};
2043+
}
2044+
}
2045+
2046+
/**
2047+
* Compare two semver versions
2048+
* @param {string} v1
2049+
* @param {string} v2
2050+
* @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
2051+
*/
2052+
function compareVersions(v1, v2) {
2053+
const parts1 = v1.split('.').map(Number);
2054+
const parts2 = v2.split('.').map(Number);
2055+
2056+
for (let i = 0; i < 3; i++) {
2057+
const p1 = parts1[i] || 0;
2058+
const p2 = parts2[i] || 0;
2059+
if (p1 > p2) return 1;
2060+
if (p1 < p2) return -1;
2061+
}
2062+
return 0;
2063+
}

ccw/src/templates/dashboard-css/01-base.css

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,133 @@ body {
159159
display: block;
160160
}
161161

162+
163+
/* ===================================
164+
Version Update Banner
165+
=================================== */
166+
167+
.version-update-banner {
168+
position: sticky;
169+
top: 0;
170+
z-index: 100;
171+
background: linear-gradient(135deg, hsl(var(--primary) / 0.1), hsl(var(--accent) / 0.1));
172+
border-bottom: 1px solid hsl(var(--primary) / 0.3);
173+
padding: 0.75rem 1rem;
174+
transform: translateY(-100%);
175+
opacity: 0;
176+
transition: transform 0.3s ease, opacity 0.3s ease;
177+
}
178+
179+
.version-update-banner.show {
180+
transform: translateY(0);
181+
opacity: 1;
182+
}
183+
184+
.version-banner-content {
185+
display: flex;
186+
align-items: center;
187+
gap: 0.75rem;
188+
max-width: 1400px;
189+
margin: 0 auto;
190+
flex-wrap: wrap;
191+
}
192+
193+
.version-banner-icon {
194+
font-size: 1.25rem;
195+
}
196+
197+
.version-banner-text {
198+
flex: 1;
199+
font-size: 0.875rem;
200+
color: hsl(var(--foreground));
201+
}
202+
203+
.version-banner-text code {
204+
background: hsl(var(--muted));
205+
padding: 0.125rem 0.375rem;
206+
border-radius: 0.25rem;
207+
font-family: var(--font-mono);
208+
font-size: 0.8125rem;
209+
}
210+
211+
.version-banner-btn {
212+
display: inline-flex;
213+
align-items: center;
214+
gap: 0.375rem;
215+
padding: 0.375rem 0.75rem;
216+
font-size: 0.8125rem;
217+
font-weight: 500;
218+
color: hsl(var(--primary-foreground));
219+
background: hsl(var(--primary));
220+
border: none;
221+
border-radius: 0.375rem;
222+
cursor: pointer;
223+
transition: background 0.2s, transform 0.1s;
224+
}
225+
226+
.version-banner-btn:hover {
227+
background: hsl(var(--primary) / 0.9);
228+
transform: translateY(-1px);
229+
}
230+
231+
.version-banner-btn:active {
232+
transform: translateY(0);
233+
}
234+
235+
.version-banner-btn.secondary {
236+
background: hsl(var(--secondary));
237+
color: hsl(var(--secondary-foreground));
238+
}
239+
240+
.version-banner-btn.secondary:hover {
241+
background: hsl(var(--secondary) / 0.8);
242+
}
243+
244+
.version-banner-close {
245+
width: 1.5rem;
246+
height: 1.5rem;
247+
display: flex;
248+
align-items: center;
249+
justify-content: center;
250+
font-size: 1.25rem;
251+
color: hsl(var(--muted-foreground));
252+
background: transparent;
253+
border: none;
254+
border-radius: 0.25rem;
255+
cursor: pointer;
256+
transition: background 0.2s, color 0.2s;
257+
margin-left: auto;
258+
}
259+
260+
.version-banner-close:hover {
261+
background: hsl(var(--destructive) / 0.1);
262+
color: hsl(var(--destructive));
263+
}
264+
265+
/* Mobile responsiveness for banner */
266+
@media (max-width: 640px) {
267+
.version-banner-content {
268+
gap: 0.5rem;
269+
}
270+
271+
.version-banner-text {
272+
width: 100%;
273+
order: -1;
274+
}
275+
276+
.version-banner-btn {
277+
flex: 1;
278+
justify-content: center;
279+
}
280+
281+
.version-banner-close {
282+
position: absolute;
283+
top: 0.5rem;
284+
right: 0.5rem;
285+
}
286+
287+
.version-update-banner {
288+
position: relative;
289+
padding-right: 2.5rem;
290+
}
291+
}

0 commit comments

Comments
 (0)