Skip to content

Commit 3a9f1de

Browse files
committed
fix: pass streamStdin through KernelInterface.spawn, add pnpm store to overlay roots, transitive dep resolution
1 parent bc69d0b commit 3a9f1de

6 files changed

Lines changed: 121 additions & 10 deletions

File tree

packages/core/src/kernel/kernel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,7 @@ class KernelImpl implements Kernel {
10681068
return this.spawnManaged(command, args, {
10691069
env: ctx.env,
10701070
cwd: ctx.cwd,
1071+
streamStdin: ctx.streamStdin,
10711072
onStdout: ctx.onStdout,
10721073
onStderr: ctx.onStderr,
10731074
stdinFd: ctx.stdinFd,

packages/core/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface CommandExecutor {
1818
options: {
1919
cwd?: string;
2020
env?: Record<string, string>;
21+
streamStdin?: boolean;
2122
onStdout?: (data: Uint8Array) => void;
2223
onStderr?: (data: Uint8Array) => void;
2324
},

packages/nodejs/src/bridge-handlers.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2855,6 +2855,8 @@ export interface ModuleResolutionBridgeDeps {
28552855
sandboxToHostPath: (sandboxPath: string) => string | null;
28562856
/** Translate host path back to sandbox path. */
28572857
hostToSandboxPath: (hostPath: string) => string;
2858+
/** Additional host directories used as require.resolve paths for transitive deps. */
2859+
additionalResolvePaths?: string[];
28582860
}
28592861

28602862
function normalizeModuleResolveContext(referrer: string): string {
@@ -3032,6 +3034,7 @@ export function buildModuleResolutionBridgeHandlers(
30323034

30333035
// Handle #-prefixed subpath imports (package.json "imports" field)
30343036
if (req.startsWith("#")) {
3037+
// Try host-side resolution first with the translated hostDir.
30353038
let resolved = resolvePackageImportSync(req, hostDir, resolveMode);
30363039
if (!resolved) {
30373040
// Fallback: try resolving from the realpath of hostDir.
@@ -3044,6 +3047,31 @@ export function buildModuleResolutionBridgeHandlers(
30443047
}
30453048
} catch { /* realpath failed, skip */ }
30463049
}
3050+
if (!resolved) {
3051+
// hostDir translation may have failed (e.g. transitive deps not
3052+
// in packageRoots). Try using require.resolve with additional
3053+
// resolve paths to find the owning package on the host.
3054+
const nmPrefix = "/root/node_modules/";
3055+
if (sandboxDir.startsWith(nmPrefix)) {
3056+
const rest = sandboxDir.slice(nmPrefix.length);
3057+
const parts = rest.split("/");
3058+
const pkgName = parts[0].startsWith("@")
3059+
? parts.slice(0, 2).join("/")
3060+
: parts[0];
3061+
const paths = deps.additionalResolvePaths ?? [];
3062+
for (const searchPath of paths) {
3063+
try {
3064+
const pkgJsonPath = hostRequire.resolve(
3065+
`${pkgName}/package.json`,
3066+
{ paths: [searchPath] },
3067+
);
3068+
const pkgDir = pathDirname(pkgJsonPath);
3069+
resolved = resolvePackageImportSync(req, pkgDir, resolveMode);
3070+
if (resolved) break;
3071+
} catch { /* not found in this path */ }
3072+
}
3073+
}
3074+
}
30473075
return resolved ? deps.hostToSandboxPath(resolved) : null;
30483076
}
30493077

@@ -3849,6 +3877,7 @@ export function buildChildProcessBridgeHandlers(deps: ChildProcessBridgeDeps): B
38493877
const proc = deps.commandExecutor.spawn(String(command), args, {
38503878
cwd: options.cwd,
38513879
env: childEnv,
3880+
streamStdin: true,
38523881
onStdout: (data) => dispatchEvent(sessionId, "stdout", data),
38533882
onStderr: (data) => dispatchEvent(sessionId, "stderr", data),
38543883
});

packages/nodejs/src/execution-driver.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,22 @@ export class NodeExecutionDriver implements RuntimeDriver {
11621162
const fs = s.filesystem as any;
11631163
return typeof fs.toSandboxPath === "function" ? fs.toSandboxPath(p) : p;
11641164
},
1165+
additionalResolvePaths: (() => {
1166+
// Collect host paths from package roots for transitive dep resolution.
1167+
// Each package root's parent node_modules is a valid search path.
1168+
const fs = s.filesystem as any;
1169+
const roots: string[] = [];
1170+
if (Array.isArray(fs?.packageRoots)) {
1171+
for (const root of fs.packageRoots) {
1172+
if (root.hostPath) roots.push(root.hostPath);
1173+
}
1174+
}
1175+
// Also include moduleAccessCwd's node_modules
1176+
if (fs?.hostNodeModulesRoot) {
1177+
roots.push(fs.hostNodeModulesRoot);
1178+
}
1179+
return roots.length > 0 ? roots : undefined;
1180+
})(),
11651181
}),
11661182
...buildPtyBridgeHandlers({
11671183
onPtySetRawMode: s.onPtySetRawMode,

packages/nodejs/src/kernel-runtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export function createKernelCommandExecutor(kernel: KernelInterface, parentPid:
219219
options: {
220220
cwd?: string;
221221
env?: Record<string, string>;
222+
streamStdin?: boolean;
222223
onStdout?: (data: Uint8Array) => void;
223224
onStderr?: (data: Uint8Array) => void;
224225
},
@@ -229,6 +230,7 @@ export function createKernelCommandExecutor(kernel: KernelInterface, parentPid:
229230
ppid: parentPid,
230231
env: options.env ?? {},
231232
cwd: options.cwd ?? kernel.getcwd(parentPid),
233+
streamStdin: options.streamStdin,
232234
onStdout: options.onStdout,
233235
onStderr: options.onStderr,
234236
});

packages/nodejs/src/module-access.ts

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ export class ModuleAccessFileSystem implements VirtualFileSystem {
225225
(a, b) => b.vmPath.length - a.vmPath.length,
226226
);
227227

228-
// Expand allowed roots to include package root host paths.
228+
// Expand allowed roots to include package root host paths and their
229+
// sibling node_modules (for transitive dep resolution in pnpm).
229230
for (const root of this.packageRoots) {
230231
try {
231232
const canonical = fsSync.realpathSync(root.hostPath);
@@ -242,6 +243,24 @@ export class ModuleAccessFileSystem implements VirtualFileSystem {
242243
}
243244
}
244245
}
246+
// Add the pnpm store root if the package is inside a .pnpm directory.
247+
// This makes all transitive deps in the store accessible.
248+
const canonicalParts = canonical.split(path.sep);
249+
const pnpmIdx = canonicalParts.indexOf(".pnpm");
250+
if (pnpmIdx >= 0) {
251+
const pnpmStoreRoot = canonicalParts.slice(0, pnpmIdx + 1).join(path.sep);
252+
if (!this.overlayAllowedRoots.includes(pnpmStoreRoot)) {
253+
this.overlayAllowedRoots.push(pnpmStoreRoot);
254+
}
255+
}
256+
// Also add the parent node_modules directory for non-pnpm setups.
257+
const parentNm = path.dirname(root.hostPath);
258+
try {
259+
const canonicalParent = fsSync.realpathSync(parentNm);
260+
if (!this.overlayAllowedRoots.includes(canonicalParent)) {
261+
this.overlayAllowedRoots.push(canonicalParent);
262+
}
263+
} catch { /* skip */ }
245264
} catch {
246265
// Package root doesn't exist on host; skip.
247266
}
@@ -310,19 +329,62 @@ export class ModuleAccessFileSystem implements VirtualFileSystem {
310329
}
311330

312331
// Fall back to CWD-based node_modules.
313-
if (!this.hostNodeModulesRoot) {
314-
return null;
315-
}
316-
if (virtualPath === SANDBOX_NODE_MODULES_ROOT) {
317-
return this.hostNodeModulesRoot;
332+
if (this.hostNodeModulesRoot) {
333+
if (virtualPath === SANDBOX_NODE_MODULES_ROOT) {
334+
return this.hostNodeModulesRoot;
335+
}
336+
const relative = path.posix
337+
.relative(SANDBOX_NODE_MODULES_ROOT, virtualPath)
338+
.replace(/^\/+/, "");
339+
if (!relative) {
340+
return this.hostNodeModulesRoot;
341+
}
342+
const candidate = path.join(this.hostNodeModulesRoot, ...relative.split("/"));
343+
if (fsSync.existsSync(candidate)) {
344+
return candidate;
345+
}
318346
}
319-
const relative = path.posix
347+
348+
// Fall back: resolve transitive dependencies from package root siblings.
349+
// In pnpm, each package root sits in a node_modules/ dir alongside
350+
// its transitive deps (e.g., .pnpm/pi-agent@.../node_modules/chalk).
351+
// Check each package root's sibling node_modules for the requested package.
352+
const nmRelative = path.posix
320353
.relative(SANDBOX_NODE_MODULES_ROOT, virtualPath)
321354
.replace(/^\/+/, "");
322-
if (!relative) {
323-
return this.hostNodeModulesRoot;
355+
if (nmRelative) {
356+
const relParts = nmRelative.split("/");
357+
const pkgName = relParts[0].startsWith("@")
358+
? relParts.slice(0, 2).join("/")
359+
: relParts[0];
360+
const subPath = relParts[0].startsWith("@")
361+
? relParts.slice(2).join("/")
362+
: relParts.slice(1).join("/");
363+
for (const root of this.packageRoots) {
364+
// root.hostPath is e.g. .../node_modules/@mariozechner/pi-coding-agent
365+
// Its parent node_modules dir may contain chalk, undici, etc.
366+
const siblingPkg = path.join(path.dirname(root.hostPath), pkgName);
367+
try {
368+
if (fsSync.existsSync(siblingPkg)) {
369+
const realPkg = fsSync.realpathSync(siblingPkg);
370+
return subPath ? path.join(realPkg, ...subPath.split("/")) : realPkg;
371+
}
372+
} catch { /* skip */ }
373+
// Also try the parent's parent for scoped packages
374+
const parentNm = path.dirname(path.dirname(root.hostPath));
375+
if (parentNm !== path.dirname(root.hostPath)) {
376+
const parentSibling = path.join(parentNm, pkgName);
377+
try {
378+
if (fsSync.existsSync(parentSibling)) {
379+
const realPkg = fsSync.realpathSync(parentSibling);
380+
return subPath ? path.join(realPkg, ...subPath.split("/")) : realPkg;
381+
}
382+
} catch { /* skip */ }
383+
}
384+
}
324385
}
325-
return path.join(this.hostNodeModulesRoot, ...relative.split("/"));
386+
387+
return null;
326388
}
327389

328390
private isProjectedHostPath(pathValue: string): boolean {

0 commit comments

Comments
 (0)