@@ -279,6 +279,132 @@ function hasSystemTmux() {
279279 }
280280}
281281
282+ /**
283+ * Setup workspace package symlinks for global/bundled installs.
284+ *
285+ * When agent-relay is installed globally (npm install -g), the workspace packages
286+ * are included in the tarball at packages/* but Node.js module resolution expects
287+ * them at node_modules/@agent-relay/*. This function creates symlinks to bridge
288+ * the gap.
289+ *
290+ * This is needed because npm's bundledDependencies doesn't properly handle
291+ * workspace packages (which are symlinks during development).
292+ */
293+ function setupWorkspacePackageLinks ( ) {
294+ const pkgRoot = getPackageRoot ( ) ;
295+ const packagesDir = path . join ( pkgRoot , 'packages' ) ;
296+ const nodeModulesDir = path . join ( pkgRoot , 'node_modules' ) ;
297+ const scopeDir = path . join ( nodeModulesDir , '@agent-relay' ) ;
298+
299+ // Check if packages/ exists (we're in a bundled/global install)
300+ if ( ! fs . existsSync ( packagesDir ) ) {
301+ // Not a bundled install, workspace packages should be in node_modules already
302+ return { needed : false } ;
303+ }
304+
305+ // Check if node_modules/@agent -relay/daemon exists
306+ const testPackage = path . join ( scopeDir , 'daemon' ) ;
307+ if ( fs . existsSync ( testPackage ) ) {
308+ // Already set up (either normal npm install or previously linked)
309+ info ( 'Workspace packages already available in node_modules' ) ;
310+ return { needed : false , alreadySetup : true } ;
311+ }
312+
313+ // We need to create symlinks
314+ info ( 'Setting up workspace package links for global install...' ) ;
315+
316+ // Create node_modules/@agent -relay/ directory
317+ try {
318+ fs . mkdirSync ( scopeDir , { recursive : true } ) ;
319+ } catch ( err ) {
320+ warn ( `Failed to create @agent-relay scope directory: ${ err . message } ` ) ;
321+ return { needed : true , success : false , error : err . message } ;
322+ }
323+
324+ // Map from package directory name to npm package name
325+ const packageDirs = fs . readdirSync ( packagesDir ) . filter ( dir => {
326+ const pkgJsonPath = path . join ( packagesDir , dir , 'package.json' ) ;
327+ return fs . existsSync ( pkgJsonPath ) ;
328+ } ) ;
329+
330+ let linked = 0 ;
331+ let failed = 0 ;
332+ const errors = [ ] ;
333+
334+ for ( const dir of packageDirs ) {
335+ const sourcePath = path . join ( packagesDir , dir ) ;
336+ const targetPath = path . join ( scopeDir , dir ) ;
337+
338+ // Skip if already exists
339+ if ( fs . existsSync ( targetPath ) ) {
340+ continue ;
341+ }
342+
343+ try {
344+ // Use relative symlink for portability
345+ const relativeSource = path . relative ( scopeDir , sourcePath ) ;
346+ fs . symlinkSync ( relativeSource , targetPath , 'dir' ) ;
347+ linked ++ ;
348+ } catch ( err ) {
349+ // If symlink fails (e.g., on Windows without admin), try copying
350+ try {
351+ // Copy the package directory
352+ copyDirSync ( sourcePath , targetPath ) ;
353+ linked ++ ;
354+ } catch ( copyErr ) {
355+ failed ++ ;
356+ errors . push ( `${ dir } : ${ copyErr . message } ` ) ;
357+ }
358+ }
359+ }
360+
361+ if ( linked > 0 ) {
362+ success ( `Linked ${ linked } workspace packages to node_modules/@agent-relay/` ) ;
363+ }
364+
365+ if ( failed > 0 ) {
366+ warn ( `Failed to link ${ failed } packages: ${ errors . join ( ', ' ) } ` ) ;
367+ return { needed : true , success : false , linked, failed, errors } ;
368+ }
369+
370+ return { needed : true , success : true , linked } ;
371+ }
372+
373+ /**
374+ * Recursively copy a directory
375+ */
376+ function copyDirSync ( src , dest ) {
377+ fs . mkdirSync ( dest , { recursive : true } ) ;
378+ const entries = fs . readdirSync ( src , { withFileTypes : true } ) ;
379+
380+ for ( const entry of entries ) {
381+ const srcPath = path . join ( src , entry . name ) ;
382+ const destPath = path . join ( dest , entry . name ) ;
383+
384+ // Skip node_modules in package copies
385+ if ( entry . name === 'node_modules' ) {
386+ continue ;
387+ }
388+
389+ if ( entry . isDirectory ( ) ) {
390+ copyDirSync ( srcPath , destPath ) ;
391+ } else if ( entry . isSymbolicLink ( ) ) {
392+ // Resolve symlink and copy the target
393+ const linkTarget = fs . readlinkSync ( srcPath ) ;
394+ const resolvedTarget = path . resolve ( path . dirname ( srcPath ) , linkTarget ) ;
395+ if ( fs . existsSync ( resolvedTarget ) ) {
396+ if ( fs . statSync ( resolvedTarget ) . isDirectory ( ) ) {
397+ copyDirSync ( resolvedTarget , destPath ) ;
398+ } else {
399+ fs . copyFileSync ( resolvedTarget , destPath ) ;
400+ }
401+ }
402+ } else {
403+ fs . copyFileSync ( srcPath , destPath ) ;
404+ }
405+ }
406+ }
407+
282408/**
283409 * Install dashboard dependencies
284410 */
@@ -362,7 +488,16 @@ function patchAgentTrajectories() {
362488 success ( 'Patched agent-trajectories to record agent on trail start' ) ;
363489}
364490
365- function logPostinstallDiagnostics ( hasRelayPty , sqliteStatus ) {
491+ function logPostinstallDiagnostics ( hasRelayPty , sqliteStatus , linkResult ) {
492+ // Workspace packages status (for global installs)
493+ if ( linkResult && linkResult . needed ) {
494+ if ( linkResult . success ) {
495+ console . log ( `✓ Workspace packages linked (${ linkResult . linked } packages)` ) ;
496+ } else {
497+ console . log ( '⚠ Workspace package linking failed - CLI may not work' ) ;
498+ }
499+ }
500+
366501 if ( hasRelayPty ) {
367502 console . log ( '✓ relay-pty binary installed' ) ;
368503 } else {
@@ -388,6 +523,16 @@ function logPostinstallDiagnostics(hasRelayPty, sqliteStatus) {
388523 * Main postinstall routine
389524 */
390525async function main ( ) {
526+ // Setup workspace package links for global installs
527+ // This MUST run first so that other postinstall steps can find the packages
528+ const linkResult = setupWorkspacePackageLinks ( ) ;
529+ if ( linkResult . needed && ! linkResult . success ) {
530+ warn ( 'Workspace package linking failed - CLI may not work correctly' ) ;
531+ if ( linkResult . errors ) {
532+ linkResult . errors . forEach ( e => warn ( ` ${ e } ` ) ) ;
533+ }
534+ }
535+
391536 // Install relay-pty binary for current platform (primary mode)
392537 const hasRelayPty = installRelayPtyBinary ( ) ;
393538
@@ -401,7 +546,7 @@ async function main() {
401546 installDashboardDeps ( ) ;
402547
403548 // Always print diagnostics (even in CI)
404- logPostinstallDiagnostics ( hasRelayPty , sqliteStatus ) ;
549+ logPostinstallDiagnostics ( hasRelayPty , sqliteStatus , linkResult ) ;
405550
406551 // Skip tmux check in CI environments
407552 if ( process . env . CI === 'true' ) {
0 commit comments