Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Commit b7e45f0

Browse files
getneilmxclneil molina
authored
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>
1 parent 2287982 commit b7e45f0

4 files changed

Lines changed: 151 additions & 66 deletions

File tree

modules/desktop/electron/libs/cli.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import { spawn, exec } from "child_process";
2-
import { getGuiPath } from "./tea-dir";
2+
import { getGuiPath, getTeaPath } from "./tea-dir";
33
import fs from "fs";
44
import path from "path";
5-
import initializeTeaCli from "./initialize";
5+
import { initializeTeaCli } from "./initialize";
66

77
import { app } from "electron";
88
import log from "./logger";
99
import { MainWindowNotifier } from "./types";
1010

11-
const destinationDirectory = getGuiPath();
12-
13-
export const cliBinPath = path.join(destinationDirectory, "tea");
11+
export const cliBinPath = path.join(getTeaPath(), "tea.xyz/v*/bin/tea");
1412

1513
export async function installPackage(
1614
full_name: string,
@@ -34,7 +32,7 @@ export async function installPackage(
3432
const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } };
3533

3634
const child = spawn(
37-
`${destinationDirectory}/tea`,
35+
cliBinPath,
3836
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
3937
opts
4038
);
@@ -200,6 +198,6 @@ export async function syncPantry() {
200198
const teaVersion = await initializeTeaCli();
201199

202200
if (!teaVersion) throw new Error("no tea");
203-
log.info("Syncing pantry");
204-
await asyncExec(`cd '${destinationDirectory}' && ./tea -S`);
201+
log.info("Syncing pantry", teaVersion);
202+
await asyncExec(`DEBUG=1 "${cliBinPath}" --sync --env=false`);
205203
}

modules/desktop/electron/libs/initialize.ts

