Skip to content

Commit 7199c91

Browse files
khaliqgantclaude
andcommitted
fix: ensure packages are built before npm pack in verify-publish workflow
The verify-publish workflow was packing without building, causing global installs to fail with ERR_MODULE_NOT_FOUND for workspace packages. Changes: - Remove --ignore-scripts from npm ci to allow postinstall to run - Add explicit build step before npm pack in PR verification - Add setupWorkspacePackageLinks() to postinstall.js as fallback for global installs where bundledDependencies may not properly resolve workspace symlinks Fixes global install error: Cannot find package '@agent-relay/daemon' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4cdfd3b commit 7199c91

File tree

2 files changed

+157
-4
lines changed

2 files changed

+157
-4
lines changed

.github/workflows/verify-publish.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ jobs:
4343

4444
- name: Install deps for pack (PR only)
4545
if: github.event_name == 'pull_request'
46-
run: npm ci --ignore-scripts
46+
run: npm ci
47+
48+
- name: Build packages (PR only)
49+
if: github.event_name == 'pull_request'
50+
run: npm run build
4751

4852
- name: Pack local tarball (PR only)
4953
if: github.event_name == 'pull_request'
@@ -329,7 +333,11 @@ jobs:
329333

330334
- name: Install deps for pack (PR only)
331335
if: github.event_name == 'pull_request'
332-
run: npm ci --ignore-scripts
336+
run: npm ci
337+
338+
- name: Build packages (PR only)
339+
if: github.event_name == 'pull_request'
340+
run: npm run build
333341

334342
- name: Pack local tarball (PR only)
335343
if: github.event_name == 'pull_request'

scripts/postinstall.js

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
390525
async 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

Comments
 (0)