From e138f67969d3adc1518d701f7115af1caed96e0a Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Fri, 8 Apr 2022 20:35:57 +0100 Subject: [PATCH] Add update check --- src/cli.ts | 3 + src/cli/app.ts | 3 + src/cli/update.ts | 149 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/cli/update.ts diff --git a/src/cli.ts b/src/cli.ts index 09d8044..1978cf1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,6 +5,7 @@ import dotenv from 'dotenv'; import dotenvExpand from 'dotenv-expand'; import { dev, paths, YargsArguments } from './util.js'; import * as commands from './cli/index.js'; +import { checkUpdates } from './cli/update.js'; const debug = createDebug('cli'); @@ -42,5 +43,7 @@ yargs export default yargs; export async function main() { + if (!process.env.NXAPI_SKIP_UPDATE_CHECK) await checkUpdates(); + yargs.argv; } diff --git a/src/cli/app.ts b/src/cli/app.ts index 975b926..fc34ddb 100644 --- a/src/cli/app.ts +++ b/src/cli/app.ts @@ -27,5 +27,8 @@ export async function handler(argv: ArgumentsCamelCase) { 'dist/app/main/app-entry.cjs', ], { stdio: 'inherit', + env: { + NXAPI_SKIP_UPDATE_CHECK: '1', + }, }); } diff --git a/src/cli/update.ts b/src/cli/update.ts new file mode 100644 index 0000000..e4d7853 --- /dev/null +++ b/src/cli/update.ts @@ -0,0 +1,149 @@ +import * as path from 'path'; +import * as fs from 'fs/promises'; +import fetch from 'node-fetch'; +import createDebug from 'debug'; +import mkdirp from 'mkdirp'; +import { paths, version } from '../util.js'; + +const debug = createDebug('cli:update'); + +const RELEASES_URL = 'https://api.github.com/repos/samuelthomas2774/nxapi/releases'; + +export async function checkUpdates() { + const dir = path.resolve(import.meta.url.substr(7), '..', '..', '..'); + + try { + await fs.stat(path.join(dir, '.git')); + + debug('git repository exists, skipping update check'); + return null; + } catch (err) {} + + await mkdirp(paths.cache); + const update_cache_path = path.resolve(paths.cache, 'update.json'); + + try { + const data: UpdateCacheData = JSON.parse(await fs.readFile(update_cache_path, 'utf-8')); + + if (data && data.expires_at > Date.now()) { + if (data.current_version !== version) return data; + + if ('update_available' in data && data.update_available) { + console.warn('[nxapi] Update available - current version %s, latest %s', + data.current_version, data.latest_version); + } + + return data; + } + } catch (err) {} + + debug('Checking for updates'); + + try { + const response = await fetch(RELEASES_URL); + const releases = await response.json() as Release[]; + + const current = releases.find(r => r.tag_name === 'v' + version); + const latest = releases.find(r => !r.prerelease || current?.prerelease) ?? releases[0]; + const latest_version = latest.tag_name.replace(/^v/, ''); + + const data: UpdateCacheDataSuccess = { + created_at: Date.now(), + expires_at: Date.now() + 86400000, // 24 hours + releases, + releases_url: RELEASES_URL, + current, + current_version: version, + latest, + latest_version, + update_available: version !== latest_version, + }; + + await fs.writeFile(update_cache_path, JSON.stringify(data, null, 4) + '\n'); + + if (data.update_available) { + console.warn('[nxapi] Update available - current version %s, latest %s', version, latest_version); + } else { + debug('Using latest %s version %s', latest.prerelease ? 'prerelease' : 'stable', latest_version); + } + + debug('Next update check at %s', new Date(data.expires_at)); + + return data; + } catch (err) { + console.warn('[nxapi] Update check failed', err); + + const data: UpdateCacheDataFailed = { + created_at: Date.now(), + expires_at: Date.now() + 1800000, // 30 minutes + current_version: version, + error_message: (err as Error).message, + }; + + await fs.writeFile(update_cache_path, JSON.stringify(data, null, 4) + '\n'); + + return data; + } +} + +export type UpdateCacheData = UpdateCacheDataSuccess | UpdateCacheDataFailed; + +export interface UpdateCacheDataSuccess { + created_at: number; + expires_at: number; + releases: Release[]; + releases_url: string; + current: Release | undefined; + current_version: string; + latest: Release; + latest_version: string; + update_available: boolean; +} + +export interface UpdateCacheDataFailed { + created_at: number; + expires_at: number; + current_version: string; + error_message: string; +} + +interface Release { + url: string; + assets_url: string; + upload_url: string; + html_url: string; + id: number; + author: ReleaseAuthor; + node_id: string; + tag_name: string; + target_commitish: string; + name: string; + draft: boolean; + prerelease: boolean; + created_at: string; + published_at: string; + assets: unknown[]; + tarball_url: string; + zipball_url: string; + body: string; +} +interface ReleaseAuthor { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: 'User'; + site_admin: boolean; +}