find tea by absolute path, not by name

PM2 hands the sidecar a login-less PATH that excludes ~/.local/bin, which is
exactly where the platform installs a release binary. So `tea whoami` failed
with "the token may be wrong, or tea is not installed" on a machine where tea
was sitting in the owner's home and working perfectly from a shell.

Third time today the same trap: the probe in os-deps, then the agent's env, now
this. Anything the platform installs to ~/.local/bin has to be found
deliberately, because nothing the platform spawns inherits a PATH that includes
it.

Resolved per call rather than cached, so an install that adds it mid-run is
picked up instead of a cached "missing" outliving the thing that fixed it.
This commit is contained in:
officer
2026-08-19 10:14:10 +00:00
parent bf27793b44
commit 4a7aa40a28
+16 -2
View File
@@ -35,6 +35,20 @@ export type TeaLogin = { name: string; url: string; user: string | null; hasToke
/** The owner IS this process, so no escalation — and no sudo prompt on a machine that has no sudo. */
const isOwnerUser = (osUser: string | null): boolean => !osUser || osUser === process.env.USER;
/**
* `tea`, by absolute path.
*
* NOT by bare name. PM2 hands this sidecar a login-less PATH that does not include `~/.local/bin`, which
* is precisely where the platform installs a release binary — so `tea whoami` failed with "not
* installed" on a machine where `tea` was sitting in the owner's home and working perfectly from a
* shell. The same trap caught the agent's PATH an hour earlier.
*
* Resolved once per call rather than cached: an install can add it while this process is running, and a
* cached "missing" would outlive the thing that fixed it.
*/
const teaBinary = (): string =>
Bun.which('tea', { PATH: `${join(homedir(), '.local', 'bin')}:${process.env.PATH ?? ''}` }) ?? 'tea';
const configPathFor = (home: string): string => join(home, '.config', 'tea', 'config.yml');
/**
@@ -120,7 +134,7 @@ export async function writeTeaLogin(
token: string,
): Promise<{ ok: boolean; error?: string }> {
const osUser = await osUserFor(userId);
const command = ['tea', 'login', 'add', '--name', LOGIN_NAME, '--url', instanceUrl, '--token', token];
const command = [teaBinary(), 'login', 'add', '--name', LOGIN_NAME, '--url', instanceUrl, '--token', token];
try {
const proc = isOwnerUser(osUser)
@@ -154,7 +168,7 @@ export async function writeTeaLogin(
*/
export async function testTeaLogin(userId: number): Promise<{ ok: boolean; user?: string; error?: string }> {
const osUser = await osUserFor(userId);
const out = await asUser(osUser, ['tea', 'whoami']);
const out = await asUser(osUser, [teaBinary(), 'whoami']);
if (out === null) {
return { ok: false, error: 'tea could not authenticate — the token may be wrong, or tea is not installed' };
}