mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
feat: improve CLI update check with caching
This commit is contained in:
5
.changeset/hip-pots-study.md
Normal file
5
.changeset/hip-pots-study.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve CLI startup speed by 2x
|
||||||
@@ -1,88 +1,187 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Check npm registry for package updates
|
* @fileoverview Check npm registry for package updates with caching
|
||||||
|
*
|
||||||
|
* Uses a simple file-based cache in the OS temp directory to avoid
|
||||||
|
* hitting npm on every CLI invocation. Cache expires after 1 hour.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
import { fetchChangelogHighlights } from './changelog.js';
|
import { fetchChangelogHighlights } from './changelog.js';
|
||||||
import type { UpdateInfo } from './types.js';
|
import type { UpdateInfo } from './types.js';
|
||||||
import { compareVersions, getCurrentVersion } from './version.js';
|
import { compareVersions, getCurrentVersion } from './version.js';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cache Configuration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Cache TTL: 1 hour in milliseconds */
|
||||||
|
const CACHE_TTL_MS = 60 * 60 * 1000;
|
||||||
|
|
||||||
|
/** Cache file name */
|
||||||
|
const CACHE_FILENAME = 'taskmaster-update-cache.json';
|
||||||
|
|
||||||
|
interface UpdateCache {
|
||||||
|
timestamp: number;
|
||||||
|
latestVersion: string;
|
||||||
|
highlights?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cache Operations (Single Responsibility: cache I/O)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the update cache file in OS temp directory
|
||||||
|
*/
|
||||||
|
const getCachePath = (): string => path.join(os.tmpdir(), CACHE_FILENAME);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read cached update info if still valid
|
||||||
|
* @returns Cached data or null if expired/missing/invalid
|
||||||
|
*/
|
||||||
|
function readCache(): UpdateCache | null {
|
||||||
|
try {
|
||||||
|
const cachePath = getCachePath();
|
||||||
|
if (!fs.existsSync(cachePath)) return null;
|
||||||
|
|
||||||
|
const data: UpdateCache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
||||||
|
const isExpired = Date.now() - data.timestamp > CACHE_TTL_MS;
|
||||||
|
|
||||||
|
return isExpired ? null : data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write update info to cache
|
||||||
|
*/
|
||||||
|
function writeCache(latestVersion: string, highlights?: string[]): void {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(
|
||||||
|
getCachePath(),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
timestamp: Date.now(),
|
||||||
|
latestVersion,
|
||||||
|
highlights
|
||||||
|
} satisfies UpdateCache,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Cache write failures are non-critical - silently ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// NPM Registry Operations (Single Responsibility: npm API)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Request timeout for npm registry */
|
||||||
|
const NPM_TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch latest version from npm registry
|
||||||
|
* @returns Latest version string or null on failure
|
||||||
|
*/
|
||||||
|
function fetchLatestVersion(currentVersion: string): Promise<string | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = https.request(
|
||||||
|
{
|
||||||
|
hostname: 'registry.npmjs.org',
|
||||||
|
path: '/task-master-ai',
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.npm.install-v1+json',
|
||||||
|
'User-Agent': `task-master-ai/${currentVersion}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => (data += chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const npmData = JSON.parse(data);
|
||||||
|
resolve(npmData['dist-tags']?.latest || null);
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on('error', () => resolve(null));
|
||||||
|
req.setTimeout(NPM_TIMEOUT_MS, () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Public API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build UpdateInfo response
|
||||||
|
*/
|
||||||
|
function buildUpdateInfo(
|
||||||
|
currentVersion: string,
|
||||||
|
latestVersion: string,
|
||||||
|
highlights?: string[]
|
||||||
|
): UpdateInfo {
|
||||||
|
return {
|
||||||
|
currentVersion,
|
||||||
|
latestVersion,
|
||||||
|
needsUpdate: compareVersions(currentVersion, latestVersion) < 0,
|
||||||
|
highlights
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for newer version of task-master-ai
|
* Check for newer version of task-master-ai
|
||||||
|
* Uses a 1-hour cache to avoid hitting npm on every CLI invocation
|
||||||
*/
|
*/
|
||||||
export async function checkForUpdate(
|
export async function checkForUpdate(
|
||||||
currentVersionOverride?: string
|
currentVersionOverride?: string
|
||||||
): Promise<UpdateInfo> {
|
): Promise<UpdateInfo> {
|
||||||
const currentVersion = currentVersionOverride || getCurrentVersion();
|
const currentVersion = currentVersionOverride || getCurrentVersion();
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
// Return cached result if valid
|
||||||
const options = {
|
const cached = readCache();
|
||||||
hostname: 'registry.npmjs.org',
|
if (cached) {
|
||||||
path: '/task-master-ai',
|
return buildUpdateInfo(
|
||||||
method: 'GET',
|
currentVersion,
|
||||||
headers: {
|
cached.latestVersion,
|
||||||
Accept: 'application/vnd.npm.install-v1+json',
|
cached.highlights
|
||||||
'User-Agent': `task-master-ai/${currentVersion}`
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const req = https.request(options, (res) => {
|
// Fetch from npm registry
|
||||||
let data = '';
|
const latestVersion = await fetchLatestVersion(currentVersion);
|
||||||
|
if (!latestVersion) {
|
||||||
|
return buildUpdateInfo(currentVersion, currentVersion);
|
||||||
|
}
|
||||||
|
|
||||||
res.on('data', (chunk) => {
|
// Fetch changelog highlights if update available
|
||||||
data += chunk;
|
const needsUpdate = compareVersions(currentVersion, latestVersion) < 0;
|
||||||
});
|
const highlights = needsUpdate
|
||||||
|
? await fetchChangelogHighlights(latestVersion)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
res.on('end', async () => {
|
// Cache result
|
||||||
try {
|
writeCache(latestVersion, highlights);
|
||||||
if (res.statusCode !== 200)
|
|
||||||
throw new Error(`npm registry status ${res.statusCode}`);
|
|
||||||
const npmData = JSON.parse(data);
|
|
||||||
const latestVersion = npmData['dist-tags']?.latest || currentVersion;
|
|
||||||
|
|
||||||
const needsUpdate =
|
return buildUpdateInfo(currentVersion, latestVersion, highlights);
|
||||||
compareVersions(currentVersion, latestVersion) < 0;
|
|
||||||
|
|
||||||
// Fetch highlights if update is needed
|
|
||||||
let highlights: string[] | undefined;
|
|
||||||
if (needsUpdate) {
|
|
||||||
highlights = await fetchChangelogHighlights(latestVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
currentVersion,
|
|
||||||
latestVersion,
|
|
||||||
needsUpdate,
|
|
||||||
highlights
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
resolve({
|
|
||||||
currentVersion,
|
|
||||||
latestVersion: currentVersion,
|
|
||||||
needsUpdate: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', () => {
|
|
||||||
resolve({
|
|
||||||
currentVersion,
|
|
||||||
latestVersion: currentVersion,
|
|
||||||
needsUpdate: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.setTimeout(3000, () => {
|
|
||||||
req.destroy();
|
|
||||||
resolve({
|
|
||||||
currentVersion,
|
|
||||||
latestVersion: currentVersion,
|
|
||||||
needsUpdate: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import rootConfig from '../../vitest.config';
|
|||||||
/**
|
/**
|
||||||
* CLI package Vitest configuration
|
* CLI package Vitest configuration
|
||||||
* Extends root config with CLI-specific settings
|
* Extends root config with CLI-specific settings
|
||||||
|
*
|
||||||
|
* Integration tests (.test.ts) spawn CLI processes and need more time.
|
||||||
|
* The 30s timeout is reasonable now that auto-update network calls are skipped
|
||||||
|
* when TASKMASTER_SKIP_AUTO_UPDATE=1 or NODE_ENV=test.
|
||||||
*/
|
*/
|
||||||
export default mergeConfig(
|
export default mergeConfig(
|
||||||
rootConfig,
|
rootConfig,
|
||||||
@@ -15,7 +19,10 @@ export default mergeConfig(
|
|||||||
'tests/**/*.spec.ts',
|
'tests/**/*.spec.ts',
|
||||||
'src/**/*.test.ts',
|
'src/**/*.test.ts',
|
||||||
'src/**/*.spec.ts'
|
'src/**/*.spec.ts'
|
||||||
]
|
],
|
||||||
|
// Integration tests spawn CLI processes - 30s is reasonable with optimized startup
|
||||||
|
testTimeout: 30000,
|
||||||
|
hookTimeout: 15000
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5402,9 +5402,16 @@ async function runCLI(argv = process.argv) {
|
|||||||
displayBanner();
|
displayBanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for updates BEFORE executing the command
|
// Check for updates BEFORE executing the command (skip entirely in test/CI mode)
|
||||||
|
const skipAutoUpdate =
|
||||||
|
process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1' ||
|
||||||
|
process.env.CI ||
|
||||||
|
process.env.NODE_ENV === 'test';
|
||||||
|
|
||||||
const currentVersion = getTaskMasterVersion();
|
const currentVersion = getTaskMasterVersion();
|
||||||
const updateInfo = await checkForUpdate(currentVersion);
|
const updateInfo = skipAutoUpdate
|
||||||
|
? { currentVersion, latestVersion: currentVersion, needsUpdate: false }
|
||||||
|
: await checkForUpdate(currentVersion);
|
||||||
|
|
||||||
if (updateInfo.needsUpdate) {
|
if (updateInfo.needsUpdate) {
|
||||||
// Display the upgrade notification first
|
// Display the upgrade notification first
|
||||||
|
|||||||
Reference in New Issue
Block a user