agent-relay 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,17 +5,34 @@
5
5
  * - packages/bridge/src/spawner.ts (AgentSpawner)
6
6
  * - packages/wrapper/src/relay-pty-orchestrator.ts (RelayPtyOrchestrator)
7
7
  *
8
- * The search order handles multiple installation scenarios:
9
- * 1. Development (local Rust build)
10
- * 2. Local npm install (node_modules/agent-relay)
11
- * 3. Global npm install via nvm
12
- * 4. System-wide installs (/usr/local/bin)
8
+ * Supports all installation scenarios:
9
+ * - npx agent-relay (no postinstall, uses platform-specific binary)
10
+ * - npm install -g agent-relay (nvm, volta, fnm, n, asdf, Homebrew, system)
11
+ * - npm install agent-relay (local project)
12
+ * - pnpm/yarn global
13
+ * - Development (monorepo with Rust builds)
14
+ * - Docker containers
13
15
  */
14
16
 
15
17
  import fs from 'node:fs';
16
18
  import os from 'node:os';
17
19
  import path from 'node:path';
18
20
 
21
+ /**
22
+ * Supported platforms and their binary names.
23
+ * Windows is not supported (relay-pty requires PTY which doesn't work on Windows).
24
+ */
25
+ const SUPPORTED_PLATFORMS: Record<string, Record<string, string>> = {
26
+ darwin: {
27
+ arm64: 'relay-pty-darwin-arm64',
28
+ x64: 'relay-pty-darwin-x64',
29
+ },
30
+ linux: {
31
+ arm64: 'relay-pty-linux-arm64',
32
+ x64: 'relay-pty-linux-x64',
33
+ },
34
+ };
35
+
19
36
  /**
20
37
  * Get the platform-specific binary name for the current system.
21
38
  * Returns null if the platform is not supported.
@@ -24,13 +41,29 @@ function getPlatformBinaryName(): string | null {
24
41
  const platform = os.platform();
25
42
  const arch = os.arch();
26
43
 
27
- // Map to supported platforms
28
- if (platform === 'darwin' && arch === 'arm64') return 'relay-pty-darwin-arm64';
29
- if (platform === 'darwin' && arch === 'x64') return 'relay-pty-darwin-x64';
30
- if (platform === 'linux' && arch === 'arm64') return 'relay-pty-linux-arm64';
31
- if (platform === 'linux' && arch === 'x64') return 'relay-pty-linux-x64';
44
+ return SUPPORTED_PLATFORMS[platform]?.[arch] ?? null;
45
+ }
32
46
 
33
- return null;
47
+ /**
48
+ * Check if the current platform is supported.
49
+ */
50
+ export function isPlatformSupported(): boolean {
51
+ const platform = os.platform();
52
+ const arch = os.arch();
53
+ return SUPPORTED_PLATFORMS[platform]?.[arch] !== undefined;
54
+ }
55
+
56
+ /**
57
+ * Get a human-readable description of supported platforms.
58
+ */
59
+ export function getSupportedPlatforms(): string {
60
+ const platforms: string[] = [];
61
+ for (const [os, archs] of Object.entries(SUPPORTED_PLATFORMS)) {
62
+ for (const arch of Object.keys(archs)) {
63
+ platforms.push(`${os}-${arch}`);
64
+ }
65
+ }
66
+ return platforms.join(', ');
34
67
  }
35
68
 
36
69
  /** Cached result of relay-pty binary check */
