mirror of
https://github.com/ivabus/gui
synced 2025-04-23 14:07:14 +03:00
copy tea/cli install strategy from tea/cli repo (#561)
- new tea cli install setup - reinstall if uninstalled/rm by user - initialize state can be cancelled --------- Co-authored-by: Max Howell <mxcl@me.com> Co-authored-by: neil molina <neil@neils-MacBook-Pro.local>
This commit is contained in:
parent
2287982310
commit
b7e45f0768
4 changed files with 153 additions and 68 deletions
|
@ -1,16 +1,14 @@
|
|||
import { spawn, exec } from "child_process";
|
||||
import { getGuiPath } from "./tea-dir";
|
||||
import { getGuiPath, getTeaPath } from "./tea-dir";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import initializeTeaCli from "./initialize";
|
||||
import { initializeTeaCli } from "./initialize";
|
||||
|
||||
import { app } from "electron";
|
||||
import log from "./logger";
|
||||
import { MainWindowNotifier } from "./types";
|
||||
|
||||
const destinationDirectory = getGuiPath();
|
||||
|
||||
export const cliBinPath = path.join(destinationDirectory, "tea");
|
||||
export const cliBinPath = path.join(getTeaPath(), "tea.xyz/v*/bin/tea");
|
||||
|
||||
export async function installPackage(
|
||||
full_name: string,
|
||||
|
@ -34,7 +32,7 @@ export async function installPackage(
|
|||
const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } };
|
||||
|
||||
const child = spawn(
|
||||
`${destinationDirectory}/tea`,
|
||||
cliBinPath,
|
||||
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
|
||||
opts
|
||||
);
|
||||
|
@ -200,6 +198,6 @@ export async function syncPantry() {
|
|||
const teaVersion = await initializeTeaCli();
|
||||
|
||||
if (!teaVersion) throw new Error("no tea");
|
||||
log.info("Syncing pantry");
|
||||
await asyncExec(`cd '${destinationDirectory}' && ./tea -S`);
|
||||
log.info("Syncing pantry", teaVersion);
|
||||
await asyncExec(`DEBUG=1 "${cliBinPath}" --sync --env=false`);
|
||||
}
|
||||
|
|
|
@ -1,78 +1,162 @@
|
|||
import fs from "fs";
|
||||
import { getGuiPath } from "./tea-dir";
|
||||
import { getGuiPath, getTeaPath } from "./tea-dir";
|
||||
import log from "./logger";
|
||||
import { cliBinPath, asyncExec } from "./cli";
|
||||
// import { cliBinPath, asyncExec } from "./cli";
|
||||
import { createInitialSessionFile } from "./auth";
|
||||
import { SemVer, isValidSemVer } from "@tea/libtea";
|
||||
import * as https from "https";
|
||||
import { spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { parse as semverParse } from "@tea/libtea";
|
||||
|
||||
const MINIMUM_TEA_VERSION = "0.31.2";
|
||||
type InitState = "NOT_INITIALIZED" | "PENDING" | "INITIALIZED";
|
||||
|
||||
const destinationDirectory = getGuiPath();
|
||||
class InitWatcher<T> {
|
||||
private initState: InitState;
|
||||
private initFunction: () => Promise<T>;
|
||||
private initialValue: T | undefined;
|
||||
private initializationPromise: Promise<T> | undefined;
|
||||
|
||||
// TODO: move device_id generation here
|
||||
|
||||
// Get the binary path from the current app directory
|
||||
const binaryUrl = "https://tea.xyz/$(uname)/$(uname -m)";
|
||||
|
||||
let initializePromise: Promise<string> | null = null;
|
||||
|
||||
export async function initializeTeaCli(): Promise<string> {
|
||||
if (initializePromise) {
|
||||
return initializePromise;
|
||||
constructor(initFunction: () => Promise<T>) {
|
||||
this.initState = "NOT_INITIALIZED";
|
||||
this.initFunction = initFunction;
|
||||
this.initialValue = undefined;
|
||||
this.initializationPromise = undefined;
|
||||
}
|
||||
|
||||
log.info("Initializing tea cli");
|
||||
initializePromise = initializeTeaCliInternal();
|
||||
async initialize(): Promise<T> {
|
||||
if (this.initState === "NOT_INITIALIZED") {
|
||||
this.initState = "PENDING";
|
||||
this.initializationPromise = this.retryFunction(this.initFunction, 3)
|
||||
.then((value) => {
|
||||
this.initialValue = value;
|
||||
this.initState = "INITIALIZED";
|
||||
return value;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.initState = "NOT_INITIALIZED";
|
||||
this.initializationPromise = undefined;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
initializePromise.catch((error) => {
|
||||
log.info("Error initializing tea cli, resetting promise:", error);
|
||||
initializePromise = null;
|
||||
});
|
||||
|
||||
return initializePromise;
|
||||
}
|
||||
|
||||
async function initializeTeaCliInternal(): Promise<string> {
|
||||
let binCheck = "";
|
||||
let needsUpdate = false;
|
||||
|
||||
// Create the destination directory if it doesn't exist
|
||||
if (!fs.existsSync(destinationDirectory)) {
|
||||
fs.mkdirSync(destinationDirectory, { recursive: true });
|
||||
return this.initializationPromise as Promise<T>;
|
||||
}
|
||||
|
||||
// replace this with max's pr
|
||||
const curlCommand = `curl --insecure -L -o "${cliBinPath}" "${binaryUrl}"`;
|
||||
|
||||
const exists = fs.existsSync(cliBinPath);
|
||||
if (exists) {
|
||||
log.info("binary tea already exists at", cliBinPath);
|
||||
async retryFunction(func: () => Promise<T>, retries: number, currentAttempt = 1): Promise<T> {
|
||||
try {
|
||||
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
||||
const teaVersion = binCheck.toString().split(" ")[1].trim();
|
||||
if (new SemVer(teaVersion).compare(new SemVer(MINIMUM_TEA_VERSION)) < 0) {
|
||||
log.info("binary tea version is too old, updating");
|
||||
needsUpdate = true;
|
||||
}
|
||||
const result = await func();
|
||||
return result;
|
||||
} catch (error) {
|
||||
// probably binary is not executable or no permission
|
||||
log.error("Error checking tea binary version:", error);
|
||||
needsUpdate = true;
|
||||
await asyncExec(`cd ${destinationDirectory} && rm tea`);
|
||||
if (currentAttempt < retries) {
|
||||
return this.retryFunction(func, retries, currentAttempt + 1);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists || needsUpdate) {
|
||||
await asyncExec(curlCommand);
|
||||
log.info("Binary downloaded and saved to", cliBinPath);
|
||||
await asyncExec("chmod u+x " + cliBinPath);
|
||||
log.info("Binary is now ready for use at", cliBinPath);
|
||||
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
||||
reset(): void {
|
||||
this.initState = "NOT_INITIALIZED";
|
||||
this.initializationPromise = undefined;
|
||||
}
|
||||
|
||||
const version = binCheck.toString().split(" ")[1];
|
||||
log.info("binary tea version:", version);
|
||||
return isValidSemVer(version.trim()) ? version : "";
|
||||
async observe(): Promise<T> {
|
||||
return await this.initialize();
|
||||
}
|
||||
|
||||
getState(): InitState {
|
||||
return this.initState;
|
||||
}
|
||||
}
|
||||
|
||||
const teaCliPrefix = path.join(getTeaPath(), "tea.xyz/v*");
|
||||
|
||||
export const cliInitializationState = new InitWatcher<string>(async () => {
|
||||
if (!fs.existsSync(path.join(teaCliPrefix, "bin/tea"))) {
|
||||
return installTeaCli();
|
||||
} else {
|
||||
const dir = fs.readlinkSync(teaCliPrefix);
|
||||
const v = semverParse(dir)?.toString();
|
||||
if (!v) throw new Error(`couldn't parse to semver: ${dir}`);
|
||||
return v;
|
||||
}
|
||||
});
|
||||
|
||||
cliInitializationState.initialize();
|
||||
|
||||
export async function initializeTeaCli(): Promise<string> {
|
||||
if (
|
||||
cliInitializationState.getState() === "INITIALIZED" &&
|
||||
!fs.existsSync(path.join(teaCliPrefix, "bin/tea"))
|
||||
) {
|
||||
cliInitializationState.reset();
|
||||
}
|
||||
return cliInitializationState.observe();
|
||||
}
|
||||
|
||||
//NOTE copy pasta from https://github.com/teaxyz/setup/blob/main/action.js
|
||||
//FIXME ideally we'd not copy pasta this
|
||||
//NOTE using `tar` is not ideal ∵ Windows and even though tar is POSIX it's still not guaranteed to be available
|
||||
async function installTeaCli() {
|
||||
const PREFIX = `${process.env.HOME}/.tea`;
|
||||
|
||||
const midfix = (() => {
|
||||
switch (process.arch) {
|
||||
case "arm64":
|
||||
return `${process.platform}/aarch64`;
|
||||
case "x64":
|
||||
return `${process.platform}/x86-64`;
|
||||
default:
|
||||
throw new Error(`unsupported platform: ${process.platform}/${process.arch}`);
|
||||
}
|
||||
})();
|
||||
|
||||
/// versions.txt is guaranteed semver-sorted
|
||||
const v: string | undefined = await new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(`https://dist.tea.xyz/tea.xyz/${midfix}/versions.txt`, (rsp) => {
|
||||
if (rsp.statusCode != 200) return reject(rsp.statusCode);
|
||||
rsp.setEncoding("utf8");
|
||||
const chunks: string[] = [];
|
||||
rsp.on("data", (x) => chunks.push(x));
|
||||
rsp.on("end", () => {
|
||||
resolve(chunks.join("").trim().split("\n").at(-1));
|
||||
});
|
||||
})
|
||||
.on("error", reject);
|
||||
});
|
||||
|
||||
if (!v) throw new Error(`invalid versions.txt for tea/cli`);
|
||||
|
||||
fs.mkdirSync(PREFIX, { recursive: true });
|
||||
|
||||
const exitcode = await new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(`https://dist.tea.xyz/tea.xyz/${midfix}/v${v}.tar.gz`, (rsp) => {
|
||||
if (rsp.statusCode != 200) return reject(rsp.statusCode);
|
||||
const tar = spawn("tar", ["xzf", "-"], {
|
||||
stdio: ["pipe", "inherit", "inherit"],
|
||||
cwd: PREFIX
|
||||
});
|
||||
rsp.pipe(tar.stdin);
|
||||
tar.on("close", resolve);
|
||||
})
|
||||
.on("error", reject);
|
||||
});
|
||||
|
||||
if (exitcode != 0) {
|
||||
throw new Error(`tar: ${exitcode}`);
|
||||
}
|
||||
|
||||
const oldwd = process.cwd();
|
||||
process.chdir(`${PREFIX}/tea.xyz`);
|
||||
if (fs.existsSync(`v*`)) fs.unlinkSync(`v*`);
|
||||
fs.symlinkSync(`v${v}`, `v*`, "dir");
|
||||
if (fs.existsSync(`v0`)) fs.unlinkSync(`v0`);
|
||||
fs.symlinkSync(`v${v}`, `v0`, "dir"); //FIXME
|
||||
process.chdir(oldwd);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
export default async function initialize(): Promise<string> {
|
||||
|
|
|
@ -6,7 +6,7 @@ import log from "./logger";
|
|||
import { syncLogsAt } from "./v1-client";
|
||||
import { installPackage, openPackageEntrypointInTerminal, syncPantry } from "./cli";
|
||||
|
||||
import initializeTeaCli from "./initialize";
|
||||
import { initializeTeaCli, cliInitializationState } from "./initialize";
|
||||
|
||||
import { getAutoUpdateStatus, getUpdater } from "./auto-updater";
|
||||
|
||||
|
@ -142,7 +142,10 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions)
|
|||
await deletePackageFolder(fullName, version);
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
return e;
|
||||
} finally {
|
||||
if (fullName === "tea.xyz") {
|
||||
cliInitializationState.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { default as SemVer, isValidSemVer } from "./semver";
|
||||
export { default as SemVer, isValidSemVer, parse } from "./semver";
|
||||
|
|
Loading…
Reference in a new issue