11import type { Command } from "commander" ;
2- import { AdminClient } from "../lib/admin-client.js" ;
32import { load as loadConfig , save as saveConfig } from "../lib/config.js" ;
4- import { promptSelect , promptText } from "../lib/terminal-ui.js" ;
3+ import { promptText } from "../lib/terminal-ui.js" ;
54import {
65 brand ,
76 bold ,
87 brandedHelp ,
98 dim ,
109 green ,
11- maskIf ,
12- printKeyValueBox ,
1310} from "../lib/ui.js" ;
1411import { getUpdateNoticeLines } from "../lib/update-check.js" ;
15- import { selectOrCreateApp } from "./config.js" ;
16- import { generateAndPersistWallet , importAndPersistWallet } from "./wallet.js" ;
17-
18- type OnboardingMethod = "browser-login" | "api-key" | "access-key" | "siwx" | "exit" ;
19-
20- function printNextSteps ( method : Exclude < OnboardingMethod , "exit" > ) : void {
21- const commandsByMethod : Record < Exclude < OnboardingMethod , "exit" > , string [ ] > = {
22- "browser-login" : [ "alchemy auth" ] ,
23- "api-key" : [ "alchemy config set api-key <key>" ] ,
24- "access-key" : [
25- "alchemy config set access-key <key>" ,
26- "alchemy config set app <app-id>" ,
27- ] ,
28- siwx : [
29- "alchemy wallet generate" ,
30- "alchemy config set wallet-key-file <path>" ,
31- "alchemy config set x402 true" ,
32- ] ,
33- } ;
34-
35- console . log ( "" ) ;
36- console . log ( ` ${ dim ( "Next steps:" ) } ` ) ;
37- for ( const command of commandsByMethod [ method ] ) {
38- console . log ( ` ${ dim ( `- ${ command } ` ) } ` ) ;
39- }
40- }
41-
42- function printAPIKeyPostSetupGuidance ( ) : void {
43- const cfg = loadConfig ( ) ?? { } ;
44- const network = cfg . network ?? "eth-mainnet" ;
45-
46- console . log ( "" ) ;
47- console . log ( ` ${ brand ( "◆" ) } ${ bold ( "Your configuration" ) } ` ) ;
48- printKeyValueBox ( [
49- [ "api-key" , cfg . api_key ? maskIf ( cfg . api_key ) : dim ( "(not set)" ) ] ,
50- [ "network" , cfg . network ? network : `${ network } ${ dim ( "(default)" ) } ` ] ,
51- ] ) ;
52-
53- console . log ( "" ) ;
54- console . log ( ` ${ brand ( "◆" ) } ${ bold ( "Next steps" ) } ` ) ;
55- printKeyValueBox ( [
56- [ "Verify setup" , "rpc eth_chainId" ] ,
57- [ "Need a different chain?" , "config set network <network>" ] ,
58- [ "List available chains" , "network list" ] ,
59- [ "View set API key" , "config get api-key" ] ,
60- [ "Need help?" , "help" ] ,
61- ] ) ;
62- }
63-
64- async function runAPIKeyOnboarding ( ) : Promise < void > {
65- const key = await promptText ( {
66- message : "Enter API Key" ,
67- cancelMessage : "Skipped API key setup." ,
68- clearAfterSubmit : true ,
69- } ) ;
70- if ( ! key || ! key . trim ( ) ) return ;
71- const cfg = loadConfig ( ) ;
72- saveConfig ( { ...cfg , api_key : key . trim ( ) } ) ;
73- console . log ( ` ${ green ( "✓" ) } Saved API key` ) ;
74- }
75-
76- async function runAccessKeyOnboarding ( ) : Promise < void > {
77- const key = await promptText ( {
78- message : "Alchemy access key" ,
79- placeholder : "Used for Admin API operations" ,
80- cancelMessage : "Skipped access key setup." ,
81- } ) ;
82- if ( ! key || ! key . trim ( ) ) return ;
83-
84- const cfg = loadConfig ( ) ;
85- saveConfig ( { ...cfg , access_key : key . trim ( ) } ) ;
86- console . log ( ` ${ green ( "✓" ) } Saved access key` ) ;
87- await selectOrCreateApp ( new AdminClient ( key . trim ( ) ) ) ;
88- }
89-
90- async function runSiwxOnboarding ( ) : Promise < void > {
91- const action = await promptSelect ( {
92- message : "SIWx wallet setup" ,
93- options : [
94- { label : "Generate a new wallet" , value : "generate" } ,
95- { label : "Import wallet from key file" , value : "import" } ,
96- ] ,
97- initialValue : "generate" ,
98- cancelMessage : "Skipped SIWx setup." ,
99- } ) ;
100- if ( ! action ) return ;
101-
102- const wallet =
103- action === "generate"
104- ? generateAndPersistWallet ( )
105- : await ( async ( ) => {
106- const path = await promptText ( {
107- message : "Wallet private key file path" ,
108- cancelMessage : "Skipped wallet import." ,
109- } ) ;
110- if ( ! path || ! path . trim ( ) ) return null ;
111- return importAndPersistWallet ( path . trim ( ) ) ;
112- } ) ( ) ;
113- if ( ! wallet ) return ;
114-
115- const cfg = loadConfig ( ) ;
116- saveConfig ( { ...cfg , x402 : true } ) ;
117- console . log ( ` ${ green ( "✓" ) } SIWx enabled with wallet ${ wallet . address } ` ) ;
118-
119- // Sign SIWE token immediately so it's cached for subsequent commands
120- try {
121- const { signSiwe } = await import ( "@alchemy/x402" ) ;
122- const { readFileSync } = await import ( "node:fs" ) ;
123- const keyPath = wallet . keyFile ;
124- const privateKey = readFileSync ( keyPath , "utf-8" ) . trim ( ) ;
125- const siweToken = await signSiwe ( { privateKey, expiresAfter : "1h" } ) ;
126- const expiresAt = new Date ( Date . now ( ) + 60 * 60 * 1000 ) . toISOString ( ) ;
127- saveConfig ( { ...loadConfig ( ) , siwe_token : siweToken , siwe_token_expires_at : expiresAt } ) ;
128- console . log ( ` ${ green ( "✓" ) } Signed SIWE token (cached for 1h)` ) ;
129- } catch {
130- // Non-fatal — token will be signed on first API call
131- }
132- }
13312
13413export async function runOnboarding (
13514 _program : Command ,
@@ -140,98 +19,40 @@ export async function runOnboarding(
14019 console . log ( ` ${ brand ( "◆" ) } ${ bold ( "Welcome to Alchemy CLI" ) } ` ) ;
14120 console . log ( ` ${ dim ( " ────────────────────────────────────" ) } ` ) ;
14221 console . log ( ` ${ dim ( " Let's get you set up with authentication." ) } ` ) ;
143- console . log ( ` ${ dim ( " Choose one auth path to continue." ) } ` ) ;
144- console . log ( ` ${ dim ( " Tip: select 'exit' to skip setup for now." ) } ` ) ;
14522 console . log ( "" ) ;
14623 if ( latestUpdate ) {
14724 for ( const line of getUpdateNoticeLines ( latestUpdate ) ) {
14825 console . log ( line ) ;
14926 }
15027 console . log ( "" ) ;
15128 }
152- const method = await promptSelect < OnboardingMethod > ( {
153- message : "Choose an auth setup path" ,
154- options : [
155- {
156- label : "Browser login" ,
157- hint : "Log in via browser (recommended)" ,
158- value : "browser-login" ,
159- } ,
160- {
161- label : "API key" ,
162- hint : "Query Alchemy RPC nodes" ,
163- value : "api-key" ,
164- } ,
165- {
166- label : "Access Key" ,
167- hint : "Admin API plus RPC nodes" ,
168- value : "access-key" ,
169- } ,
170- {
171- label : "SIWx" ,
172- hint : "Sign-In with Ethereum/Solana wallet" ,
173- value : "siwx" ,
174- } ,
175- {
176- label : "exit" ,
177- value : "exit" ,
178- } ,
179- ] ,
180- initialValue : "browser-login" ,
29+
30+ const answer = await promptText ( {
31+ message : "Press Enter to open browser and link your Alchemy account" ,
18132 cancelMessage : "Skipped onboarding." ,
18233 } ) ;
183- if ( ! method ) return false ;
184- if ( method === "exit" ) {
185- console . log ( ` ${ dim ( "Exited onboarding." ) } ` ) ;
34+ if ( answer === null ) {
18635 return false ;
18736 }
18837
189- if ( method === "browser-login" ) {
190- const { performBrowserLogin, AUTH_PORT , getLoginUrl } = await import ( "../lib/auth.js" ) ;
191- console . log ( ` Opening browser to log in...` ) ;
192- console . log ( ` ${ dim ( getLoginUrl ( AUTH_PORT ) ) } ` ) ;
193- console . log ( ` ${ dim ( "Waiting for authentication..." ) } ` ) ;
194- try {
195- const result = await performBrowserLogin ( ) ;
196- const cfg = loadConfig ( ) ;
197- saveConfig ( {
198- ...cfg ,
199- auth_token : result . token ,
200- auth_token_expires_at : result . expiresAt ,
201- } ) ;
202- console . log ( ` ${ green ( "✓" ) } Logged in successfully` ) ;
203- const { selectAppAfterAuth } = await import ( "./auth.js" ) ;
204- await selectAppAfterAuth ( result . token ) ;
205- return true ;
206- } catch ( err ) {
207- console . log ( ` ${ dim ( `Login failed: ${ err instanceof Error ? err . message : String ( err ) } ` ) } ` ) ;
208- return false ;
209- }
210- }
211- if ( method === "api-key" ) {
212- await runAPIKeyOnboarding ( ) ;
213- const complete = Boolean ( loadConfig ( ) . api_key ?. trim ( ) ) ;
214- if ( ! complete ) {
215- printNextSteps ( "api-key" ) ;
216- } else {
217- printAPIKeyPostSetupGuidance ( ) ;
218- }
219- return complete ;
220- }
221- if ( method === "access-key" ) {
222- await runAccessKeyOnboarding ( ) ;
38+ const { performBrowserLogin, AUTH_PORT , getLoginUrl } = await import ( "../lib/auth.js" ) ;
39+ console . log ( ` Opening browser to log in...` ) ;
40+ console . log ( ` ${ dim ( getLoginUrl ( AUTH_PORT ) ) } ` ) ;
41+ console . log ( ` ${ dim ( "Waiting for authentication..." ) } ` ) ;
42+ try {
43+ const result = await performBrowserLogin ( ) ;
22344 const cfg = loadConfig ( ) ;
224- const complete = Boolean ( cfg . access_key ?. trim ( ) && cfg . app ?. id && cfg . app . apiKey ) ;
225- if ( ! complete ) {
226- printNextSteps ( "access-key" ) ;
227- }
228- return complete ;
229- }
230- await runSiwxOnboarding ( ) ;
231- const cfg = loadConfig ( ) ;
232- const complete = cfg . x402 === true && Boolean ( cfg . wallet_key_file ?. trim ( ) ) ;
233- if ( ! complete ) {
234- printNextSteps ( "siwx" ) ;
45+ saveConfig ( {
46+ ...cfg ,
47+ auth_token : result . token ,
48+ auth_token_expires_at : result . expiresAt ,
49+ } ) ;
50+ console . log ( ` ${ green ( "✓" ) } Logged in successfully` ) ;
51+ const { selectAppAfterAuth } = await import ( "./auth.js" ) ;
52+ await selectAppAfterAuth ( result . token ) ;
53+ return true ;
54+ } catch ( err ) {
55+ console . log ( ` ${ dim ( `Login failed: ${ err instanceof Error ? err . message : String ( err ) } ` ) } ` ) ;
56+ return false ;
23557 }
236- return complete ;
23758}
0 commit comments