diff --git a/modules/desktop/electron/electron.ts b/modules/desktop/electron/electron.ts index 90d1251..a73c68c 100644 --- a/modules/desktop/electron/electron.ts +++ b/modules/desktop/electron/electron.ts @@ -14,7 +14,7 @@ import initializePushNotification, { syncPackageTopicSubscriptions } from "./libs/push-notification"; -import init, { initializeTeaCli } from "./libs/initialize"; +import init from "./libs/initialize"; import { readSessionData } from "./libs/auth"; import { isDev } from "./libs/auto-updater"; @@ -38,11 +38,10 @@ if (app.isPackaged) { } }); Sentry.configureScope(async (scope) => { - const [session, cliVersion] = await Promise.all([readSessionData(), initializeTeaCli()]); + const session = await readSessionData(); scope.setUser({ id: session.device_id, // device_id this should exist in our pg db: developer_id is to many device_id - username: session?.user?.login || "", // github username or handler - tea: cliVersion + username: session?.user?.login || "" // github username or handler }); }); setSentryLogging(Sentry); diff --git a/modules/desktop/electron/libs/cli.ts b/modules/desktop/electron/libs/cli.ts index cdea472..5f7da28 100644 --- a/modules/desktop/electron/libs/cli.ts +++ b/modules/desktop/electron/libs/cli.ts @@ -1,139 +1,76 @@ -import { spawn, exec } from "child_process"; -import { getGuiPath, getTeaPath } from "./tea-dir"; +import { spawn } from "child_process"; +import { getGuiPath } from "./tea-dir"; import fs from "fs"; import path from "path"; -import { initializeTeaCli } from "./initialize"; +import { hooks } from "@teaxyz/lib"; -import { app } from "electron"; import log from "./logger"; import { MainWindowNotifier } from "./types"; - -// Be careful with globbing when passing this to a shell which might expand it. Either escape it or quote it. -export const cliBinPath = path.join(getTeaPath(), "tea.xyz/v*/bin/tea"); +import { Installation, Package, porcelain } from "@teaxyz/lib"; +import type { Resolution } from "@teaxyz/lib/script/src/plumbing/resolve"; export async function installPackage( full_name: string, version: string, notifyMainWindow: MainWindowNotifier ) { - const teaVersion = await initializeTeaCli(); - const progressNotifier = newInstallProgressNotifier(full_name, notifyMainWindow); - - if (!teaVersion) throw new Error("no tea"); + const notifier = newInstallProgressNotifier(full_name, notifyMainWindow); const qualifedPackage = `${full_name}@${version}`; - log.info(`installing package ${qualifedPackage}`); - - let stdout = ""; - let stderr = ""; - - await new Promise((resolve, reject) => { - // tea requires HOME to be set. - const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } }; - - const child = spawn( - cliBinPath, - ["--env=false", "--sync", "--json", `+${qualifedPackage}`], - opts - ); - - child.stdout.on("data", (data) => { - stdout += data.toString().trim(); - }); - - child.stderr.on("data", (data) => { - try { - data - .toString() - .split("\n") - .map((s: string) => s.trim()) - .filter((s: string) => s.length > 0) - .forEach((line) => { - try { - const msg = JSON.parse(line.trim()); - progressNotifier(msg); - } catch (err) { - log.error("handling cli notification line", line, err); - } - }); - - stderr += data.toString(); - } catch (err) { - log.error("error processing cli data", data.toString(), err); - } - }); - - child.on("exit", (code) => { - log.info("cli exited with code:", code); - log.info("cli stdout:", stdout); - if (code !== 0) { - log.info("cli stderr:", stderr); - reject(new Error("tea exited with non-zero code: " + code)); - } else { - resolve(null); - } - }); - - child.on("error", () => { - reject(new Error(stderr)); - }); - }); + const result = await porcelain.install(qualifedPackage, notifier); + console.log(`successfully installed ${qualifedPackage}`, result); } -// This is hacky and kind of complex because of the output we get from the CLI. When the CLI -// gives better output this definitely should get looked at. function newInstallProgressNotifier(full_name: string, notifyMainWindow: MainWindowNotifier) { - // the install progress is super spammy, only send every 10th update - let counter = 0; - // the totall number of packages to install - this is set by the "resolved" message let numberOfPackages = 1; // the current package number - this is incremented by the "installed" or "downloaded" message let currentPackageNumber = 0; - return function (msg: any) { - if (msg.status !== "downloading" && msg.status !== "installing") { - log.info("cli:", msg); - } - - if (msg.status === "resolved") { - numberOfPackages = msg.pkgs?.length ?? 1; - log.info(`installing ${numberOfPackages} packages`); - } else if (msg.status === "downloading") { - counter++; - if (counter % 10 !== 0) return; - - const { received = 0, "content-size": contentSize = 0 } = msg; - if (contentSize > 0) { - // how many total pacakges are completed - const overallProgress = (currentPackageNumber / numberOfPackages) * 100; - // how much of the current package is completed - const packageProgress = (received / contentSize) * 100; - // progress is the total packages completed plus the percentage of the current package - const progress = overallProgress + packageProgress / numberOfPackages; - notifyMainWindow("install-progress", { full_name, progress }); + return { + resolved: ({ pending }: Resolution) => { + numberOfPackages = pending.length ?? 1; + log.info(`resolved ${numberOfPackages} packages to install`); + }, + installing: ({ pkg, progress }: { pkg: Package; progress: number | undefined }) => { + log.info(`installing ${pkg.project}@${pkg.version} - ${progress}`); + if (progress && progress > 0) { + // how many total packages are completed + const completedProgress = (currentPackageNumber / numberOfPackages) * 100; + // overallProgress is the total packages completed plus the percentage of the current package + const overallProgress = completedProgress + (progress / numberOfPackages) * 100; + notifyMainWindow("install-progress", { full_name, progress: overallProgress }); } - } else if (msg.status === "installed") { + }, + installed: (installation: Installation) => { + log.info("installed", installation); + const { project, version } = installation.pkg; + currentPackageNumber++; const progress = (currentPackageNumber / numberOfPackages) * 100; notifyMainWindow("install-progress", { full_name, progress }); - notifyPackageInstalled(msg.pkg, notifyMainWindow); + notifyMainWindow("pkg-installed", { full_name: project, version: version.toString() }); } }; } -const notifyPackageInstalled = (rawPkg: string, notifyMainWindow: MainWindowNotifier) => { - try { - const [full_name, version] = rawPkg.split("="); - notifyMainWindow("pkg-installed", { full_name, version }); - } catch (err) { - log.error("failed to notify package installed", err); +// the tea cli package is needed to open any other package in the terminal, so make sure it's installed and return the path +async function installTeaCli() { + const installations = await porcelain.install("tea.xyz"); + const teaPkg = installations.find((i) => i.pkg.project === "tea.xyz"); + if (!teaPkg) { + throw new Error("could not find or install tea cli!"); } -}; + + return teaPkg.path.join("bin/tea"); +} export async function openPackageEntrypointInTerminal(pkg: string) { + const cliBinPath = await installTeaCli(); + log.info(`opening package ${pkg} with tea cli at ${cliBinPath}`); + let sh = `"${cliBinPath}" --sync --env=false +${pkg} `; switch (pkg) { case "github.com/AUTOMATIC1111/stable-diffusion-webui": @@ -194,24 +131,8 @@ const createCommandScriptFile = async (cmd: string): Promise => { return tmpFilePath; }; -export async function asyncExec(cmd: string): Promise { - return new Promise((resolve, reject) => { - exec(cmd, (err, stdout) => { - if (err) { - console.log("err:", err); - reject(err); - return; - } - console.log("stdout:", stdout); - resolve(stdout); - }); - }); -} - export async function syncPantry() { - const teaVersion = await initializeTeaCli(); - - if (!teaVersion) throw new Error("no tea"); - log.info("Syncing pantry", teaVersion); - await asyncExec(`DEBUG=1 "${cliBinPath}" --sync --env=false`); + log.info("syncing pantry"); + await hooks.useSync(); + log.info("syncing pantry completed"); } diff --git a/modules/desktop/electron/libs/initialize.ts b/modules/desktop/electron/libs/initialize.ts index c3d5b9b..cd0360e 100644 --- a/modules/desktop/electron/libs/initialize.ts +++ b/modules/desktop/electron/libs/initialize.ts @@ -1,10 +1,4 @@ -import fs from "fs"; -import { getTeaPath } from "./tea-dir"; import { authFileState } from "./auth"; -import * as https from "https"; -import { spawn } from "child_process"; -import path from "path"; -import { semver } from "@teaxyz/lib"; type InitState = "NOT_INITIALIZED" | "PENDING" | "INITIALIZED"; @@ -63,99 +57,6 @@ export class InitWatcher { return this.initState; } } - -// Be careful with globbing when passing this to a shell which might expand it. Either escape it or quote it. -const teaCliPrefix = path.join(getTeaPath(), "tea.xyz/v*"); - -export const cliInitializationState = new InitWatcher(async () => { - if (!fs.existsSync(path.join(teaCliPrefix, "bin/tea"))) { - return installTeaCli(); - } else { - const dir = fs.readlinkSync(teaCliPrefix); - const v = semver.parse(dir)?.toString(); - if (!v) throw new Error(`couldn't parse to semver: ${dir}`); - return v; - } -}); - -cliInitializationState.initialize(); - -export async function initializeTeaCli(): Promise { - 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 { - const [version] = await Promise.all([initializeTeaCli(), authFileState.observe()]); - return version; +export default async function initialize() { + await authFileState.observe(); } diff --git a/modules/desktop/electron/libs/ipc.ts b/modules/desktop/electron/libs/ipc.ts index 8e1b7aa..cca0fb9 100644 --- a/modules/desktop/electron/libs/ipc.ts +++ b/modules/desktop/electron/libs/ipc.ts @@ -13,8 +13,6 @@ import log from "./logger"; import { syncLogsAt } from "./v1-client"; import { installPackage, openPackageEntrypointInTerminal, syncPantry } from "./cli"; -import { initializeTeaCli, cliInitializationState } from "./initialize"; - import { getAutoUpdateStatus, getUpdater, isDev } from "./auto-updater"; import { loadPackageCache, writePackageCache } from "./package"; @@ -59,8 +57,7 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) ipcMain.handle("get-session", async () => { try { log.info("getting session"); - const [session, cliVersion] = await Promise.all([readSessionData(), initializeTeaCli()]); - session.teaVersion = cliVersion; + const session = await readSessionData(); log.debug(session ? "found session data" : "no session data found"); return session; } catch (error) { @@ -168,10 +165,6 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) } } catch (e) { log.error(e); - } finally { - if (fullName === "tea.xyz") { - cliInitializationState.reset(); - } } } ); @@ -193,21 +186,6 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) } }); - ipcMain.handle("get-tea-version", async () => { - try { - log.info("installing tea cli"); - const version = await initializeTeaCli(); - if (!version) { - throw new Error("failed to install tea cli"); - } - - return { version, message: "" }; - } catch (error) { - log.error(error); - return { version: "", message: error.message }; - } - }); - ipcMain.handle("topbar-double-click", async (event: Electron.IpcMainInvokeEvent) => { const mainWindow = BrowserWindow.fromWebContents(event.sender); if (mainWindow) { diff --git a/modules/desktop/package.json b/modules/desktop/package.json index ab4e41a..05f3157 100644 --- a/modules/desktop/package.json +++ b/modules/desktop/package.json @@ -88,7 +88,7 @@ "@sentry/electron": "^4.4.0", "@sentry/svelte": "^7.47.0", "@sentry/vite-plugin": "^0.7.2", - "@teaxyz/lib": "^0.2.2", + "@teaxyz/lib": "^0.3.0", "@types/electron": "^1.6.10", "@types/mousetrap": "^1.6.11", "@vitest/coverage-c8": "^0.27.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f492738..f180e55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,7 +30,7 @@ importers: '@sveltejs/adapter-static': ^1.0.0-next.48 '@sveltejs/kit': ^1.15.9 '@tea/ui': workspace:* - '@teaxyz/lib': ^0.2.2 + '@teaxyz/lib': ^0.3.0 '@testing-library/jest-dom': ^5.16.5 '@testing-library/svelte': ^3.2.2 '@testing-library/webdriverio': ^3.2.1 @@ -110,7 +110,7 @@ importers: '@sentry/electron': 4.5.0 '@sentry/svelte': 7.51.2_svelte@3.59.1 '@sentry/vite-plugin': 0.7.2 - '@teaxyz/lib': 0.2.2 + '@teaxyz/lib': 0.3.0 '@types/electron': 1.6.10 '@types/mousetrap': 1.6.11 '@vitest/coverage-c8': 0.27.3_jsdom@21.1.2 @@ -3637,8 +3637,8 @@ packages: tailwindcss: 3.3.2 dev: false - /@teaxyz/lib/0.2.2: - resolution: {integrity: sha512-xChLHuuwbUXZcHMmqMsGIB6RbXqEb1hXgRYJk4Zg3KfozhRllXuiZ6VkUZYPib66ygMjPlndclG7tj2cDlsLkg==} + /@teaxyz/lib/0.3.0: + resolution: {integrity: sha512-NFoVdSE4iX5JBdiXXOo0aGk4fsTb5zjBOqkY9ldA8asa8pITSJjPOxoa34H7gkkxuTcjv/zjeiOREveWdibcdA==} dependencies: '@deno/shim-crypto': 0.3.1 '@deno/shim-deno': 0.16.1