diff --git a/src/cli/main.ts b/src/cli/main.ts new file mode 100644 index 0000000..db8e03e --- /dev/null +++ b/src/cli/main.ts @@ -0,0 +1,20 @@ +import yargs from "yargs"; +import { standalone } from "../../config.json"; +import { SelfUpdateUtility } from "./update"; + +if (standalone) SelfUpdateUtility.init(); + +yargs(process.argv.slice(2)).command( + "update", + "update", + { + url: { + alias: "u", + describe: "the URL to download the new executable from", + }, + }, + async (argv) => { + if (typeof argv.url === "string") + await SelfUpdateUtility.get().updateCLI(argv.url); + }, +); diff --git a/src/cli/update.ts b/src/cli/update.ts index c25f4eb..f17b788 100644 --- a/src/cli/update.ts +++ b/src/cli/update.ts @@ -1,30 +1,90 @@ -#!/usr/bin/env ts-node - -import { renameSync, unlinkSync, createWriteStream } from "fs"; +import { renameSync, unlinkSync, createWriteStream, existsSync } from "fs"; import child_process = require("child_process"); import { get as httpGet, type IncomingMessage } from "http"; -import { standalone } from "../config.json"; +import { standalone } from "../../config.json"; /** * self updates the cli executable * @warning DO NOT USE THIS IF NOT PART OF STANDALONE EXECUTABLE */ -export async function updateCLI(downloadUrl: string): Promise { - if (!standalone) - throw new Error( - "LemLink is not part of a standalone executable, and thus cannot self update", - ); +export class SelfUpdateUtility { + /** + * So how the heck does this CLI update itself? + * + * 1. Attempt to download new executable from downloadUrl to newPath + * 2. Rename current executable to oldPath + * 3. Rename newly downloaded executable to filePath + * 4. On startup of the cli, we attempt to remove oldPath + */ + + protected readonly filePath: string; // normal executable path + protected readonly newPath: string; // the new executable that will be downloaded + protected readonly oldPath: string; + + private static singleton?: SelfUpdateUtility; + /** + * self updates the cli executable + * @warning DO NOT USE THIS IF NOT PART OF STANDALONE EXECUTABLE + */ + private constructor() { + if (!standalone) + throw new Error( + "LemLink is not part of a standalone executable, and thus cannot self update", + ); + const nodeExecutablePath = process.env._; + if (nodeExecutablePath === undefined) { + throw new Error("Could not find node executable path"); + } + + this.filePath = nodeExecutablePath; + this.newPath = this.filePath + "-new"; + this.oldPath = this.filePath + "-old"; + + this.attemptToDeleteOldPathFile(); + } + + /** + * Should be run at the start of CLI program + */ + public static init(): void { + if (this.singleton !== undefined) { + console.error( + "Self Update Utility has already been initialized, canceling init..", + ); + return; + } + this.singleton = new SelfUpdateUtility(); + } + + public static get(): SelfUpdateUtility { + if (this.singleton === undefined) + throw new Error( + "Self Update Utility was not initialized at the start of CLI program!!", + ); + return this.singleton; + } + + /** + * @returns true if oldPath exists, false if otherwise + */ + private attemptToDeleteOldPathFile(): boolean { + if (existsSync(this.oldPath)) { + unlinkSync(this.oldPath); + return true; + } + return false; + } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const filePath = process.env._!; - const newPath = filePath + "-new"; - const oldPath = filePath + "-old"; - try { - console.log("attempting to delete old file"); - unlinkSync(oldPath); - } catch (err) { + /** + * Replaces the current CLI executable with the one found at {@link downloadUrl} + * @param downloadUrl url from which a new executable should be downloaded + */ + public async updateCLI(downloadUrl: string): Promise { + /** + * See top of this file for info on how this works + */ console.log("Downloading new file..."); - const fileStream = createWriteStream(newPath); + const fileStream = createWriteStream(this.newPath); const response = await new Promise((resolve) => httpGet(downloadUrl, resolve), @@ -37,8 +97,8 @@ export async function updateCLI(downloadUrl: string): Promise { console.log("File downloaded successfully."); console.log("Replacing current file..."); - renameSync(filePath, oldPath); - renameSync(newPath, filePath); + renameSync(this.filePath, this.oldPath); + renameSync(this.newPath, this.filePath); console.log("Replaced current file successfully!"); console.log("Restarting...");