@@ -6,16 +6,117 @@ import { runCommandWithTimeout } from "../process/exec.js";
66import { resolveUserPath } from "../utils.js" ;
77import { normalizeServePath } from "./gmail.js" ;
88
9+ let cachedPythonPath : string | null | undefined ;
10+
11+ function findExecutablesOnPath ( bins : string [ ] ) : string [ ] {
12+ const pathEnv = process . env . PATH ?? "" ;
13+ const parts = pathEnv . split ( path . delimiter ) . filter ( Boolean ) ;
14+ const seen = new Set < string > ( ) ;
15+ const matches : string [ ] = [ ] ;
16+ for ( const part of parts ) {
17+ for ( const bin of bins ) {
18+ const candidate = path . join ( part , bin ) ;
19+ if ( seen . has ( candidate ) ) continue ;
20+ try {
21+ fs . accessSync ( candidate , fs . constants . X_OK ) ;
22+ matches . push ( candidate ) ;
23+ seen . add ( candidate ) ;
24+ } catch {
25+ // keep scanning
26+ }
27+ }
28+ }
29+ return matches ;
30+ }
31+
32+ function ensurePathIncludes ( dirPath : string , position : "append" | "prepend" ) {
33+ const pathEnv = process . env . PATH ?? "" ;
34+ const parts = pathEnv . split ( path . delimiter ) . filter ( Boolean ) ;
35+ if ( parts . includes ( dirPath ) ) return ;
36+ const next =
37+ position === "prepend" ? [ dirPath , ...parts ] : [ ...parts , dirPath ] ;
38+ process . env . PATH = next . join ( path . delimiter ) ;
39+ }
40+
41+ function ensureGcloudOnPath ( ) : boolean {
42+ if ( hasBinary ( "gcloud" ) ) return true ;
43+ const candidates = [
44+ "/opt/homebrew/share/google-cloud-sdk/bin/gcloud" ,
45+ "/usr/local/share/google-cloud-sdk/bin/gcloud" ,
46+ "/opt/homebrew/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/gcloud" ,
47+ "/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/gcloud" ,
48+ ] ;
49+ for ( const candidate of candidates ) {
50+ try {
51+ fs . accessSync ( candidate , fs . constants . X_OK ) ;
52+ ensurePathIncludes ( path . dirname ( candidate ) , "append" ) ;
53+ return true ;
54+ } catch {
55+ // keep scanning
56+ }
57+ }
58+ return false ;
59+ }
60+
61+ export async function resolvePythonExecutablePath ( ) : Promise < string | undefined > {
62+ if ( cachedPythonPath !== undefined ) {
63+ return cachedPythonPath ?? undefined ;
64+ }
65+ const candidates = findExecutablesOnPath ( [ "python3" , "python" ] ) ;
66+ for ( const candidate of candidates ) {
67+ const res = await runCommandWithTimeout (
68+ [
69+ candidate ,
70+ "-c" ,
71+ "import os, sys; print(os.path.realpath(sys.executable))" ,
72+ ] ,
73+ { timeoutMs : 2_000 } ,
74+ ) ;
75+ if ( res . code !== 0 ) continue ;
76+ const resolved = res . stdout . trim ( ) . split ( / \s + / ) [ 0 ] ;
77+ if ( ! resolved ) continue ;
78+ try {
79+ fs . accessSync ( resolved , fs . constants . X_OK ) ;
80+ cachedPythonPath = resolved ;
81+ return resolved ;
82+ } catch {
83+ // keep scanning
84+ }
85+ }
86+ cachedPythonPath = null ;
87+ return undefined ;
88+ }
89+
90+ async function gcloudEnv ( ) : Promise < NodeJS . ProcessEnv | undefined > {
91+ if ( process . env . CLOUDSDK_PYTHON ) return undefined ;
92+ const pythonPath = await resolvePythonExecutablePath ( ) ;
93+ if ( ! pythonPath ) return undefined ;
94+ return { CLOUDSDK_PYTHON : pythonPath } ;
95+ }
96+
97+ async function runGcloudCommand (
98+ args : string [ ] ,
99+ timeoutMs : number ,
100+ ) : Promise < Awaited < ReturnType < typeof runCommandWithTimeout > > > {
101+ return await runCommandWithTimeout ( [ "gcloud" , ...args ] , {
102+ timeoutMs,
103+ env : await gcloudEnv ( ) ,
104+ } ) ;
105+ }
106+
9107export async function ensureDependency ( bin : string , brewArgs : string [ ] ) {
108+ if ( bin === "gcloud" && ensureGcloudOnPath ( ) ) return ;
10109 if ( hasBinary ( bin ) ) return ;
11110 if ( process . platform !== "darwin" ) {
12111 throw new Error ( `${ bin } not installed; install it and retry` ) ;
13112 }
14113 if ( ! hasBinary ( "brew" ) ) {
15114 throw new Error ( "Homebrew not installed (install brew and retry)" ) ;
16115 }
116+ const brewEnv = bin === "gcloud" ? await gcloudEnv ( ) : undefined ;
17117 const result = await runCommandWithTimeout ( [ "brew" , "install" , ...brewArgs ] , {
18118 timeoutMs : 600_000 ,
119+ env : brewEnv ,
19120 } ) ;
20121 if ( result . code !== 0 ) {
21122 throw new Error (
@@ -28,49 +129,29 @@ export async function ensureDependency(bin: string, brewArgs: string[]) {
28129}
29130
30131export async function ensureGcloudAuth ( ) {
31- const res = await runCommandWithTimeout (
32- [
33- "gcloud" ,
34- "auth" ,
35- "list" ,
36- "--filter" ,
37- "status:ACTIVE" ,
38- "--format" ,
39- "value(account)" ,
40- ] ,
41- { timeoutMs : 30_000 } ,
132+ const res = await runGcloudCommand (
133+ [ "auth" , "list" , "--filter" , "status:ACTIVE" , "--format" , "value(account)" ] ,
134+ 30_000 ,
42135 ) ;
43136 if ( res . code === 0 && res . stdout . trim ( ) ) return ;
44- const login = await runCommandWithTimeout ( [ "gcloud" , "auth" , "login" ] , {
45- timeoutMs : 600_000 ,
46- } ) ;
137+ const login = await runGcloudCommand ( [ "auth" , "login" ] , 600_000 ) ;
47138 if ( login . code !== 0 ) {
48139 throw new Error ( login . stderr || "gcloud auth login failed" ) ;
49140 }
50141}
51142
52143export async function runGcloud ( args : string [ ] ) {
53- const result = await runCommandWithTimeout ( [ "gcloud" , ...args ] , {
54- timeoutMs : 120_000 ,
55- } ) ;
144+ const result = await runGcloudCommand ( args , 120_000 ) ;
56145 if ( result . code !== 0 ) {
57146 throw new Error ( result . stderr || result . stdout || "gcloud command failed" ) ;
58147 }
59148 return result ;
60149}
61150
62151export async function ensureTopic ( projectId : string , topicName : string ) {
63- const describe = await runCommandWithTimeout (
64- [
65- "gcloud" ,
66- "pubsub" ,
67- "topics" ,
68- "describe" ,
69- topicName ,
70- "--project" ,
71- projectId ,
72- ] ,
73- { timeoutMs : 30_000 } ,
152+ const describe = await runGcloudCommand (
153+ [ "pubsub" , "topics" , "describe" , topicName , "--project" , projectId ] ,
154+ 30_000 ,
74155 ) ;
75156 if ( describe . code === 0 ) return ;
76157 await runGcloud ( [
@@ -89,17 +170,9 @@ export async function ensureSubscription(
89170 topicName : string ,
90171 pushEndpoint : string ,
91172) {
92- const describe = await runCommandWithTimeout (
93- [
94- "gcloud" ,
95- "pubsub" ,
96- "subscriptions" ,
97- "describe" ,
98- subscription ,
99- "--project" ,
100- projectId ,
101- ] ,
102- { timeoutMs : 30_000 } ,
173+ const describe = await runGcloudCommand (
174+ [ "pubsub" , "subscriptions" , "describe" , subscription , "--project" , projectId ] ,
175+ 30_000 ,
103176 ) ;
104177 if ( describe . code === 0 ) {
105178 await runGcloud ( [
@@ -188,17 +261,16 @@ export async function resolveProjectIdFromGogCredentials(): Promise<
188261 const clientId = extractGogClientId ( parsed ) ;
189262 const projectNumber = extractProjectNumber ( clientId ) ;
190263 if ( ! projectNumber ) continue ;
191- const res = await runCommandWithTimeout (
264+ const res = await runGcloudCommand (
192265 [
193- "gcloud" ,
194266 "projects" ,
195267 "list" ,
196268 "--filter" ,
197269 `projectNumber=${ projectNumber } ` ,
198270 "--format" ,
199271 "value(projectId)" ,
200272 ] ,
201- { timeoutMs : 30_000 } ,
273+ 30_000 ,
202274 ) ;
203275 if ( res . code !== 0 ) continue ;
204276 const projectId = res . stdout . trim ( ) . split ( / \s + / ) [ 0 ] ;
0 commit comments