From 4a7aa40a2821e988d2f27bac665366a36b2f6b8d Mon Sep 17 00:00:00 2001 From: officer Date: Wed, 19 Aug 2026 10:14:10 +0000 Subject: [PATCH] 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. --- sidecar/tea-config.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/sidecar/tea-config.ts b/sidecar/tea-config.ts index 8376d26..3b17bbe 100644 --- a/sidecar/tea-config.ts +++ b/sidecar/tea-config.ts @@ -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' }; }