@@ -7,6 +7,7 @@ import * as path from 'path';
77import * as semver from 'semver' ;
88import { OutputHelper } from '@quenty/cli-output-helpers' ;
99import { readFile , writeFile } from 'fs/promises' ;
10+ import { Memoize } from 'typescript-memoize' ;
1011
1112const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 ; // 24 hours
1213
@@ -32,137 +33,155 @@ interface VersionCheckerOptions {
3233 verbose ?: boolean ;
3334}
3435
35- export async function checkForUpdatesAsync (
36- options : VersionCheckerOptions
37- ) : Promise < void > {
38- try {
39- await checkForUpdatesInternalAsync ( options ) ;
40- } catch ( error ) {
41- const name = options . humanReadableName || options . packageName ;
42- OutputHelper . box ( `Failed to check for updates for ${ name } due to ${ error } ` ) ;
36+ export class VersionChecker {
37+ public static async checkForUpdatesAsync (
38+ options : VersionCheckerOptions
39+ ) : Promise < UpdateCheckResult | undefined > {
40+ try {
41+ return await VersionChecker . _checkForUpdatesInternalAsync ( options ) ;
42+ } catch ( error ) {
43+ const name = options . humanReadableName || options . packageName ;
44+ OutputHelper . box (
45+ `Failed to check for updates for ${ name } due to ${ error } `
46+ ) ;
47+ }
48+
49+ return undefined ;
4350 }
44- }
4551
46- async function checkForUpdatesInternalAsync (
47- options : VersionCheckerOptions
48- ) : Promise < void > {
49- const {
50- packageName,
51- registryUrl,
52- currentVersion,
53- packageJsonPath,
54- updateCommand = `npm install -g ${ packageName } @latest` ,
55- } = options ;
56-
57- const version = await queryOurVersionAsync ( currentVersion , packageJsonPath ) ;
58- if ( ! version ) {
52+ private static async _checkForUpdatesInternalAsync (
53+ options : VersionCheckerOptions
54+ ) : Promise < UpdateCheckResult | undefined > {
55+ const {
56+ packageName,
57+ registryUrl,
58+ currentVersion,
59+ packageJsonPath,
60+ updateCommand = `npm install -g ${ packageName } @latest` ,
61+ } = options ;
62+
63+ const version = await VersionChecker . _queryOurVersionAsync (
64+ currentVersion ,
65+ packageJsonPath
66+ ) ;
67+ if ( ! version ) {
68+ if ( options . verbose ) {
69+ OutputHelper . error (
70+ `Could not determine current version for ${ packageName } , skipping update check.`
71+ ) ;
72+ }
73+ return undefined ;
74+ }
75+
76+ const result = await VersionChecker . _queryUpdateStateAsync (
77+ packageName ,
78+ version ,
79+ registryUrl
80+ ) ;
81+
5982 if ( options . verbose ) {
60- OutputHelper . error (
61- `Could not determine current version for ${ packageName } , skipping update check. `
83+ OutputHelper . info (
84+ `Checked for updates for ${ packageName } . Current version: ${ result . currentVersion } , Latest version: ${ result . latestVersion } , and update available: ${ result . updateAvailable } `
6285 ) ;
6386 }
64- return ;
65- }
6687
67- const result = await queryUpdateStateAsync ( packageName , version , registryUrl ) ;
88+ if ( result . updateAvailable ) {
89+ const name = options . humanReadableName || packageName ;
90+ const text = [
91+ `${ name } update available: ${ result . currentVersion } → ${ result . latestVersion } ` ,
92+ '' ,
93+ OutputHelper . formatHint ( `Run '${ updateCommand } ' to update` ) ,
94+ ] . join ( '\n' ) ;
6895
69- if ( options . verbose ) {
70- OutputHelper . info (
71- `Checked for updates for ${ packageName } . Current version: ${ result . currentVersion } , Latest version: ${ result . latestVersion } , and update available: ${ result . updateAvailable } `
72- ) ;
96+ OutputHelper . box ( text , { centered : true } ) ;
97+ }
98+
99+ return result ;
73100 }
74101
75- if ( result . updateAvailable ) {
76- const name = options . humanReadableName || packageName ;
77- const text = [
78- `${ name } update available: ${ result . currentVersion } → ${ result . latestVersion } ` ,
79- '' ,
80- OutputHelper . formatHint ( `Run '${ updateCommand } ' to update` ) ,
81- ] . join ( '\n' ) ;
102+ @Memoize ( )
103+ private static async _queryOurVersionAsync (
104+ currentVersion : string | undefined ,
105+ packageJsonPath : string | undefined
106+ ) : Promise < string | null > {
107+ if ( currentVersion ) {
108+ return currentVersion ;
109+ }
82110
83- OutputHelper . box ( text , { centered : true } ) ;
84- }
85- }
111+ if ( ! packageJsonPath ) {
112+ throw new Error (
113+ 'Either currentVersion or packageJsonPath must be provided to determine the current version.'
114+ ) ;
115+ }
86116
87- async function queryOurVersionAsync (
88- currentVersion : string | undefined ,
89- packageJsonPath : string | undefined
90- ) : Promise < string | null > {
91- if ( currentVersion ) {
92- return currentVersion ;
117+ const pkg = JSON . parse ( await readFile ( packageJsonPath , 'utf8' ) ) ;
118+ return pkg . version || null ;
93119 }
94120
95- if ( ! packageJsonPath ) {
96- throw new Error (
97- 'Either currentVersion or packageJsonPath must be provided to determine the current version.'
98- ) ;
99- }
121+ private static async _queryUpdateStateAsync (
122+ packageName : string ,
123+ currentVersion : string ,
124+ registryUrl : string
125+ ) : Promise < UpdateCheckResult > {
126+ // Use a simple cache file in the user's home directory
127+ const cacheKey = `${ packageName
128+ . replace ( '/' , '-' )
129+ . replace ( '@' , '' ) } -version`;
130+ const cacheFile = path . join ( os . homedir ( ) , '.nevermore-version-cache' ) ;
131+
132+ // Try to read cached data
133+ let cachedData : VersionCache | undefined ;
134+ let loadedCacheData ;
135+ try {
136+ const cacheContent = await readFile ( cacheFile , 'utf-8' ) ;
137+ loadedCacheData = JSON . parse ( cacheContent ) ;
138+ cachedData = loadedCacheData [ cacheKey ] as VersionCache | undefined ;
139+ } catch ( error ) {
140+ // Cache file doesn't exist or is invalid, will check for updates
141+ }
100142
101- const pkg = JSON . parse ( await readFile ( packageJsonPath , 'utf8' ) ) ;
102- return pkg . version || null ;
103- }
143+ // If we checked recently, skip
144+ const now = Date . now ( ) ;
145+ if (
146+ cachedData &&
147+ ( now - cachedData . lastCheck < CHECK_INTERVAL_MS ||
148+ cachedData . currentVersion !== currentVersion )
149+ ) {
150+ return {
151+ updateAvailable : semver . gt ( cachedData . latestVersion , currentVersion ) ,
152+ currentVersion : currentVersion ,
153+ latestVersion : cachedData . latestVersion ,
154+ } ;
155+ }
104156
105- async function queryUpdateStateAsync (
106- packageName : string ,
107- currentVersion : string ,
108- registryUrl : string
109- ) : Promise < UpdateCheckResult > {
110- // Use a simple cache file in the user's home directory
111- const cacheKey = `${ packageName . replace ( '/' , '-' ) . replace ( '@' , '' ) } -version` ;
112- const cacheFile = path . join ( os . homedir ( ) , '.nevermore-version-cache' ) ;
113-
114- // Try to read cached data
115- let cachedData : VersionCache | undefined ;
116- let loadedCacheData ;
117- try {
118- const cacheContent = await readFile ( cacheFile , 'utf-8' ) ;
119- loadedCacheData = JSON . parse ( cacheContent ) ;
120- cachedData = loadedCacheData [ cacheKey ] as VersionCache | undefined ;
121- } catch ( error ) {
122- // Cache file doesn't exist or is invalid, will check for updates
123- }
157+ const { default : latestVersion } = await import ( 'latest-version' ) ;
124158
125- // If we checked recently, skip
126- const now = Date . now ( ) ;
127- if (
128- cachedData &&
129- ( now - cachedData . lastCheck < CHECK_INTERVAL_MS ||
130- cachedData . currentVersion !== currentVersion )
131- ) {
132- return {
133- updateAvailable : semver . gt ( cachedData . latestVersion , currentVersion ) ,
159+ // Check for new version
160+ const latestVersionString = await latestVersion ( packageName , {
161+ registryUrl : registryUrl ,
162+ } ) ;
163+
164+ // Save to cache
165+ const newCache : VersionCache = {
166+ lastCheck : now ,
167+ latestVersion : latestVersionString ,
134168 currentVersion : currentVersion ,
135- latestVersion : cachedData . latestVersion ,
136169 } ;
137- }
170+ const newResults = loadedCacheData || { } ;
171+ newResults [ cacheKey ] = newCache ;
172+
173+ try {
174+ await writeFile ( cacheFile , JSON . stringify ( newResults , null , 2 ) , 'utf-8' ) ;
175+ } catch ( error ) {
176+ // Ignore cache write errors, update check still worked
177+ OutputHelper . warn ( `Failed to write cache file: ${ error } ` ) ;
178+ }
138179
139- const { default : latestVersion } = await import ( 'latest-version' ) ;
140-
141- // Check for new version
142- const latestVersionString = await latestVersion ( packageName , {
143- registryUrl : registryUrl ,
144- } ) ;
145-
146- // Save to cache
147- const newCache : VersionCache = {
148- lastCheck : now ,
149- latestVersion : latestVersionString ,
150- currentVersion : currentVersion ,
151- } ;
152- const newResults = loadedCacheData || { } ;
153- newResults [ cacheKey ] = newCache ;
154-
155- try {
156- await writeFile ( cacheFile , JSON . stringify ( newResults , null , 2 ) , 'utf-8' ) ;
157- } catch ( error ) {
158- // Ignore cache write errors, update check still worked
159- OutputHelper . warn ( `Failed to write cache file: ${ error } ` ) ;
180+ // Return whether update is available
181+ return {
182+ updateAvailable : semver . gt ( latestVersionString , currentVersion ) ,
183+ currentVersion : currentVersion ,
184+ latestVersion : latestVersionString ,
185+ } ;
160186 }
161-
162- // Return whether update is available
163- return {
164- updateAvailable : semver . gt ( latestVersionString , currentVersion ) ,
165- currentVersion : currentVersion ,
166- latestVersion : latestVersionString ,
167- } ;
168187}
0 commit comments