diff --git a/apps/backend/core/worktree.py b/apps/backend/core/worktree.py index 55a3a79e0e..22f727f4bf 100644 --- a/apps/backend/core/worktree.py +++ b/apps/backend/core/worktree.py @@ -39,6 +39,11 @@ T = TypeVar("T") +# Detects an existing `ignore-workspace=...` directive in a .npmrc, regardless +# of leading whitespace. Used to decide whether to append it when the host repo +# already commits a .npmrc (registry/auth config) into worktrees. +_IGNORE_WORKSPACE_PATTERN = re.compile(r"^\s*ignore-workspace\s*=", re.MULTILINE) + def _is_retryable_network_error(stderr: str) -> bool: """Check if an error is a retryable network/connection issue.""" @@ -733,6 +738,31 @@ def create_worktree(self, spec_name: str) -> WorktreeInfo: print(f"Created worktree: {worktree_path.name} on branch {branch_name}") + # Write .npmrc to prevent pnpm/npm from walking up to the parent + # workspace root (pnpm-workspace.yaml / package.json#workspaces). + # Without this, running `pnpm install` inside the worktree silently + # re-links the parent repo's node_modules into the worktree's pnpm + # store, corrupting the parent's dependency graph after cleanup. + # See: https://pnpm.io/npmrc#ignore-workspace + # If the host repo already commits a .npmrc (registry/auth config), we + # append the directive instead of skipping — otherwise the workspace + # fix gets bypassed silently for monorepos that need it most. + npmrc_path = worktree_path / ".npmrc" + try: + existing = npmrc_path.read_text(encoding="utf-8") if npmrc_path.exists() else "" + if not _IGNORE_WORKSPACE_PATTERN.search(existing): + separator = "" if not existing or existing.endswith("\n") else "\n" + npmrc_path.write_text( + existing + separator + "ignore-workspace=true\n", + encoding="utf-8", + ) + logger.debug( + "Ensured ignore-workspace=true in .npmrc for worktree: %s", + worktree_path, + ) + except OSError as e: + logger.warning("Could not write .npmrc to worktree %s: %s", worktree_path, e) + return WorktreeInfo( path=worktree_path, branch=branch_name, diff --git a/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts b/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts index b241fba7f6..cfc98af5d0 100644 --- a/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts @@ -834,6 +834,27 @@ async function createTerminalWorktree( debugLog('[TerminalWorktree] Created worktree in detached HEAD mode from', baseRef); } + // Write .npmrc to prevent pnpm/npm from walking up to the parent + // workspace root (pnpm-workspace.yaml / package.json#workspaces). + // Without this, running `pnpm install` inside the worktree silently + // re-links the parent repo's node_modules into the worktree's pnpm + // store, corrupting the parent's dependency graph after cleanup. + // See: https://pnpm.io/npmrc#ignore-workspace + // If the host repo already commits a .npmrc (registry/auth config), we + // append the directive instead of skipping — otherwise the workspace + // fix gets bypassed silently for monorepos that need it most. + const npmrcPath = path.join(worktreePath, '.npmrc'); + try { + const existing = existsSync(npmrcPath) ? readFileSync(npmrcPath, 'utf-8') : ''; + if (!/^\s*ignore-workspace\s*=/m.test(existing)) { + const separator = !existing || existing.endsWith('\n') ? '' : '\n'; + writeFileSync(npmrcPath, existing + separator + 'ignore-workspace=true\n', 'utf-8'); + debugLog('[TerminalWorktree] Ensured ignore-workspace=true in .npmrc for worktree:', worktreePath); + } + } catch (npmrcError) { + debugError('[TerminalWorktree] Could not write .npmrc to worktree:', npmrcError); + } + // Set up dependencies (node_modules, venvs, etc.) for tooling support // This allows pre-commit hooks to run typecheck without npm install in worktree const setupDeps = await setupWorktreeDependencies(projectPath, worktreePath);