open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'hopper-ow52gn6c-login-remote-ux'

+74 -14
+49 -14
src/cmd/login.js
··· 3 3 4 4 import { createServer } from 'node:http'; 5 5 import { spawn } from 'node:child_process'; 6 + import { createInterface } from 'node:readline'; 6 7 import { loadConfig, saveConfig } from '../lib/config.js'; 7 8 import { createOAuthClient, createSessionStore, createStore } from '../lib/oauth.js'; 8 9 ··· 13 14 .argument('<handle>', 'Bluesky handle (e.g. alice.bsky.social)') 14 15 .option('-v, --verbose', 'Show discovery details') 15 16 .option('--reset', 'Force re-login even if credentials are valid') 17 + .option('--remote', 'Skip browser launch; prompt to paste callback URL (auto-detected over SSH)') 18 + .option('--browser <command>', 'Browser command to use (e.g. firefox)') 16 19 .action(async (handle, opts) => { 17 - const { verbose, reset } = opts; 20 + const { verbose, reset, remote, browser } = opts; 21 + const isRemote = remote || !!(process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.SSH_CLIENT); 18 22 handle = handle.replace(/^@/, ''); 19 23 20 24 if (!reset) { ··· 30 34 31 35 let server; 32 36 let timeout; 37 + let rl; 33 38 34 39 try { 35 40 let resolveCallback; ··· 83 88 console.log(`[verbose] Authorization URL: ${authUrl.toString()}`); 84 89 } 85 90 86 - const platform = process.platform; 87 - const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open'; 88 - const args = platform === 'win32' ? ['/c', 'start', authUrl.toString()] : [authUrl.toString()]; 91 + if (isRemote) { 92 + console.log("You're on a remote system. Open this URL in your local browser:"); 93 + console.log(` ${authUrl.toString()}\n`); 94 + } else { 95 + const platform = process.platform; 96 + const cmd = browser || (platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open'); 97 + const browserArgs = !browser && platform === 'win32' ? ['/c', 'start', authUrl.toString()] : [authUrl.toString()]; 98 + 99 + try { 100 + const child = spawn(cmd, browserArgs, { stdio: 'ignore', detached: true }); 101 + child.unref(); 102 + } catch { 103 + // Ignore browser-open failures and rely on printed URL. 104 + } 89 105 90 - try { 91 - const child = spawn(cmd, args, { stdio: 'ignore', detached: true }); 92 - child.unref(); 93 - } catch { 94 - // Ignore browser-open failures and rely on printed URL. 106 + console.log(`Open this URL in your browser:\n ${authUrl.toString()}\n`); 95 107 } 96 108 97 - console.log(`Open this URL in your browser:\n ${authUrl.toString()}\n`); 109 + if (isRemote) { 110 + rl = createInterface({ input: process.stdin, output: process.stdout }); 111 + rl.question('Paste the callback URL from your browser: ', (line) => { 112 + try { 113 + const url = new URL(line.trim()); 114 + const params = new URLSearchParams(url.searchParams); 115 + if (!callbackResolved) { 116 + callbackResolved = true; 117 + resolveCallback(params); 118 + } 119 + } catch { 120 + console.error('Invalid URL. Please paste the full callback URL.'); 121 + } 122 + }); 123 + } 98 124 99 125 const timeoutMs = 5 * 60 * 1000; 100 126 const timeoutPromise = new Promise((_, reject) => { ··· 107 133 108 134 clearTimeout(timeout); 109 135 timeout = undefined; 136 + server.closeAllConnections?.(); 137 + server.close(); 138 + if (rl) { 139 + rl.close(); 140 + } 110 141 111 142 if (verbose) { 112 143 console.log(`[verbose] Callback received with params: ${params.toString()}`); ··· 121 152 throw new Error(`OAuth error: ${oauthError}`); 122 153 } 123 154 155 + console.log('Exchanging token...'); 124 156 const { session } = await client.callback(params); 125 157 126 158 if (verbose) { 127 159 console.log(`[verbose] Token exchange result for DID: ${session.did}`); 128 160 } 129 161 130 - console.log(`DID: ${session.did}`); 131 - 132 162 const config = loadConfig(); 133 163 config.did = session.did; 134 164 saveConfig(config); 135 - console.log('Logged in'); 165 + console.log(`Logged in as ${session.did}`); 136 166 } catch (err) { 137 167 console.error(err instanceof Error ? err.message : String(err)); 138 168 process.exitCode = 1; ··· 141 171 clearTimeout(timeout); 142 172 } 143 173 144 - if (server) { 174 + if (rl) { 175 + rl.close(); 176 + } 177 + 178 + if (server?.listening) { 179 + server.closeAllConnections?.(); 145 180 server.close(); 146 181 } 147 182 }
+25
test/login.test.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { describe, test, expect } from 'bun:test'; 5 + import { run } from './helpers.js'; 6 + 7 + describe('login', () => { 8 + test('--help shows --remote and --browser options', () => { 9 + const { stdout, exitCode } = run('login --help'); 10 + expect(exitCode).toBe(0); 11 + expect(stdout).toContain('--remote'); 12 + expect(stdout).toContain('--browser'); 13 + }); 14 + 15 + test('--help shows --reset option', () => { 16 + const { stdout, exitCode } = run('login --help'); 17 + expect(exitCode).toBe(0); 18 + expect(stdout).toContain('--reset'); 19 + }); 20 + 21 + test('requires handle argument', () => { 22 + const result = run('login'); 23 + expect(result.exitCode).not.toBe(0); 24 + }); 25 + });