@@ -4,6 +4,13 @@ import { hideBin } from 'yargs/helpers'
44import { createClient , type SupabaseClient } from '@supabase/supabase-js'
55import { addPowerSyncRemote , syncPowerSyncRepository , seedDemoRepository } from './index.js'
66import { loginWithDaemonDevice , logout as logoutSession } from './auth/login.js'
7+ import {
8+ completeDaemonDeviceLogin ,
9+ extractDeviceChallenge ,
10+ fetchDaemonAuthStatus ,
11+ postDaemonAuthDevice ,
12+ resolveDaemonBaseUrl ,
13+ } from './auth/daemon-client.js'
714import {
815 resolveProfile ,
916 listProfiles ,
@@ -25,6 +32,10 @@ interface LoginCommandArgs {
2532 endpoint ? : string
2633 session ? : string
2734 daemonUrl ? : string
35+ supabaseEmail ? : string
36+ supabasePassword ? : string
37+ supabaseUrl ? : string
38+ supabaseAnonKey ? : string
2839}
2940
3041interface DemoSeedCommandArgs {
@@ -134,6 +145,10 @@ function firstNonEmpty(...candidates: Array<string | null | undefined>): string
134145 return undefined
135146}
136147
148+ function hasText ( value : unknown ) : value is string {
149+ return typeof value === 'string' && value . trim ( ) . length > 0
150+ }
151+
137152function parseKeyPath ( input : string ) : string [ ] {
138153 const segments = input
139154 . split ( '.' )
@@ -343,10 +358,129 @@ async function runDemoSeedCommand(args: DemoSeedCommandArgs) {
343358}
344359
345360async function runLoginCommand ( args : LoginCommandArgs ) {
346- if ( args . session ) {
361+ const wantsSupabasePasswordLogin =
362+ hasText ( args . supabaseEmail ) ||
363+ hasText ( args . supabasePassword ) ||
364+ hasText ( args . supabaseUrl ) ||
365+ hasText ( args . supabaseAnonKey )
366+
367+ if ( args . session && ! wantsSupabasePasswordLogin ) {
347368 console . warn ( `${ LOG_PREFIX } Ignoring legacy --session option; Supabase session persistence is automatic.` )
348369 }
349370
371+ if ( wantsSupabasePasswordLogin ) {
372+ const daemonBaseUrl = await resolveDaemonBaseUrl ( { daemonUrl : args . daemonUrl } )
373+ const status = await fetchDaemonAuthStatus ( daemonBaseUrl )
374+
375+ if ( status ?. status === 'ready' ) {
376+ persistDaemonCredentials ( '' , status . expiresAt ?? null , status . context ?? null )
377+ console . log ( '✅ PowerSync daemon already authenticated via Supabase.' )
378+ if ( status . expiresAt ) {
379+ console . log ( ` Expires: ${ status . expiresAt } ` )
380+ }
381+ return
382+ }
383+
384+ const supabaseUrl = firstNonEmpty (
385+ args . supabaseUrl ,
386+ process . env . SUPABASE_URL ,
387+ process . env . POWERGIT_TEST_SUPABASE_URL ,
388+ )
389+ const supabaseAnonKey = firstNonEmpty (
390+ args . supabaseAnonKey ,
391+ process . env . SUPABASE_ANON_KEY ,
392+ process . env . POWERGIT_TEST_SUPABASE_ANON_KEY ,
393+ )
394+ if ( ! supabaseUrl || ! supabaseAnonKey ) {
395+ throw new Error ( 'Supabase URL and anon key are required. Configure SUPABASE_URL and SUPABASE_ANON_KEY.' )
396+ }
397+
398+ const email = firstNonEmpty ( args . supabaseEmail , process . env . SUPABASE_EMAIL , process . env . POWERGIT_TEST_SUPABASE_EMAIL )
399+ const password = firstNonEmpty (
400+ args . supabasePassword ,
401+ process . env . SUPABASE_PASSWORD ,
402+ process . env . POWERGIT_TEST_SUPABASE_PASSWORD ,
403+ )
404+ if ( ! email || ! password ) {
405+ throw new Error ( 'Supabase email and password are required. Use --supabase-email/--supabase-password.' )
406+ }
407+
408+ const authStoragePath = resolveSupabaseSessionPath ( args . session )
409+ const supabaseStorage = createSupabaseFileStorage ( authStoragePath )
410+ const supabase = createClient ( supabaseUrl , supabaseAnonKey , {
411+ auth : {
412+ persistSession : true ,
413+ autoRefreshToken : true ,
414+ storage : supabaseStorage ,
415+ storageKey : 'powergit' ,
416+ } ,
417+ } )
418+
419+ const { data , error } = await supabase . auth . signInWithPassword ( { email, password } )
420+ if ( error ) {
421+ throw new Error ( `Supabase login failed (${ error . name ?? 'AuthError' } ): ${ error . message } ` )
422+ }
423+ const session = data ?. session ?? ( await supabase . auth . getSession ( ) ) . data . session
424+ const accessToken = session ?. access_token ?. trim ?. ( ) ?? ''
425+ const refreshToken = session ?. refresh_token ?. trim ?. ( ) ?? ''
426+ if ( ! accessToken || ! refreshToken ) {
427+ throw new Error ( 'Supabase login response did not include an access_token and refresh_token.' )
428+ }
429+
430+ const existingChallenge = extractDeviceChallenge ( status ) ?. challengeId ?? null
431+ const pendingStatus = existingChallenge
432+ ? status
433+ : await postDaemonAuthDevice ( daemonBaseUrl , {
434+ endpoint : args . endpoint ,
435+ } )
436+ if ( pendingStatus ?. status === 'ready' ) {
437+ persistDaemonCredentials ( '' , pendingStatus . expiresAt ?? null , pendingStatus . context ?? null )
438+ console . log ( '✅ PowerSync daemon authenticated via Supabase.' )
439+ if ( pendingStatus . expiresAt ) {
440+ console . log ( ` Expires: ${ pendingStatus . expiresAt } ` )
441+ }
442+ return
443+ }
444+ const challengeId = existingChallenge ?? extractDeviceChallenge ( pendingStatus ) ?. challengeId ?? null
445+ if ( ! challengeId ) {
446+ throw new Error ( 'Daemon did not provide a device code for login.' )
447+ }
448+
449+ const final = await completeDaemonDeviceLogin ( daemonBaseUrl , {
450+ challengeId,
451+ endpoint : args . endpoint ?? null ,
452+ session : {
453+ access_token : accessToken ,
454+ refresh_token : refreshToken ,
455+ expires_in : session ?. expires_in ?? null ,
456+ expires_at : session ?. expires_at ?? null ,
457+ } ,
458+ metadata : null ,
459+ } )
460+
461+ if ( ! final || final . status !== 'ready' ) {
462+ const refreshed = await fetchDaemonAuthStatus ( daemonBaseUrl )
463+ const resolved = refreshed ?? final
464+ if ( resolved ?. status === 'ready' ) {
465+ persistDaemonCredentials ( '' , resolved . expiresAt ?? null , resolved . context ?? null )
466+ console . log ( '✅ PowerSync daemon authenticated via Supabase.' )
467+ if ( resolved . expiresAt ) {
468+ console . log ( ` Expires: ${ resolved . expiresAt } ` )
469+ }
470+ return
471+ }
472+ const reason = resolved ?. reason ? ` (${ resolved . reason } )` : ''
473+ throw new Error ( `Daemon login did not complete successfully${ reason } .` )
474+ }
475+
476+ persistDaemonCredentials ( '' , final . expiresAt ?? null , final . context ?? null )
477+ console . log ( '✅ PowerSync daemon authenticated via Supabase.' )
478+ if ( final . expiresAt ) {
479+ console . log ( ` Expires: ${ final . expiresAt } ` )
480+ }
481+ return
482+ }
483+
350484 let observedChallenge : {
351485 challengeId ?: string | null
352486 verificationUrl ?: string | null
@@ -537,7 +671,7 @@ function printUsage() {
537671 console . log (
538672 ` ${ CLI_NAME } demo-seed [--remote-url <url>] [--remote <name>] [--branch <branch>] [--skip-sync] [--keep-repo] [--template-url <url>] [--no-template]` ,
539673 )
540- console . log ( ` ${ CLI_NAME } login [--endpoint <url>] [--daemon-url <url>]` )
674+ console . log ( ` ${ CLI_NAME } login [--endpoint <url>] [--daemon-url <url>] [--supabase-email <email>] [--supabase-password <password>] ` )
541675 console . log ( ` ${ CLI_NAME } daemon stop [--daemon-url <url>] [--wait <ms>]` )
542676 console . log ( ` ${ CLI_NAME } logout [--daemon-url <url>]` )
543677 console . log ( ` ${ CLI_NAME } profile list|show|set|use …` )
@@ -554,7 +688,7 @@ function buildCli() {
554688 ` ${ CLI_NAME } sync [--remote <name>]\n` +
555689 ` ${ CLI_NAME } org list|create|members …\n` +
556690 ` ${ CLI_NAME } demo-seed [--remote-url <url>] [--remote <name>] [--branch <branch>] [--skip-sync] [--keep-repo] [--template-url <url>] [--no-template]\n` +
557- ` ${ CLI_NAME } login [--endpoint <url>] [--daemon-url <url>]\n` +
691+ ` ${ CLI_NAME } login [--endpoint <url>] [--daemon-url <url>] [--supabase-email <email>] [--supabase-password <password>] \n` +
558692 ` ${ CLI_NAME } daemon stop [--daemon-url <url>] [--wait <ms>]\n` +
559693 ` ${ CLI_NAME } logout [--daemon-url <url>]\n` ,
560694 )
@@ -1067,6 +1201,22 @@ function buildCli() {
10671201 type : 'string' ,
10681202 describe : 'Legacy credential cache path' ,
10691203 } )
1204+ . option ( 'supabase-email' , {
1205+ type : 'string' ,
1206+ describe : 'Supabase login email (non-interactive).' ,
1207+ } )
1208+ . option ( 'supabase-password' , {
1209+ type : 'string' ,
1210+ describe : 'Supabase login password (non-interactive).' ,
1211+ } )
1212+ . option ( 'supabase-url' , {
1213+ type : 'string' ,
1214+ describe : 'Supabase URL override for password login (optional).' ,
1215+ } )
1216+ . option ( 'supabase-anon-key' , {
1217+ type : 'string' ,
1218+ describe : 'Supabase anon key override for password login (optional).' ,
1219+ } )
10701220 . option ( 'daemon-url' , {
10711221 type : 'string' ,
10721222 describe : 'PowerSync daemon base URL' ,
@@ -1076,6 +1226,10 @@ function buildCli() {
10761226 endpoint : argv . endpoint as string | undefined ,
10771227 session : argv . session as string | undefined ,
10781228 daemonUrl : argv [ 'daemon-url' ] as string | undefined ,
1229+ supabaseEmail : argv [ 'supabase-email' ] as string | undefined ,
1230+ supabasePassword : argv [ 'supabase-password' ] as string | undefined ,
1231+ supabaseUrl : argv [ 'supabase-url' ] as string | undefined ,
1232+ supabaseAnonKey : argv [ 'supabase-anon-key' ] as string | undefined ,
10791233 } )
10801234 } ,
10811235 )
0 commit comments