* Execute a command inside the devcontainer. * Overrides LocalBaseRuntime.exec() to use `devcontainer exec`.
(command: string, options: ExecOptions)
| 586 | * Overrides LocalBaseRuntime.exec() to use `devcontainer exec`. |
| 587 | */ |
| 588 | override exec(command: string, options: ExecOptions): Promise<ExecStream> { |
| 589 | const startTime = performance.now(); |
| 590 | |
| 591 | // Short-circuit if already aborted |
| 592 | if (options.abortSignal?.aborted) { |
| 593 | throw new RuntimeError("Operation aborted before execution", "exec"); |
| 594 | } |
| 595 | |
| 596 | // Build devcontainer exec args |
| 597 | const workspaceFolder = this.currentWorkspacePath; |
| 598 | if (!workspaceFolder) { |
| 599 | throw new RuntimeError("Devcontainer not initialized. Call ensureReady() first.", "exec"); |
| 600 | } |
| 601 | |
| 602 | const args = ["exec", "--workspace-folder", workspaceFolder]; |
| 603 | |
| 604 | if (this.configPath) { |
| 605 | args.push("--config", this.configPath); |
| 606 | } |
| 607 | |
| 608 | // Merge cached container credential env + caller env + non-interactive vars. |
| 609 | // Spread order: container env (lowest) < caller env < NON_INTERACTIVE (highest). |
| 610 | const envVars = { ...this.containerEnv, ...options.env, ...NON_INTERACTIVE_ENV_VARS }; |
| 611 | for (const [key, value] of Object.entries(envVars)) { |
| 612 | args.push("--remote-env", `${key}=${value}`); |
| 613 | } |
| 614 | |
| 615 | // Build the full command with cd |
| 616 | // Map host workspace path to container path; fall back to container workspace if unmappable |
| 617 | const mappedCwd = options.cwd ? this.mapHostPathToContainer(options.cwd) : null; |
| 618 | const cwd = mappedCwd ?? this.resolveContainerCwd(options.cwd, workspaceFolder); |
| 619 | const fullCommand = `cd ${shescape.quote(cwd)} && ${command}`; |
| 620 | args.push("--", "bash", "-c", fullCommand); |
| 621 | |
| 622 | const childProcess = spawn("devcontainer", args, { |
| 623 | stdio: ["pipe", "pipe", "pipe"], |
| 624 | detached: true, |
| 625 | windowsHide: true, |
| 626 | cwd: workspaceFolder, |
| 627 | }); |
| 628 | |
| 629 | const disposable = new DisposableProcess(childProcess); |
| 630 | |
| 631 | // Register cleanup to kill process tree and force-close stdio on timeout/abort. |
| 632 | disposable.addCleanup(() => { |
| 633 | if (childProcess.pid === undefined) return; |
| 634 | killProcessTree(childProcess.pid); |
| 635 | forceCloseStdio(childProcess); |
| 636 | }); |
| 637 | |
| 638 | // Convert Node.js streams to Web Streams (casts required for ExecStream compatibility) |
| 639 | /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ |
| 640 | const stdout = Readable.toWeb(childProcess.stdout!) as unknown as ReadableStream<Uint8Array>; |
| 641 | const stderr = Readable.toWeb(childProcess.stderr!) as unknown as ReadableStream<Uint8Array>; |
| 642 | const stdin = Writable.toWeb(childProcess.stdin!) as unknown as WritableStream<Uint8Array>; |
| 643 | /* eslint-enable @typescript-eslint/no-unnecessary-type-assertion */ |
| 644 | |
| 645 | let timedOut = false; |
no test coverage detected