Skip to content

Commit 821a695

Browse files
authored
Merge pull request #358 from AgentWorkforce/publish-edits
dashboard binary logic
2 parents 62d166f + 1615a91 commit 821a695

File tree

6 files changed

+170
-34
lines changed

6 files changed

+170
-34
lines changed

.github/workflows/publish.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,13 @@ jobs:
165165
- name: Build standalone binary
166166
run: |
167167
mkdir -p release-binaries
168+
# Exclude native modules - bun runtime has built-in bun:sqlite
168169
bun build \
169170
--compile \
170171
--minify \
171172
--target=${{ matrix.target }} \
173+
--external=better-sqlite3 \
174+
--external=ssh2 \
172175
--define="process.env.AGENT_RELAY_VERSION=\"${{ steps.version.outputs.version }}\"" \
173176
./dist/src/cli/index.js \
174177
--outfile release-binaries/${{ matrix.binary_name }}

install.sh

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,89 @@ download_relay_pty() {
107107
fi
108108
}
109109

110+
# Download standalone dashboard-server binary
111+
download_dashboard_binary() {
112+
if [ "${AGENT_RELAY_NO_DASHBOARD}" = "true" ]; then
113+
info "Skipping dashboard installation (AGENT_RELAY_NO_DASHBOARD=true)"
114+
return 0
115+
fi
116+
117+
step "Downloading dashboard-server binary..."
118+
119+
local binary_name="relay-dashboard-server-${PLATFORM}"
120+
local compressed_url="https://github.com/$REPO_DASHBOARD/releases/latest/download/${binary_name}.gz"
121+
local uncompressed_url="https://github.com/$REPO_DASHBOARD/releases/latest/download/${binary_name}"
122+
local target_path="$BIN_DIR/relay-dashboard-server"
123+
local temp_file="/tmp/dashboard-download-$$"
124+
125+
mkdir -p "$BIN_DIR"
126+
127+
# Setup cleanup trap for temp files
128+
trap 'rm -f "${temp_file}.gz" "${temp_file}"' EXIT
129+
130+
# Try compressed binary first (faster download)
131+
if has_command gunzip; then
132+
info "Trying compressed dashboard binary..."
133+
134+
if curl -fsSL "$compressed_url" -o "${temp_file}.gz" 2>/dev/null; then
135+
# Check if we got a valid gzip file
136+
local is_gzip=false
137+
if has_command file; then
138+
file "${temp_file}.gz" 2>/dev/null | grep -q "gzip" && is_gzip=true
139+
else
140+
head -c 2 "${temp_file}.gz" 2>/dev/null | od -An -tx1 | grep -q "1f 8b" && is_gzip=true
141+
fi
142+
143+
if [ "$is_gzip" = true ]; then
144+
if gunzip -c "${temp_file}.gz" > "$target_path" 2>/dev/null; then
145+
rm -f "${temp_file}.gz"
146+
chmod +x "$target_path"
147+
148+
if "$target_path" --version &>/dev/null; then
149+
success "Downloaded standalone dashboard-server binary"
150+
trap - EXIT
151+
return 0
152+
else
153+
warn "Dashboard binary failed verification, trying uncompressed..."
154+
rm -f "$target_path"
155+
fi
156+
else
157+
rm -f "${temp_file}.gz" "$target_path"
158+
fi
159+
else
160+
rm -f "${temp_file}.gz"
161+
fi
162+
fi
163+
fi
164+
165+
# Fall back to uncompressed binary
166+
info "Trying uncompressed dashboard binary..."
167+
168+
if curl -fsSL "$uncompressed_url" -o "$target_path" 2>/dev/null; then
169+
local file_size
170+
file_size=$(stat -f%z "$target_path" 2>/dev/null || stat -c%s "$target_path" 2>/dev/null || echo "0")
171+
172+
if [ "$file_size" -gt 1000000 ]; then
173+
chmod +x "$target_path"
174+
175+
if "$target_path" --version &>/dev/null; then
176+
success "Downloaded standalone dashboard-server binary"
177+
trap - EXIT
178+
return 0
179+
else
180+
warn "Dashboard binary failed verification"
181+
rm -f "$target_path"
182+
fi
183+
else
184+
rm -f "$target_path"
185+
fi
186+
fi
187+
188+
trap - EXIT
189+
info "No standalone dashboard binary available for $PLATFORM"
190+
return 1
191+
}
192+
110193
# Check if a command exists
111194
has_command() {
112195
command -v "$1" &> /dev/null
@@ -286,8 +369,11 @@ Or use nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/instal
286369

287370
# Install dashboard if not skipped
288371
if [ "${AGENT_RELAY_NO_DASHBOARD}" != "true" ]; then
289-
info "Installing dashboard..."
290-
npm install -g @agent-relay/dashboard-server 2>/dev/null || true
372+
# Try binary first, fall back to npm
373+
if ! download_dashboard_binary; then
374+
info "Installing dashboard via npm..."
375+
npm install -g @agent-relay/dashboard-server 2>/dev/null || true
376+
fi
291377
fi
292378

293379
success "Installed via npm"
@@ -419,6 +505,8 @@ main() {
419505
if download_standalone_binary; then
420506
# Also download relay-pty binary if available
421507
download_relay_pty || true
508+
# Download dashboard-server binary if available
509+
download_dashboard_binary || true
422510
verify_installation && print_usage && exit 0
423511
fi
424512

src/cli/commands/doctor.test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
66
let tempRoot: string;
77
let dataDir: string;
88
let storageConfig: { type?: string; path?: string };
9-
let betterAvailable = true;
10-
let nodeAvailable = true;
119
let mockStore: Map<string, string>;
1210

1311
// Store availability in an object to ensure closure works correctly across module resets
@@ -152,8 +150,6 @@ beforeEach(() => {
152150
path: path.join(dataDir, 'messages.sqlite'),
153151
};
154152
mockStore = new Map<string, string>();
155-
betterAvailable = true;
156-
nodeAvailable = true;
157153
mockAvailability.betterAvailable = true;
158154
mockAvailability.nodeAvailable = true;
159155
process.env.AGENT_RELAY_DOCTOR_NODE_VERSION = '22.1.0';
@@ -202,8 +198,6 @@ describe('doctor diagnostics', () => {
202198
});
203199

204200
it('fails gracefully when no SQLite drivers are available', async () => {
205-
betterAvailable = false;
206-
nodeAvailable = false;
207201
mockAvailability.betterAvailable = false;
208202
mockAvailability.nodeAvailable = false;
209203
process.env.AGENT_RELAY_DOCTOR_NODE_SQLITE_AVAILABLE = '0';

src/cli/commands/doctor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ async function checkBetterSqlite3(): Promise<CheckResult> {
104104
ok: true,
105105
message: `Available (v${version})`,
106106
};
107-
} catch (err: any) {
107+
} catch {
108108
return {
109109
name: 'better-sqlite3',
110110
ok: false,
@@ -152,7 +152,7 @@ async function checkNodeSqlite(): Promise<CheckResult> {
152152
ok: true,
153153
message: `Available (Node ${nodeVersion.raw})`,
154154
};
155-
} catch (err: any) {
155+
} catch {
156156
return {
157157
name: 'node:sqlite',
158158
ok: false,
@@ -267,7 +267,7 @@ async function checkDbPermissions(
267267
ok: true,
268268
message: `${displayPath} (${mode}${sizeDisplay})`,
269269
};
270-
} catch (err) {
270+
} catch {
271271
return {
272272
name: 'Database file',
273273
ok: false,
@@ -368,7 +368,7 @@ async function checkWriteTest(
368368
ok: true,
369369
message: 'OK',
370370
};
371-
} catch (err: any) {
371+
} catch {
372372
db?.close?.();
373373
return {
374374
name: 'Write test',
@@ -434,7 +434,7 @@ async function checkReadTest(
434434
ok: true,
435435
message: 'OK',
436436
};
437-
} catch (err: any) {
437+
} catch {
438438
db?.close?.();
439439
return {
440440
name: 'Read test',

src/cli/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, beforeAll, afterAll, vi, beforeEach, afterEach } from 'vitest';
1+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
22
import { exec } from 'node:child_process';
33
import { promisify } from 'node:util';
44
import path from 'node:path';

src/cli/index.ts

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { createStorageAdapter } from '@agent-relay/storage/adapter';
2626
import {
2727
initTelemetry,
2828
track,
29-
isTelemetryEnabled,
3029
enableTelemetry,
3130
disableTelemetry,
3231
getStatus,
@@ -37,12 +36,53 @@ import fs from 'node:fs';
3736
import path from 'node:path';
3837
import readline from 'node:readline';
3938
import { 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';
4140
import { 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
*/
4888
function 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

Comments
 (0)