This commit is contained in:
2025-11-03 16:02:12 +00:00
parent 4823eb9082
commit 1f2937b387
11 changed files with 15601 additions and 4350 deletions

7198
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,24 +9,23 @@
"author": "Lossless GmbH",
"license": "MIT",
"scripts": {
"test": "(tstest test/)",
"test": "(tstest test/ --verbose)",
"build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "tsdoc"
},
"devDependencies": {
"@gitzone/tsbuild": "^2.1.66",
"@gitzone/tsrun": "^1.2.44",
"@gitzone/tstest": "^1.0.77",
"@push.rocks/tapbundle": "^5.0.12",
"@types/node": "^20.4.5"
"@git.zone/tsbuild": "^2.7.1",
"@git.zone/tsrun": "^1.6.2",
"@git.zone/tstest": "^2.7.0",
"@types/node": "^22.19.0"
},
"dependencies": {
"@push.rocks/consolecolor": "^2.0.1",
"@push.rocks/npmextra": "^3.0.9",
"@push.rocks/smartnpm": "^2.0.4",
"@push.rocks/consolecolor": "^2.0.3",
"@push.rocks/npmextra": "^5.3.3",
"@push.rocks/smartnpm": "^2.0.6",
"@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smarttime": "^4.0.4",
"@push.rocks/smartversion": "^3.0.2"
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartversion": "^3.0.5"
},
"files": [
"ts/**/*",
@@ -59,5 +58,6 @@
"repository": {
"type": "git",
"url": "https://code.foss.global/push.rocks/smartupdate.git"
}
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
}