Lines changed: 139 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,162 @@
11
import fs from "fs";
2-
import { getGuiPath } from "./tea-dir";
2+
import { getGuiPath, getTeaPath } from "./tea-dir";
33
import log from "./logger";
4-
import { cliBinPath, asyncExec } from "./cli";
4+
// import { cliBinPath, asyncExec } from "./cli";
55
import { createInitialSessionFile } from "./auth";
6-
import { SemVer, isValidSemVer } from "@tea/libtea";
7-
8-
const MINIMUM_TEA_VERSION = "0.31.2";
9-
10-
const destinationDirectory = getGuiPath();
11-
12-
// TODO: move device_id generation here
6+
import * as https from "https";
7+
import { spawn } from "child_process";
8+
import path from "path";
9+
import { parse as semverParse } from "@tea/libtea";
10+
11+
type InitState = "NOT_INITIALIZED" | "PENDING" | "INITIALIZED";
12+
13+
class InitWatcher<T> {
14+
private initState: InitState;
15+
private initFunction: () => Promise<T>;
16+
private initialValue: T | undefined;
17+
private initializationPromise: Promise<T> | undefined;
18+
19+
constructor(initFunction: () => Promise<T>) {
20+
this.initState = "NOT_INITIALIZED";
21+
this.initFunction = initFunction;
22+
this.initialValue = undefined;
23+
this.initializationPromise = undefined;
24+
}
1325

14-
// Get the binary path from the current app directory
15-
const binaryUrl = "https://tea.xyz/$(uname)/$(uname -m)";
26+
async initialize(): Promise<T> {
27+
if (this.initState === "NOT_INITIALIZED") {
28+
this.initState = "PENDING";
29+
this.initializationPromise = this.retryFunction(this.initFunction, 3)
30+
.then((value) => {
31+
this.initialValue = value;
32+
this.initState = "INITIALIZED";
33+
return value;
34+
})
35+
.catch((error) => {
36+
this.initState = "NOT_INITIALIZED";
37+
this.initializationPromise = undefined;
38+
throw error;
39+
});
40+
}
1641

17-
let initializePromise: Promise<string> | null = null;
42+
return this.initializationPromise as Promise<T>;
43+
}
1844

19-
export async function initializeTeaCli(): Promise<string> {
20-
if (initializePromise) {
21-
return initializePromise;
45+
async retryFunction(func: () => Promise<T>, retries: number, currentAttempt = 1): Promise<T> {
46+
try {
47+
const result = await func();
48+
return result;
49+
} catch (error) {
50+
if (currentAttempt < retries) {
51+
return this.retryFunction(func, retries, currentAttempt + 1);
52+
} else {
53+
throw error;
54+
}
55+
}
2256
}
2357

24-
log.info("Initializing tea cli");
25-
initializePromise = initializeTeaCliInternal();
58+
reset(): void {
59+
this.initState = "NOT_INITIALIZED";
60+
this.initializationPromise = undefined;
61+
}
2662

27-
initializePromise.catch((error) => {
28-
log.info("Error initializing tea cli, resetting promise:", error);
29-
initializePromise = null;
30-
});
63+
async observe(): Promise<T> {
64+
return await this.initialize();
65+
}
3166

32-
return initializePromise;
67+
getState(): InitState {
68+
return this.initState;
69+
}
3370
}
3471

35-
async function initializeTeaCliInternal(): Promise<string> {
36-
let binCheck = "";
37-
let needsUpdate = false;
72+
const teaCliPrefix = path.join(getTeaPath(), "tea.xyz/v*");
3873

39-
// Create the destination directory if it doesn't exist
40-
if (!fs.existsSync(destinationDirectory)) {
41-
fs.mkdirSync(destinationDirectory, { recursive: true });
74+
export const cliInitializationState = new InitWatcher<string>(async () => {
75+
if (!fs.existsSync(path.join(teaCliPrefix, "bin/tea"))) {
76+
return installTeaCli();
77+
} else {
78+
const dir = fs.readlinkSync(teaCliPrefix);
79+
const v = semverParse(dir)?.toString();
80+
if (!v) throw new Error(`couldn't parse to semver: ${dir}`);
81+
return v;
4282
}
83+
});
4384

44-
// replace this with max's pr
45-
const curlCommand = `curl --insecure -L -o "${cliBinPath}" "${binaryUrl}"`;
85+
cliInitializationState.initialize();
4686

47-
const exists = fs.existsSync(cliBinPath);
48-
if (exists) {
49-
log.info("binary tea already exists at", cliBinPath);
50-
try {
51-
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
52-
const teaVersion = binCheck.toString().split(" ")[1].trim();
53-
if (new SemVer(teaVersion).compare(new SemVer(MINIMUM_TEA_VERSION)) < 0) {
54-
log.info("binary tea version is too old, updating");
55-
needsUpdate = true;
56-
}
57-
} catch (error) {
58-
// probably binary is not executable or no permission
59-
log.error("Error checking tea binary version:", error);
60-
needsUpdate = true;
61-
await asyncExec(`cd ${destinationDirectory} && rm tea`);
62-
}
87+
export async function initializeTeaCli(): Promise<string> {
88+
if (
89+
cliInitializationState.getState() === "INITIALIZED" &&
90+
!fs.existsSync(path.join(teaCliPrefix, "bin/tea"))
91+
) {
92+
cliInitializationState.reset();
6393
}
94+
return cliInitializationState.observe();
95+
}
6496

65-
if (!exists || needsUpdate) {
66-
await asyncExec(curlCommand);
67-
log.info("Binary downloaded and saved to", cliBinPath);
68-
await asyncExec("chmod u+x " + cliBinPath);
69-
log.info("Binary is now ready for use at", cliBinPath);
70-
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
97+
//NOTE copy pasta from https://github.com/teaxyz/setup/blob/main/action.js
98+
//FIXME ideally we'd not copy pasta this
99+
//NOTE using `tar` is not ideal ∵ Windows and even though tar is POSIX it's still not guaranteed to be available
100+
async function installTeaCli() {
101+
const PREFIX = `${process.env.HOME}/.tea`;
102+
103+
const midfix = (() => {
104+
switch (process.arch) {
105+
case "arm64":
106+
return `${process.platform}/aarch64`;
107+
case "x64":
108+
return `${process.platform}/x86-64`;
109+
default:
110+
throw new Error(`unsupported platform: ${process.platform}/${process.arch}`);
111+
}
112+
})();
113+
114+
/// versions.txt is guaranteed semver-sorted
115+
const v: string | undefined = await new Promise((resolve, reject) => {
116+
https
117+
.get(`https://dist.tea.xyz/tea.xyz/${midfix}/versions.txt`, (rsp) => {
118+
if (rsp.statusCode != 200) return reject(rsp.statusCode);
119+
rsp.setEncoding("utf8");
120+
const chunks: string[] = [];
121+
rsp.on("data", (x) => chunks.push(x));
122+
rsp.on("end", () => {
123+
resolve(chunks.join("").trim().split("\n").at(-1));
124+
});
125+
})
126+
.on("error", reject);
127+
});
128+
129+
if (!v) throw new Error(`invalid versions.txt for tea/cli`);
130+
131+
fs.mkdirSync(PREFIX, { recursive: true });
132+
133+
const exitcode = await new Promise((resolve, reject) => {
134+
https
135+
.get(`https://dist.tea.xyz/tea.xyz/${midfix}/v${v}.tar.gz`, (rsp) => {
136+
if (rsp.statusCode != 200) return reject(rsp.statusCode);
137+
const tar = spawn("tar", ["xzf", "-"], {
138+
stdio: ["pipe", "inherit", "inherit"],
139+
cwd: PREFIX
140+
});
141+
rsp.pipe(tar.stdin);
142+
tar.on("close", resolve);
143+
})
144+
.on("error", reject);
145+
});
146+
147+
if (exitcode != 0) {
148+
throw new Error(`tar: ${exitcode}`);
71149
}
72150

73-
const version = binCheck.toString().split(" ")[1];
74-
log.info("binary tea version:", version);
75-
return isValidSemVer(version.trim()) ? version : "";
151+
const oldwd = process.cwd();
152+
process.chdir(`${PREFIX}/tea.xyz`);
153+
if (fs.existsSync(`v*`)) fs.unlinkSync(`v*`);
154+
fs.symlinkSync(`v${v}`, `v*`, "dir");
155+
if (fs.existsSync(`v0`)) fs.unlinkSync(`v0`);
156+
fs.symlinkSync(`v${v}`, `v0`, "dir"); //FIXME
157+
process.chdir(oldwd);
158+
159+
return v;
76160
}
77161

78162
export default async function initialize(): Promise<string> {

modules/desktop/electron/libs/ipc.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import log from "./logger";
66
import { syncLogsAt } from "./v1-client";
77
import { installPackage, openPackageEntrypointInTerminal, syncPantry } from "./cli";
88

9-
import initializeTeaCli from "./initialize";
9+
import { initializeTeaCli, cliInitializationState } from "./initialize";
1010

1111
import { getAutoUpdateStatus, getUpdater } from "./auto-updater";
1212

@@ -142,7 +142,10 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions)
142142
await deletePackageFolder(fullName, version);
143143
} catch (e) {
144144
log.error(e);
145-
return e;
145+
} finally {
146+
if (fullName === "tea.xyz") {
147+
cliInitializationState.reset();
148+
}
146149
}
147150
}
148151
);

modules/libtea/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { default as SemVer, isValidSemVer } from "./semver";
1+
export { default as SemVer, isValidSemVer, parse } from "./semver";

0 commit comments

Comments
 (0)