@@ -26,7 +26,6 @@ import { createStorageAdapter } from '@agent-relay/storage/adapter';
2626import {
2727 initTelemetry ,
2828 track ,
29- isTelemetryEnabled ,
3029 enableTelemetry ,
3130 disableTelemetry ,
3231 getStatus ,
@@ -37,12 +36,53 @@ import fs from 'node:fs';
3736import path from 'node:path' ;
3837import readline from 'node:readline' ;
3938import { promisify } from 'node:util' ;
40- import { exec , spawn as spawnProcess } from 'node:child_process' ;
39+ import { exec , execSync , spawn as spawnProcess } from 'node:child_process' ;
4140import { fileURLToPath } from 'node:url' ;
4241
4342
4443/**
45- * Start dashboard via npx (downloads and runs if not installed).
44+ * Find the dashboard binary if installed as standalone.
45+ * Checks PATH and common installation locations.
46+ */
47+ function findDashboardBinary ( ) : string | null {
48+ const binaryName = 'relay-dashboard-server' ;
49+ const homeDir = process . env . HOME || process . env . USERPROFILE || '' ;
50+
51+ // Common locations to check
52+ const searchPaths = [
53+ // In PATH (using which/where)
54+ binaryName ,
55+ // Common installation directories
56+ path . join ( homeDir , '.local' , 'bin' , binaryName ) ,
57+ path . join ( homeDir , '.agent-relay' , 'bin' , binaryName ) ,
58+ '/usr/local/bin/' + binaryName ,
59+ ] ;
60+
61+ for ( const searchPath of searchPaths ) {
62+ try {
63+ // For absolute paths, check if file exists and is executable
64+ if ( path . isAbsolute ( searchPath ) ) {
65+ if ( fs . existsSync ( searchPath ) ) {
66+ fs . accessSync ( searchPath , fs . constants . X_OK ) ;
67+ return searchPath ;
68+ }
69+ } else {
70+ // For relative paths (just binary name), check if it's in PATH
71+ const result = execSync ( `which ${ searchPath } 2>/dev/null || where ${ searchPath } 2>nul` , { encoding : 'utf8' } ) . trim ( ) ;
72+ if ( result ) {
73+ return result . split ( '\n' ) [ 0 ] ; // Take first result
74+ }
75+ }
76+ } catch {
77+ // Continue to next path
78+ }
79+ }
80+
81+ return null ;
82+ }
83+
84+ /**
85+ * Start dashboard (prefers standalone binary, falls back to npx).
4686 * Returns the spawned child process, port, and a promise that resolves when ready.
4787 */
4888function startDashboardViaNpx ( options : {
@@ -51,23 +91,34 @@ function startDashboardViaNpx(options: {
5191 teamDir : string ;
5292 projectRoot : string ;
5393} ) : { process: ReturnType < typeof spawnProcess > ; port: number ; ready: Promise < void > } {
54- console . log ( 'Starting dashboard via npx (this may take a moment on first run)...' ) ;
94+ const dashboardBinary = findDashboardBinary ( ) ;
5595
56- const dashboardProcess = spawnProcess ( 'npx' , [
57- '--yes' ,
58- '@agent-relay/dashboard-server' ,
96+ let dashboardProcess : ReturnType < typeof spawnProcess > ;
97+ const args = [
5998 '--integrated' ,
6099 '--port' , String ( options . port ) ,
61100 '--data-dir' , options . dataDir ,
62101 '--team-dir' , options . teamDir ,
63102 '--project-root' , options . projectRoot ,
64- ] , {
65- stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
66- env : {
67- ...process . env ,
68- // Pass any additional env vars needed
69- } ,
70- } ) ;
103+ ] ;
104+
105+ if ( dashboardBinary ) {
106+ console . log ( `Starting dashboard using binary: ${ dashboardBinary } ` ) ;
107+ dashboardProcess = spawnProcess ( dashboardBinary , args , {
108+ stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
109+ env : { ...process . env } ,
110+ } ) ;
111+ } else {
112+ console . log ( 'Starting dashboard via npx (this may take a moment on first run)...' ) ;
113+ dashboardProcess = spawnProcess ( 'npx' , [
114+ '--yes' ,
115+ '@agent-relay/dashboard-server' ,
116+ ...args ,
117+ ] , {
118+ stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
119+ env : { ...process . env } ,
120+ } ) ;
121+ }
71122
72123 // Promise that resolves when dashboard is ready (or after timeout)
73124 let resolveReady : ( ) => void ;
@@ -2349,13 +2400,13 @@ program
23492400
23502401 // Try daemon socket first (preferred path)
23512402 try {
2352- const paths = getProjectPaths ( ) ;
2403+ const _paths = getProjectPaths ( ) ;
23532404
23542405 // TODO: Re-enable daemon-based spawning when client.spawn() is implemented
23552406 // See: docs/SDK-MIGRATION-PLAN.md for planned implementation
23562407 // For now, fall through to HTTP API
23572408 throw new Error ( 'Daemon-based spawn not yet implemented' ) ;
2358- } catch ( daemonErr ) {
2409+ } catch {
23592410 // Fall through to HTTP API
23602411 // console.log('Daemon not available, trying HTTP API...');
23612412 }
@@ -2404,10 +2455,10 @@ program
24042455
24052456 // Try daemon socket first (preferred path)
24062457 try {
2407- const paths = getProjectPaths ( ) ;
2458+ const _paths = getProjectPaths ( ) ;
24082459
2409- const client = new RelayClient ( {
2410- socketPath : paths . socketPath ,
2460+ const _client = new RelayClient ( {
2461+ socketPath : _paths . socketPath ,
24112462 agentName : '__cli_releaser__' ,
24122463 quiet : true ,
24132464 reconnect : false ,
@@ -2420,7 +2471,7 @@ program
24202471 // See: docs/SDK-MIGRATION-PLAN.md for planned implementation
24212472 // For now, fall through to HTTP API
24222473 throw new Error ( 'Daemon-based release not yet implemented' ) ;
2423- } catch ( daemonErr ) {
2474+ } catch {
24242475 // Fall through to HTTP API
24252476 // console.log('Daemon not available, trying HTTP API...');
24262477 }
0 commit comments