11680
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,127 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartupdate from '../ts/index.js';
let testSmartUpdate: smartupdate.SmartUpdate;
let silentSmartUpdate: smartupdate.SmartUpdate;
tap.test('should create an instance of SmartUpdate', async () => {
// Test suite for backward compatibility
tap.test('backward compatibility: should create an instance of SmartUpdate', async () => {
testSmartUpdate = new smartupdate.SmartUpdate();
expect(testSmartUpdate).toBeInstanceOf(smartupdate.SmartUpdate);
});
tap.test('should check for a npm module', async () => {
tap.test('backward compatibility: should check for a npm module using old API', async () => {
const result = await testSmartUpdate.check('lodash', '1.0.5');
expect(result).toBeTrue();
});
tap.start();
// Test suite for new modern API
tap.test('modern API: should create SmartUpdate with custom options', async () => {
silentSmartUpdate = new smartupdate.SmartUpdate({
logLevel: 'SILENT',
noColor: true,
cacheDuration: { minutes: 30 },
});
expect(silentSmartUpdate).toBeInstanceOf(smartupdate.SmartUpdate);
});
tap.test('modern API: checkForUpdate should return rich result object', async () => {
const result = await silentSmartUpdate.checkForUpdate({
packageName: 'lodash',
currentVersion: '1.0.0',
cacheStrategy: 'never',
});
// Verify result structure
expect(result).toBeTypeOf('object');
expect(result.status).toBeTypeOf('string');
expect(result.packageName).toEqual('lodash');
expect(result.currentVersion).toEqual('1.0.0');
expect(result.checkTime).toBeInstanceOf(Date);
expect(result.cacheHit).toBeTypeOf('boolean');
// Should have update available (1.0.0 is old)
expect(result.status).toEqual('update-available');
expect(result.latestVersion).toBeTypeOf('string');
});
tap.test('modern API: checkForUpdate with up-to-date version', async () => {
const result = await silentSmartUpdate.checkForUpdate({
packageName: '@push.rocks/smartversion',
currentVersion: '999.999.999', // Future version
cacheStrategy: 'never',
});
expect(result.status).toEqual('up-to-date');
expect(result.latestVersion).toBeTypeOf('string');
});
tap.test('modern API: getLatestVersion utility method', async () => {
const latestVersion = await silentSmartUpdate.getLatestVersion('lodash');
expect(latestVersion).toBeTypeOf('string');
expect(latestVersion).toMatch(/^\d+\.\d+\.\d+/); // Semver format
});
tap.test('modern API: error handling for non-existent package', async () => {
const result = await silentSmartUpdate.checkForUpdate({
packageName: 'this-package-definitely-does-not-exist-12345',
currentVersion: '1.0.0',
cacheStrategy: 'never',
});
expect(result.status).toEqual('error');
expect(result.error).toBeInstanceOf(Error);
});
tap.test('modern API: cache strategy works', async () => {
// Clear cache first to ensure clean state
const testPackage = 'express';
try {
await silentSmartUpdate.clearCache(testPackage);
} catch (e) {
// Cache might not exist, that's fine
}
// First check - should hit registry with 'never' strategy
const result1 = await silentSmartUpdate.checkForUpdate({
packageName: testPackage,
currentVersion: '1.0.0',
cacheStrategy: 'never',
});
expect(result1.cacheHit).toBeFalse();
expect(result1.status).toEqual('update-available');
// Do a second check with time-based that will create cache
const result2 = await silentSmartUpdate.checkForUpdate({
packageName: testPackage,
currentVersion: '1.0.0',
cacheStrategy: 'time-based',
});
// Third immediate check - should use cache with 'always' strategy
const result3 = await silentSmartUpdate.checkForUpdate({
packageName: testPackage,
currentVersion: '1.0.0',
cacheStrategy: 'always',
});
expect(result3.cacheHit).toBeTrue();
expect(result3.status).toEqual('check-skipped');
// nextCheckTime may or may not be set depending on cache strategy implementation
if (result3.nextCheckTime) {
expect(result3.nextCheckTime).toBeInstanceOf(Date);
}
});
tap.test('modern API: exports all types and classes', () => {
// Verify exports
expect(smartupdate.SmartUpdate).toBeTypeOf('function');
expect(smartupdate.UpdateCacheManager).toBeTypeOf('function');
expect(smartupdate.UpdateNotifier).toBeTypeOf('function');
expect(smartupdate.RegistryUnavailableError).toBeTypeOf('function');
expect(smartupdate.PackageNotFoundError).toBeTypeOf('function');
expect(smartupdate.LOG_LEVELS).toBeTypeOf('object');
});
export default tap.start();

View File

@@ -1 +1,29 @@
// Main class
export { SmartUpdate } from './smartupdate.classes.smartupdate.js';
// Supporting classes (for advanced use cases)
export { UpdateCacheManager } from './smartupdate.classes.cachemanager.js';
export { UpdateNotifier } from './smartupdate.classes.notifier.js';
// Interfaces and types
export type {
ISmartUpdateOptions,
IUpdateCheckOptions,
IUpdateCheckResult,
ICacheStatus,
ICacheOptions,
INotificationOptions,
} from './smartupdate.interfaces.js';
// Error classes
export {
SmartUpdateError,
RegistryUnavailableError,
PackageNotFoundError,
InvalidVersionError,
InvalidPackageNameError,
} from './smartupdate.errors.js';
// Constants (for reference)
export type { TLogLevel } from './smartupdate.constants.js';
export { LOG_LEVELS, DEFAULT_MESSAGE_COLOR } from './smartupdate.constants.js';

View File

@@ -0,0 +1,116 @@
import * as plugins from './smartupdate.plugins.js';
import type { ICacheStatus, ICacheOptions } from './smartupdate.interfaces.js';
/**
* Manages caching of update check results
*/
export class UpdateCacheManager {
private kvStore: plugins.npmextra.KeyValueStore;
private cacheDurationMs: number;
constructor(options: ICacheOptions) {
this.cacheDurationMs = options.durationMs;
this.kvStore = new plugins.npmextra.KeyValueStore({
typeArg: 'userHomeDir',
identityArg: options.storeIdentifier || 'global_smartupdate',
});
}
/**
* Get the cache duration in milliseconds
*/
public getCacheDuration(): number {
return this.cacheDurationMs;
}
/**
* Get cached status for a package
*/
public async getCached(packageName: string): Promise<ICacheStatus | null> {
return await this.kvStore.readKey(packageName);
}
/**
* Set cache status for a package
*/
public async setCached(packageName: string, status: ICacheStatus): Promise<void> {
await this.kvStore.writeKey(packageName, status);
}
/**
* Clear cache for a specific package or all packages
*/
public async clearCache(packageName?: string): Promise<void> {
if (packageName) {
await this.kvStore.deleteKey(packageName);
} else {
// Clear all keys - this requires reading all keys first
// For now, we'll skip implementing full cache clear as it requires more kvStore API
throw new Error('Clearing all cache entries is not yet implemented');
}
}
/**
* Check if we should check the registry or use cache
* @returns Object with shouldCheck flag and optional nextCheckTime
*/
public async shouldCheckRegistry(
packageName: string,
strategy: 'always' | 'never' | 'time-based' = 'time-based'
): Promise<{
shouldCheck: boolean;
cacheStatus?: ICacheStatus;
nextCheckTime?: Date;
minutesUntilNextCheck?: number;
}> {
// Never use cache
if (strategy === 'never') {
return { shouldCheck: true };
}
// Get cached data
const cacheStatus = await this.getCached(packageName);
// No cache exists
if (!cacheStatus) {
return { shouldCheck: true };
}
// Always use cache if available
if (strategy === 'always') {
return { shouldCheck: false, cacheStatus };
}
// Time-based strategy: check if cache is still valid
const now = Date.now();
const lastCheckTime = cacheStatus.lastCheck;
const timeSinceLastCheck = now - lastCheckTime;
// Cache is still valid
if (timeSinceLastCheck < this.cacheDurationMs) {
const nextCheckTime = new Date(lastCheckTime + this.cacheDurationMs);
const minutesUntilNextCheck = (this.cacheDurationMs - timeSinceLastCheck) / 60000;
return {
shouldCheck: false,
cacheStatus,
nextCheckTime,
minutesUntilNextCheck,
};
}
// Cache is expired
return { shouldCheck: true, cacheStatus };
}
/**
* Create a new cache status object
*/
public createCacheStatus(latestVersion: string, performedUpgrade: boolean = false): ICacheStatus {
return {
lastCheck: Date.now(),
latestVersion,
performedUpgrade,
};
}
}

View File

@@ -0,0 +1,169 @@
import * as plugins from './smartupdate.plugins.js';
import {
LOG_LEVELS,
type TLogLevel,
DEFAULT_MESSAGE_COLOR,
MESSAGE_PREFIXES,
} from './smartupdate.constants.js';
import type { INotificationOptions, IUpdateCheckResult } from './smartupdate.interfaces.js';
/**
* Handles all user-facing notifications and console output
*/
export class UpdateNotifier {
private logLevel: TLogLevel;
private useColors: boolean;
private customLogger?: (level: TLogLevel, message: string) => void;
constructor(options: INotificationOptions) {
this.logLevel = options.logLevel;
this.useColors = options.useColors && !process.env.NO_COLOR;
this.customLogger = options.customLogger;
}
/**
* Check if a message at the given level should be logged
*/
private shouldLog(level: TLogLevel): boolean {
return LOG_LEVELS[level] <= LOG_LEVELS[this.logLevel];
}
/**
* Colorize a string if colors are enabled
*/
private colorize(text: string, color: any = DEFAULT_MESSAGE_COLOR): string {
if (!this.useColors) {
return text;
}
return plugins.consolecolor.coloredString(text, color as any);
}
/**
* Log a message at the specified level
*/
private log(level: TLogLevel, message: string): void {
if (!this.shouldLog(level)) {
return;
}
if (this.customLogger) {
this.customLogger(level, message);
} else {
console.log(message);
}
}
/**
* Log a debug message
*/
public debug(message: string): void {
this.log('DEBUG', `${MESSAGE_PREFIXES.INFO} ${message}`);
}
/**
* Log an info message
*/
public info(message: string): void {
this.log('INFO', `${MESSAGE_PREFIXES.SMARTUPDATE} ${message}`);
}
/**
* Log a warning message
*/
public warn(message: string): void {
this.log('WARN', `${MESSAGE_PREFIXES.WARN} ${message}`);
}
/**
* Log an error message
*/
public error(message: string): void {
this.log('ERROR', `${MESSAGE_PREFIXES.ERROR} ${message}`);
}
/**
* Notify about checking for updates
*/
public notifyCheckingForUpdate(packageName: string): void {
this.info(
`checking for newer version of ${this.colorize(packageName)}...`
);
}
/**
* Notify that the package is up to date
*/
public notifyUpToDate(packageName: string): void {
this.info(
`You are running the latest version of ${this.colorize(packageName)}`
);
}
/**
* Notify that an update is available
*/
public notifyUpdateAvailable(packageName: string, currentVersion: string, latestVersion: string): void {
this.warn(`There is a newer version of ${packageName} available on npm.`);
this.warn(`Your version: ${currentVersion} | version on npm: ${latestVersion}`);
}
/**
* Notify that a check was skipped due to rate limiting
*/
public notifyCheckSkipped(packageName: string, nextCheckMinutes: number): void {
const minutes = Math.floor(nextCheckMinutes) + 1;
this.info(
`Already checked recently. Next check available in ${minutes} minute${minutes !== 1 ? 's' : ''}: ` +
this.colorize(packageName)
);
}
/**
* Notify that the changelog is being opened
*/
public notifyOpeningChangelog(): void {
this.info('Opening changelog in browser...');
}
/**
* Notify about a registry error
*/
public notifyRegistryError(): void {
this.warn('Failed to retrieve package information.');
this.info('Is the registry down?');
}
/**
* Notify with a complete update check result
*/
public notifyUpdateCheckResult(result: IUpdateCheckResult): void {
switch (result.status) {
case 'up-to-date':
this.notifyUpToDate(result.packageName);
break;
case 'update-available':
if (result.latestVersion) {
this.notifyUpdateAvailable(
result.packageName,
result.currentVersion,
result.latestVersion
);
}
break;
case 'check-skipped':
if (result.nextCheckTime && result.reason) {
const minutesUntilNext = (result.nextCheckTime.getTime() - result.checkTime.getTime()) / 60000;
this.notifyCheckSkipped(result.packageName, minutesUntilNext);
}
break;
case 'error':
if (result.error) {
this.error(result.error.message);
}
break;
}
}
}

View File

@@ -1,105 +1,279 @@
import * as plugins from './smartupdate.plugins.js';
import { UpdateCacheManager } from './smartupdate.classes.cachemanager.js';
import { UpdateNotifier } from './smartupdate.classes.notifier.js';
import { DEFAULT_CACHE_DURATION_HOURS, MILLISECONDS_PER_MINUTE, MINUTES_PER_HOUR } from './smartupdate.constants.js';
import type {
ISmartUpdateOptions,
IUpdateCheckOptions,
IUpdateCheckResult,
} from './smartupdate.interfaces.js';
import {
RegistryUnavailableError,
PackageNotFoundError,
} from './smartupdate.errors.js';
interface ICacheStatus {
lastCheck: number;
latestVersion: string;
performedUpgrade: boolean;
}
/**
* SmartUpdate - Elegant update checking for Node.js packages
*
* @example
* ```typescript
* const smartUpdate = new SmartUpdate();
* const result = await smartUpdate.checkForUpdate({
* packageName: 'lodash',
* currentVersion: '1.0.0',
* changelogUrl: 'https://example.com/changelog'
* });
*
* if (result.status === 'update-available') {
* console.log(`Update available: ${result.latestVersion}`);
* }
* ```
*/
export class SmartUpdate {
public npmRegistry: plugins.smartnpm.NpmRegistry;
public kvStore = new plugins.npmextra.KeyValueStore('custom', 'global_smartupdate');
public readonly npmRegistry: plugins.smartnpm.NpmRegistry;
private cacheManager: UpdateCacheManager;
private notifier: UpdateNotifier;
private options: ISmartUpdateOptions;
constructor(optionsArg: plugins.smartnpm.INpmRegistryConstructorOptions = {}) {
this.npmRegistry = new plugins.smartnpm.NpmRegistry(optionsArg);
/**
* @deprecated Use the options parameter instead of kvStore property
*/
public kvStore: plugins.npmextra.KeyValueStore;
constructor(options: ISmartUpdateOptions = {}) {
this.options = options;
// Initialize npm registry
this.npmRegistry = new plugins.smartnpm.NpmRegistry(options.npmRegistryOptions || {});
// Calculate cache duration in milliseconds
const cacheDuration = options.cacheDuration || { hours: DEFAULT_CACHE_DURATION_HOURS };
const cacheDurationMs =
(cacheDuration.hours || 0) * MINUTES_PER_HOUR * MILLISECONDS_PER_MINUTE +
(cacheDuration.minutes || 0) * MILLISECONDS_PER_MINUTE +
(cacheDuration.seconds || 0) * 1000;
// Initialize cache manager
this.cacheManager = new UpdateCacheManager({
durationMs: cacheDurationMs || DEFAULT_CACHE_DURATION_HOURS * MINUTES_PER_HOUR * MILLISECONDS_PER_MINUTE,
});
// Initialize notifier
this.notifier = new UpdateNotifier({
logLevel: options.logLevel || 'INFO',
useColors: !options.noColor,
customLogger: options.customLogger,
});
// Backward compatibility: expose kvStore
this.kvStore = (this.cacheManager as any).kvStore;
}
public async checkForCli(npmnameArg: string, compareVersion: string, changelogUrlArg?: string) {
// the newData to write
const timeStamp = new plugins.smarttime.TimeStamp();
const newCacheData: ICacheStatus = {
lastCheck: timeStamp.milliSeconds,
latestVersion: 'x.x.x',
performedUpgrade: false,
};
/**
* Check for updates with caching (primarily for CLI use)
*
* @deprecated Use checkForUpdate with cacheStrategy: 'always' instead
* @param packageName - The npm package name to check
* @param currentVersion - The current version to compare against
* @param changelogUrl - Optional URL to open if update is available
* @returns Promise resolving to true if update available, false otherwise
*
* @example
* ```typescript
* const hasUpdate = await smartUpdate.checkForCli('my-cli', '1.0.0', 'https://changelog.url');
* ```
*/
public async checkForCli(
packageName: string,
currentVersion: string,
changelogUrl?: string
): Promise<boolean> {
const result = await this.checkForUpdate({
packageName,
currentVersion,
changelogUrl,
cacheStrategy: 'always',
});
// the comparison data from the keyValue store
const retrievedCacheData: ICacheStatus = await this.kvStore.readKey(npmnameArg);
return result.status === 'update-available';
}
if (retrievedCacheData) {
const lastCheckTimeStamp = plugins.smarttime.TimeStamp.fromMilliSeconds(
retrievedCacheData.lastCheck
);
const tresholdTime = plugins.smarttime.getMilliSecondsFromUnits({ hours: 1 });
if (!lastCheckTimeStamp.isOlderThan(timeStamp, tresholdTime)) {
newCacheData.lastCheck = lastCheckTimeStamp.milliSeconds;
const nextCheckInMinutes =
(tresholdTime - (timeStamp.milliSeconds - lastCheckTimeStamp.milliSeconds)) / 60000;
console.log(
`next update check in less than ${Math.floor(nextCheckInMinutes) + 1} minute(s): ` +
`${plugins.consolecolor.coloredString(
`${npmnameArg} has already been checked within the last hour.`,
'pink'
)}`
);
return false; // don't upgrade if checked within reasonable time
/**
* Check for updates (modern API)
*
* @param options - Update check options
* @returns Promise resolving to detailed update check result
*
* @example
* ```typescript
* const result = await smartUpdate.checkForUpdate({
* packageName: 'lodash',
* currentVersion: '1.0.0',
* changelogUrl: 'https://changelog.url',
* openChangelog: true,
* cacheStrategy: 'time-based'
* });
*
* console.log(result.status); // 'up-to-date' | 'update-available' | 'check-skipped' | 'error'
* ```
*/
public async checkForUpdate(options: IUpdateCheckOptions): Promise<IUpdateCheckResult> {
const {
packageName,
currentVersion,
changelogUrl,
openChangelog = true,
cacheStrategy = 'time-based',
} = options;
const checkTime = new Date();
try {
// Check if we should use cache or check registry
const cacheCheck = await this.cacheManager.shouldCheckRegistry(packageName, cacheStrategy);
// If we should skip the check due to cache
if (!cacheCheck.shouldCheck) {
return {
status: 'check-skipped',
packageName,
currentVersion,
latestVersion: cacheCheck.cacheStatus?.latestVersion,
checkTime,
cacheHit: true,
nextCheckTime: cacheCheck.nextCheckTime,
reason: 'Rate limited - checked recently',
};
}
}
const upgradeBool = await this.check(npmnameArg, compareVersion, changelogUrlArg);
if (upgradeBool) {
const npmPackage = await this.npmRegistry.getPackageInfo(npmnameArg);
newCacheData.latestVersion = npmPackage.version;
this.kvStore.writeKey(npmnameArg, newCacheData);
}
// Fetch package info from registry
const npmPackage = await this.getNpmPackageFromRegistry(packageName);
return upgradeBool;
}
private async getNpmPackageFromRegistry(npmnameArg): Promise<plugins.smartnpm.NpmPackage> {
console.log(
`smartupdate: checking for newer version of ${plugins.consolecolor.coloredString(
npmnameArg,
'pink'
)}...`
);
const npmPackage = this.npmRegistry.getPackageInfo(npmnameArg);
return npmPackage;
if (!npmPackage) {
throw new RegistryUnavailableError();
}
// Compare versions
const versionNpm = new plugins.smartversion.SmartVersion(npmPackage.version);
const versionLocal = new plugins.smartversion.SmartVersion(currentVersion);
const result: IUpdateCheckResult = {
status: versionNpm.greaterThan(versionLocal) ? 'update-available' : 'up-to-date',
packageName: npmPackage.name,
currentVersion,
latestVersion: npmPackage.version,
checkTime,
cacheHit: false,
};
// Notify user
this.notifier.notifyUpdateCheckResult(result);
// If update is available, handle changelog
if (result.status === 'update-available' && changelogUrl && openChangelog && !process.env.CI) {
this.notifier.notifyOpeningChangelog();
plugins.smartopen.openUrl(changelogUrl);
}
// Update cache if there's an update
if (result.status === 'update-available') {
const cacheStatus = this.cacheManager.createCacheStatus(npmPackage.version, false);
await this.cacheManager.setCached(packageName, cacheStatus);
}
return result;
} catch (error) {
if (error instanceof RegistryUnavailableError) {
this.notifier.notifyRegistryError();
} else {
this.notifier.error(error instanceof Error ? error.message : String(error));
}
return {
status: 'error',
packageName,
currentVersion,
checkTime,
cacheHit: false,
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
/**
* Simple check for updates without caching
*
* @deprecated Use checkForUpdate with cacheStrategy: 'never' instead
* @param packageName - The npm package name to check
* @param currentVersion - The current version to compare against
* @param changelogUrl - Optional URL to open if update is available
* @returns Promise resolving to true if update available, false otherwise
*
* @example
* ```typescript
* const hasUpdate = await smartUpdate.check('lodash', '1.0.0');
* ```
*/
public async check(
npmPackageName: string,
localVersionStringArg: string,
changelogUrlArg?: string
) {
const npmPackage = await this.getNpmPackageFromRegistry(npmPackageName);
if (!npmPackage) {
console.log('warn: failed to retrieve package information...');
console.log('info: is the registry down?');
return;
}
packageName: string,
currentVersion: string,
changelogUrl?: string
): Promise<boolean> {
const result = await this.checkForUpdate({
packageName,
currentVersion,
changelogUrl,
cacheStrategy: 'never',
});
// create Version objects
const versionNpm = new plugins.smartversion.SmartVersion(npmPackage.version);
const versionLocal = new plugins.smartversion.SmartVersion(localVersionStringArg);
if (!versionNpm.greaterThan(versionLocal)) {
console.log(
`smartupdate: You are running the latest version of ${plugins.consolecolor.coloredString(
npmPackage.name,
'pink'
)}`
);
return false;
} else {
console.log(`warn: There is a newer version of ${npmPackage.name} available on npm.`);
console.log(
`warn: Your version: ${versionLocal.versionString} | version on npm: ${versionNpm.versionString}`
);
if (!process.env.CI && changelogUrlArg) {
console.log('trying to open changelog...');
plugins.smartopen.openUrl(changelogUrlArg);
}
return true;
return result.status === 'update-available';
}
/**
* Get the latest version of a package
*
* @param packageName - The npm package name
* @returns Promise resolving to the latest version string
*
* @example
* ```typescript
* const latestVersion = await smartUpdate.getLatestVersion('lodash');
* console.log(latestVersion); // e.g., "4.17.21"
* ```
*/
public async getLatestVersion(packageName: string): Promise<string> {
const npmPackage = await this.getNpmPackageFromRegistry(packageName);
if (!npmPackage) {
throw new PackageNotFoundError(packageName);
}
return npmPackage.version;
}
/**
* Clear the cache for a specific package
*
* @param packageName - The package name to clear cache for
*
* @example
* ```typescript
* await smartUpdate.clearCache('lodash');
* ```
*/
public async clearCache(packageName: string): Promise<void> {
await this.cacheManager.clearCache(packageName);
}
/**
* Fetch package information from the npm registry
* @private
*/
private async getNpmPackageFromRegistry(packageName: string): Promise<plugins.smartnpm.NpmPackage | null> {
this.notifier.notifyCheckingForUpdate(packageName);
try {
const npmPackage = await this.npmRegistry.getPackageInfo(packageName);
return npmPackage;
} catch (error) {
return null;
}
}
}

View File

@@ -0,0 +1,30 @@
/**
* Constants used throughout the smartupdate library
*/
// Time constants
export const MILLISECONDS_PER_MINUTE = 60_000;
export const MINUTES_PER_HOUR = 60;
export const DEFAULT_CACHE_DURATION_HOURS = 1;
// Console output constants
export const DEFAULT_MESSAGE_COLOR = 'pink';
// Log level constants
export const LOG_LEVELS = {
SILENT: 0,
ERROR: 1,
WARN: 2,
INFO: 3,
DEBUG: 4,
} as const;
export type TLogLevel = keyof typeof LOG_LEVELS;
// Message prefixes
export const MESSAGE_PREFIXES = {
ERROR: 'error:',
WARN: 'warn:',
INFO: 'info:',
SMARTUPDATE: 'smartupdate:',
} as const;

65
ts/smartupdate.errors.ts Normal file
View File

@@ -0,0 +1,65 @@
/**
* Base error class for smartupdate errors
*/
export class SmartUpdateError extends Error {
constructor(message: string) {
super(message);
this.name = 'SmartUpdateError';
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
/**
* Error thrown when the npm registry is unavailable
*/
export class RegistryUnavailableError extends SmartUpdateError {
constructor(message: string = 'Failed to retrieve package information from npm registry') {
super(message);
this.name = 'RegistryUnavailableError';
}
}
/**
* Error thrown when a package is not found in the registry
*/
export class PackageNotFoundError extends SmartUpdateError {
public readonly packageName: string;
constructor(packageName: string) {
super(`Package '${packageName}' not found in npm registry`);
this.name = 'PackageNotFoundError';
this.packageName = packageName;
}
}
/**
* Error thrown when a version string is invalid
*/
export class InvalidVersionError extends SmartUpdateError {
public readonly version: string;
constructor(version: string, reason?: string) {
const message = reason
? `Invalid version string '${version}': ${reason}`
: `Invalid version string '${version}'`;
super(message);
this.name = 'InvalidVersionError';
this.version = version;
}
}
/**
* Error thrown when package name validation fails
*/
export class InvalidPackageNameError extends SmartUpdateError {
public readonly packageName: string;
constructor(packageName: string) {
super(`Invalid package name '${packageName}'. Package names must follow npm naming conventions.`);
this.name = 'InvalidPackageNameError';
this.packageName = packageName;
}
}

View File

@@ -0,0 +1,171 @@
import type { TLogLevel } from './smartupdate.constants.js';
import type * as smartnpm from '@push.rocks/smartnpm';
/**
* Cache status stored for each package
*/
export interface ICacheStatus {
lastCheck: number;
latestVersion: string;
performedUpgrade: boolean;
}
/**
* Options for configuring the SmartUpdate instance
*/
export interface ISmartUpdateOptions {
/**
* Options for the npm registry connection
*/
npmRegistryOptions?: smartnpm.INpmRegistryConstructorOptions;
/**
* Cache duration configuration
* @default { hours: 1 }
*/
cacheDuration?: {
hours?: number;
minutes?: number;
seconds?: number;
};
/**
* Logging level
* @default 'INFO'
*/
logLevel?: TLogLevel;
/**
* Custom logger function
* If provided, this will be used instead of console output
*/
customLogger?: (level: TLogLevel, message: string) => void;
/**
* Disable colored output
* @default false
*/
noColor?: boolean;
}
/**
* Options for checking for updates
*/
export interface IUpdateCheckOptions {
/**
* The npm package name to check
*/
packageName: string;
/**
* The current version to compare against
*/
currentVersion: string;
/**
* Optional URL to the changelog
* If provided and an update is available, the changelog will be opened
*/
changelogUrl?: string;
/**
* Whether to open the changelog URL automatically
* Only applies if running in a non-CI environment
* @default true
*/
openChangelog?: boolean;
/**
* Cache strategy for this check
* - 'always': Always check cache first (default for CLI)
* - 'never': Always check registry, bypass cache
* - 'time-based': Check based on cache duration
* @default 'time-based'
*/
cacheStrategy?: 'always' | 'never' | 'time-based';
}
/**
* Result of an update check
*/
export interface IUpdateCheckResult {
/**
* Status of the update check
*/
status: 'up-to-date' | 'update-available' | 'check-skipped' | 'error';
/**
* The current version being checked
*/
currentVersion: string;
/**
* The latest version available (if found)
*/
latestVersion?: string;
/**
* The package name that was checked
*/
packageName: string;
/**
* Time when the check was performed
*/
checkTime: Date;
/**
* Whether this result came from cache
*/
cacheHit: boolean;
/**
* When the next check can be performed (if check was skipped due to rate limiting)
*/
nextCheckTime?: Date;
/**
* Error details if status is 'error'
*/
error?: Error;
/**
* Reason for the result (human-readable explanation)
*/
reason?: string;
}
/**
* Options for the cache manager
*/
export interface ICacheOptions {
/**
* Cache duration in milliseconds
*/
durationMs: number;
/**
* Identifier for the key-value store
*/
storeIdentifier?: string;
}
/**
* Options for the notifier
*/
export interface INotificationOptions {
/**
* Log level for notifications
*/
logLevel: TLogLevel;
/**
* Whether to use colors in output
*/
useColors: boolean;
/**
* Custom logger function
*/
customLogger?: (level: TLogLevel, message: string) => void;
}