@@ -51,13 +84,8 @@ export function getLastSearchPaths(): string[] {
51
84
  /**
52
85
  * Find the relay-pty binary.
53
86
  *
54
- * Search order:
55
- * 1. RELAY_PTY_BINARY environment variable (explicit override)
56
- * 2. bin/relay-pty in package root (installed by postinstall)
57
- * 3. relay-pty/target/release/relay-pty (local Rust build)
58
- * 4. /usr/local/bin/relay-pty (global install)
59
- * 5. In node_modules when installed as dependency
60
- * 6. Global npm installs (nvm) - both scoped and root packages
87
+ * Search order prioritizes platform-specific binaries FIRST because npx doesn't run postinstall.
88
+ * This ensures `npx agent-relay up` works without requiring global installation.
61
89
  *
62
90
  * @param callerDirname - The __dirname of the calling module (needed to resolve relative paths)
63
91
  * @returns Path to relay-pty binary, or null if not found
@@ -65,99 +93,149 @@ export function getLastSearchPaths(): string[] {
65
93
  export function findRelayPtyBinary(callerDirname: string): string | null {
66
94
  // Check for explicit environment variable override first
67
95
  const envOverride = process.env.RELAY_PTY_BINARY;
68
- if (envOverride && fs.existsSync(envOverride)) {
96
+ if (envOverride && isExecutable(envOverride)) {
69
97
  lastSearchPaths = [envOverride];
70
98
  return envOverride;
71
99
  }
72
- // Determine the agent-relay package root
73
- // This code runs from either:
74
- // - packages/{package}/dist/ (development/workspace)
75
- // - node_modules/@agent-relay/{package}/dist/ (npm install)
76
- //
77
- // We need to find the agent-relay package root where bin/relay-pty lives
78
- let packageRoot: string;
79
-
80
- // Check if we're inside node_modules/@agent-relay/*/
81
- if (callerDirname.includes('node_modules/@agent-relay/')) {
82
- // Go from node_modules/@agent-relay/{package}/dist/ to agent-relay/
83
- // dist/ -> {package}/ -> @agent-relay/ -> node_modules/ -> agent-relay/
84
- packageRoot = path.join(callerDirname, '..', '..', '..', '..');
85
- } else if (callerDirname.includes('node_modules/agent-relay')) {
86
- // Direct dependency: node_modules/agent-relay/packages/{package}/dist/
87
- // dist/ -> {package}/ -> packages/ -> agent-relay/
88
- packageRoot = path.join(callerDirname, '..', '..', '..');
89
- } else {
90
- // Development: packages/{package}/dist/ -> packages/ -> project root
91
- packageRoot = path.join(callerDirname, '..', '..', '..');
100
+
101
+ // Get platform-specific binary name (critical for npx where postinstall doesn't run)
102
+ const platformBinary = getPlatformBinaryName();
103
+
104
+ // Normalize path separators for cross-platform regex matching
105
+ const normalizedCaller = callerDirname.replace(/\\/g, '/');
106
+
107
+ // Collect all possible package root locations
108
+ const packageRoots: string[] = [];
109
+
110
+ // Find node_modules root from caller path
111
+ // Matches: /path/to/node_modules/@agent-relay/bridge/dist/
112
+ // Or: /path/to/node_modules/agent-relay/dist/src/cli/
113
+ const scopedMatch = normalizedCaller.match(/^(.+?\/node_modules)\/@agent-relay\//);
114
+ const directMatch = normalizedCaller.match(/^(.+?\/node_modules\/agent-relay)/);
115
+
116
+ if (scopedMatch) {
117
+ // Running from @agent-relay/* package - binary is in sibling agent-relay package
118
+ packageRoots.push(path.join(scopedMatch[1], 'agent-relay'));
92
119
  }
93
120
 
94
- // Find the node_modules root for global installs
95
- // When running from node_modules/@agent-relay/dashboard/node_modules/@agent-relay/wrapper/dist/
96
- // we need to look for agent-relay at node_modules/agent-relay
97
- // Use non-greedy match (.+?) to get the FIRST node_modules, not the last
98
- let nodeModulesRoot: string | null = null;
99
- const nodeModulesMatch = callerDirname.match(/^(.+?\/node_modules)\/@agent-relay\//);
100
- if (nodeModulesMatch) {
101
- nodeModulesRoot = nodeModulesMatch[1];
121
+ if (directMatch) {
122
+ // Running from agent-relay package directly
123
+ packageRoots.push(directMatch[1]);
102
124
  }
103
125
 
104
- // Get platform-specific binary name as fallback (in case postinstall didn't run)
105
- const platformBinary = getPlatformBinaryName();
126
+ // Development: packages/{package}/dist/ -> project root
127
+ if (!normalizedCaller.includes('node_modules')) {
128
+ packageRoots.push(path.join(callerDirname, '..', '..', '..'));
129
+ }
106
130
 
107
- const candidates = [
108
- // Primary: installed by postinstall from platform-specific binary
109
- path.join(packageRoot, 'bin', 'relay-pty'),
110
- // Development: local Rust build
111
- path.join(packageRoot, 'relay-pty', 'target', 'release', 'relay-pty'),
112
- path.join(packageRoot, 'relay-pty', 'target', 'debug', 'relay-pty'),
113
- // Local build in cwd (for development)
114
- path.join(process.cwd(), 'relay-pty', 'target', 'release', 'relay-pty'),
115
- // Docker container (CI tests)
116
- '/app/bin/relay-pty',
117
- // Installed globally
118
- '/usr/local/bin/relay-pty',
119
- // In node_modules (when installed as local dependency)
120
- path.join(process.cwd(), 'node_modules', 'agent-relay', 'bin', 'relay-pty'),
121
- // Global npm install (nvm) - root package
122
- path.join(process.env.HOME || '', '.nvm', 'versions', 'node', process.version, 'lib', 'node_modules', 'agent-relay', 'bin', 'relay-pty'),
123
- ];
124
-
125
- // Add candidate for root agent-relay package when running from scoped @agent-relay/* packages
126
- if (nodeModulesRoot) {
127
- candidates.push(path.join(nodeModulesRoot, 'agent-relay', 'bin', 'relay-pty'));
131
+ const home = process.env.HOME || process.env.USERPROFILE || '';
132
+
133
+ // npx cache locations - npm stores packages here when running via npx
134
+ if (home) {
135
+ const npxCacheBase = path.join(home, '.npm', '_npx');
136
+ if (fs.existsSync(npxCacheBase)) {
137
+ try {
138
+ const entries = fs.readdirSync(npxCacheBase);
139
+ for (const entry of entries) {
140
+ const npxPackage = path.join(npxCacheBase, entry, 'node_modules', 'agent-relay');
141
+ if (fs.existsSync(npxPackage)) {
142
+ packageRoots.push(npxPackage);
143
+ }
144
+ }
145
+ } catch {
146
+ // Ignore read errors
147
+ }
148
+ }
128
149
  }
129
150
 
130
- // Try common global npm paths
131
- if (process.env.HOME) {
132
- // Homebrew npm (macOS)
133
- candidates.push(path.join('/usr/local/lib/node_modules', 'agent-relay', 'bin', 'relay-pty'));
134
- candidates.push(path.join('/opt/homebrew/lib/node_modules', 'agent-relay', 'bin', 'relay-pty'));
151
+ // Add cwd-based paths for local installs
152
+ packageRoots.push(path.join(process.cwd(), 'node_modules', 'agent-relay'));
153
+
154
+ // Global install locations - support ALL major Node version managers
155
+ if (home) {
156
+ // nvm (most common)
157
+ packageRoots.push(
158
+ path.join(home, '.nvm', 'versions', 'node', process.version, 'lib', 'node_modules', 'agent-relay')
159
+ );
160
+
161
+ // volta (increasingly popular)
162
+ packageRoots.push(
163
+ path.join(home, '.volta', 'tools', 'image', 'packages', 'agent-relay', 'lib', 'node_modules', 'agent-relay')
164
+ );
165
+
166
+ // fnm (fast Node manager)
167
+ packageRoots.push(
168
+ path.join(home, '.fnm', 'node-versions', process.version, 'installation', 'lib', 'node_modules', 'agent-relay')
169
+ );
170
+
171
+ // n (simple Node version manager)
172
+ packageRoots.push(
173
+ path.join(home, 'n', 'lib', 'node_modules', 'agent-relay')
174
+ );
175
+
176
+ // asdf (universal version manager)
177
+ packageRoots.push(
178
+ path.join(home, '.asdf', 'installs', 'nodejs', process.version.replace('v', ''), 'lib', 'node_modules', 'agent-relay')
179
+ );
180
+
135
181
  // pnpm global
136
- candidates.push(path.join(process.env.HOME, '.local', 'share', 'pnpm', 'global', 'node_modules', 'agent-relay', 'bin', 'relay-pty'));
182
+ packageRoots.push(
183
+ path.join(home, '.local', 'share', 'pnpm', 'global', 'node_modules', 'agent-relay')
184
+ );
185
+
186
+ // yarn global (yarn 1.x)
187
+ packageRoots.push(
188
+ path.join(home, '.config', 'yarn', 'global', 'node_modules', 'agent-relay')
189
+ );
190
+
191
+ // yarn global (alternative location)
192
+ packageRoots.push(
193
+ path.join(home, '.yarn', 'global', 'node_modules', 'agent-relay')
194
+ );
137
195
  }
138
196
 
139
- // FALLBACK: If postinstall didn't run, try platform-specific binaries directly
140
- // This handles cases where better-sqlite3 rebuild failed and blocked postinstall.js
141
- if (platformBinary) {
142
- // Add platform-specific binary candidates for all the same locations
143
- candidates.push(path.join(packageRoot, 'bin', platformBinary));
144
- candidates.push(path.join(process.cwd(), 'node_modules', 'agent-relay', 'bin', platformBinary));
145
- candidates.push(path.join(process.env.HOME || '', '.nvm', 'versions', 'node', process.version, 'lib', 'node_modules', 'agent-relay', 'bin', platformBinary));
146
- if (nodeModulesRoot) {
147
- candidates.push(path.join(nodeModulesRoot, 'agent-relay', 'bin', platformBinary));
148
- }
149
- if (process.env.HOME) {
150
- candidates.push(path.join('/usr/local/lib/node_modules', 'agent-relay', 'bin', platformBinary));
151
- candidates.push(path.join('/opt/homebrew/lib/node_modules', 'agent-relay', 'bin', platformBinary));
152
- candidates.push(path.join(process.env.HOME, '.local', 'share', 'pnpm', 'global', 'node_modules', 'agent-relay', 'bin', platformBinary));
197
+ // Homebrew npm (macOS)
198
+ packageRoots.push('/usr/local/lib/node_modules/agent-relay');
199
+ packageRoots.push('/opt/homebrew/lib/node_modules/agent-relay');
200
+
201
+ // Linux system-wide npm
202
+ packageRoots.push('/usr/lib/node_modules/agent-relay');
203
+
204
+ // Build candidates list - PRIORITIZE platform-specific binaries
205
+ // This is critical for npx since postinstall doesn't run
206
+ const candidates: string[] = [];
207
+
208
+ for (const root of packageRoots) {
209
+ // Platform-specific binary FIRST (works without postinstall)
210
+ if (platformBinary) {
211
+ candidates.push(path.join(root, 'bin', platformBinary));
153
212
  }
213
+ // Generic binary (requires postinstall to have run)
214
+ candidates.push(path.join(root, 'bin', 'relay-pty'));
215
+ }
216
+
217
+ // Development: local Rust builds
218
+ const devRoot = normalizedCaller.includes('node_modules')
219
+ ? null
220
+ : path.join(callerDirname, '..', '..', '..');
221
+ if (devRoot) {
222
+ candidates.push(path.join(devRoot, 'relay-pty', 'target', 'release', 'relay-pty'));
223
+ candidates.push(path.join(devRoot, 'relay-pty', 'target', 'debug', 'relay-pty'));
154
224
  }
225
+ candidates.push(path.join(process.cwd(), 'relay-pty', 'target', 'release', 'relay-pty'));
226
+
227
+ // Docker container (CI tests)
228
+ candidates.push('/app/bin/relay-pty');
229
+
230
+ // System-wide installs
231
+ candidates.push('/usr/local/bin/relay-pty');
232
+ candidates.push('/usr/bin/relay-pty');
155
233
 
156
234
  // Store search paths for debugging
157
235
  lastSearchPaths = candidates;
158
236
 
159
237
  for (const candidate of candidates) {
160
- if (fs.existsSync(candidate)) {
238
+ if (isExecutable(candidate)) {
161
239
  return candidate;
162
240
  }
163
241
  }
@@ -165,6 +243,19 @@ export function findRelayPtyBinary(callerDirname: string): string | null {
165
243
  return null;
166
244
  }
167
245
 
246
+ /**
247
+ * Check if a file exists and is executable.
248
+ */
249
+ function isExecutable(filePath: string): boolean {
250
+ try {
251
+ fs.accessSync(filePath, fs.constants.X_OK);
252
+ return true;
253
+ } catch {
254
+ // File doesn't exist or isn't executable
255
+ return false;
256
+ }
257
+ }
258
+
168
259
  /**
169
260
  * Check if relay-pty binary is available (cached).
170
261
  * Returns true if the binary exists, false otherwise.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/wrapper",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "CLI agent wrappers for Agent Relay - tmux, pty integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,11 +30,11 @@
30
30
  "clean": "rm -rf dist"
31
31
  },
32
32
  "dependencies": {
33
- "@agent-relay/api-types": "2.1.0",
34
- "@agent-relay/protocol": "2.1.0",
35
- "@agent-relay/config": "2.1.0",
36
- "@agent-relay/continuity": "2.1.0",
37
- "@agent-relay/resiliency": "2.1.0",
33
+ "@agent-relay/api-types": "2.1.1",
34
+ "@agent-relay/protocol": "2.1.1",
35
+ "@agent-relay/config": "2.1.1",
36
+ "@agent-relay/continuity": "2.1.1",
37
+ "@agent-relay/resiliency": "2.1.1",
38
38
  "zod": "^3.23.8"
39
39
  },
40
40
  "devDependencies": {