Add device pairing command and voice mute toggle

Introduce `paseo daemon pair` CLI command that generates a local pairing offer URL with QR code without requiring a running daemon. Update message input to support mute/unmute toggle during realtime voice sessions with visual feedback (MicOff icon). Fix android:prod build script to use prebuild + assembleRelease flow.
This commit is contained in:
Mohamed Boudra
2026-02-07 15:56:14 +07:00
parent 902a415911
commit f9ebf6b523
8 changed files with 164 additions and 21 deletions
@@ -3,12 +3,14 @@ import { startCommand } from './start.js'
import { runStatusCommand } from './status.js'
import { runStopCommand } from './stop.js'
import { runRestartCommand } from './restart.js'
import { pairCommand } from './pair.js'
import { withOutput } from '../../output/index.js'
export function createDaemonCommand(): Command {
const daemon = new Command('daemon').description('Manage the Paseo daemon')
daemon.addCommand(startCommand())
daemon.addCommand(pairCommand())
daemon
.command('status')
+42
View File
@@ -0,0 +1,42 @@
import { Command } from 'commander'
import chalk from 'chalk'
import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from '@getpaseo/server'
interface PairOptions {
home?: string
}
export function pairCommand(): Command {
return new Command('pair')
.description('Print the daemon pairing QR code and link')
.option('--home <path>', 'Paseo home directory (default: ~/.paseo)')
.action(async (options: PairOptions) => {
await runPairCommand(options)
})
}
export async function runPairCommand(options: PairOptions): Promise<void> {
if (options.home) {
process.env.PASEO_HOME = options.home
}
const paseoHome = resolvePaseoHome()
const config = loadConfig(paseoHome)
const pairing = await generateLocalPairingOffer({
paseoHome,
relayEnabled: config.relayEnabled,
relayEndpoint: config.relayEndpoint,
relayPublicEndpoint: config.relayPublicEndpoint,
appBaseUrl: config.appBaseUrl,
includeQr: true,
})
if (!pairing.relayEnabled || !pairing.url) {
console.error(chalk.red('Relay pairing is disabled for this daemon config.'))
console.error(chalk.yellow('Enable relay and run this command again.'))
process.exit(1)
}
const qrBlock = pairing.qr ? `${pairing.qr}\n` : ''
process.stdout.write(`\nScan to pair:\n${qrBlock}${pairing.url}\n`)
}
+21 -8
View File
@@ -8,6 +8,7 @@
*
* Tests:
* - daemon --help shows subcommands
* - daemon pair prints a local pairing link without requiring a running daemon
* - daemon status fails gracefully when daemon not running
* - daemon status --json outputs valid JSON (even for errors)
* - daemon stop handles daemon not running gracefully
@@ -37,12 +38,24 @@ try {
assert(result.stdout.includes('status'), 'help should mention status')
assert(result.stdout.includes('stop'), 'help should mention stop')
assert(result.stdout.includes('restart'), 'help should mention restart')
assert(result.stdout.includes('pair'), 'help should mention pair')
console.log('✓ daemon --help shows subcommands\n')
}
// Test 2: daemon status fails gracefully when daemon not running
// Test 2: daemon pair works without daemon process
{
console.log('Test 2: daemon status fails gracefully when not running')
console.log('Test 2: daemon pair prints local pairing URL')
const result =
await $`PASEO_HOME=${paseoHome} npx paseo daemon pair`.nothrow()
assert.strictEqual(result.exitCode, 0, 'daemon pair should succeed')
assert(result.stdout.includes('Scan to pair:'), 'output should include scan header')
assert(result.stdout.includes('#offer='), 'output should include pairing offer fragment')
console.log('✓ daemon pair prints local pairing URL\n')
}
// Test 3: daemon status fails gracefully when daemon not running
{
console.log('Test 3: daemon status fails gracefully when not running')
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo daemon status`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
@@ -56,9 +69,9 @@ try {
console.log('✓ daemon status fails gracefully when not running\n')
}
// Test 3: daemon status --json outputs valid JSON (even for errors)
// Test 4: daemon status --json outputs valid JSON (even for errors)
{
console.log('Test 3: daemon status --json outputs JSON')
console.log('Test 4: daemon status --json outputs JSON')
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo daemon status --json`.nothrow()
// Should still fail (daemon not running)
@@ -79,9 +92,9 @@ try {
}
}
// Test 4: daemon stop handles daemon not running gracefully
// Test 5: daemon stop handles daemon not running gracefully
{
console.log('Test 4: daemon stop handles daemon not running')
console.log('Test 5: daemon stop handles daemon not running')
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo daemon stop`.nothrow()
// Stop should succeed even if daemon not running (idempotent)
@@ -105,9 +118,9 @@ try {
}
}
// Test 5: daemon restart fails when daemon not running
// Test 6: daemon restart fails when daemon not running
{
console.log('Test 5: daemon restart fails when daemon not running')
console.log('Test 6: daemon restart fails when daemon not running')
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo daemon restart`.nothrow()
// Restart should fail when daemon not running (can't restart something that's not running)