fix(bash): preserve configured shell behavior

This commit is contained in:
jb
2026-09-04 17:11:46 -04:00
parent 3cf354b74b
commit e1c5c55c15
4 changed files with 85 additions and 20 deletions
+4 -4
View File
@@ -4,15 +4,15 @@ Pi extension that adds an optional `cwd` argument to the built-in `bash` tool. A
The override delegates execution and rendering to Pi's built-in Bash implementation. Existing `command` and `timeout` calls remain compatible.
## Install
## Try from this checkout
```bash
pi install npm:@jbcode/pi-bash-cwd
pi -e ./extensions/bash-cwd/index.ts
```
Restart Pi or run `/reload` after installation. Pi warns that the extension replaces the built-in `bash` tool; that warning is expected.
Pi warns that the extension replaces the built-in `bash` tool; that warning is expected. The package is not published yet. After publication, install it with `pi install npm:@jbcode/pi-bash-cwd` and restart Pi or run `/reload`.
Use this package with Pi 0.84.x. Remove it after moving to a Pi release that honors `ctx.cwd` and can be imported by extensions without undeclared runtime dependencies.
Use this package with Pi 0.84.x. Remove it only after moving to a Pi release that exposes equivalent validated per-call `cwd` behavior and can be imported by extensions without undeclared runtime dependencies.
## Verify
+52 -4
View File
@@ -1,6 +1,13 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import {
access,
chmod,
mkdir,
mkdtemp,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
@@ -29,7 +36,7 @@ async function executeIn(
cwd?: string,
) {
const params = cwd === undefined ? { command } : { command, cwd };
return createContextualBashTool().execute(
return createContextualBashTool({}).execute(
"bash-cwd-test",
params,
undefined,
@@ -91,14 +98,55 @@ test("bash honors hidden-worktree and nested-package cwd arguments", async (t) =
await assert.rejects(access(sentinel));
});
test("bash preserves configured command prefixes and shell paths", async (t) => {
const directory = await mkdtemp(join(tmpdir(), "pi-bash-cwd-options-"));
t.after(() => rm(directory, { recursive: true, force: true }));
const shellMarker = join(directory, "custom-shell-used");
const shell = join(directory, "custom-shell");
await writeFile(
shell,
`#!/bin/sh\nprintf used > "${shellMarker}"\nexec /bin/bash "$@"\n`,
);
await chmod(shell, 0o755);
await mkdir(join(directory, ".pi"));
await writeFile(
join(directory, ".pi", "settings.json"),
JSON.stringify({
shellCommandPrefix: "printf prefix-",
shellPath: shell,
}),
);
const tool = createContextualBashTool();
const result = await tool.execute(
"bash-cwd-options-test",
{ command: "printf command", cwd: directory },
undefined,
undefined,
{
cwd: directory,
sessionManager: {
getSessionId: () => "bash-cwd-test-session",
getSessionFile: () => undefined,
},
isProjectTrusted: () => true,
} as ExtensionContext,
);
assert.equal(getOutput(result), "prefix-command");
await access(shellMarker);
});
test("bash exposes cwd without changing existing arguments", () => {
const schema = createContextualBashTool().parameters;
const schema = createContextualBashTool({}).parameters;
assert.deepEqual(Object.keys(schema.properties), ["command", "cwd", "timeout"]);
assert.deepEqual(schema.required, ["command"]);
});
test("bash fails before execution when Pi omits the tool context", async () => {
const tool = createContextualBashTool();
const tool = createContextualBashTool({});
await assert.rejects(
tool.execute(
"bash-cwd-test",
+19 -3
View File
@@ -3,6 +3,9 @@ import { resolve } from "node:path";
import {
createBashToolDefinition,
getAgentDir,
SettingsManager,
type BashToolOptions,
type ExtensionAPI,
type ExtensionContext,
} from "@earendil-works/pi-coding-agent";
@@ -52,8 +55,18 @@ async function resolveWorkingDirectory(
return cwd;
}
export function createContextualBashTool() {
const baseTool = createBashToolDefinition(process.cwd());
function loadBashOptions(ctx: ExtensionContext): BashToolOptions {
const settings = SettingsManager.create(ctx.cwd, getAgentDir(), {
projectTrusted: ctx.isProjectTrusted(),
});
return {
commandPrefix: settings.getShellCommandPrefix(),
shellPath: settings.getShellPath(),
};
}
export function createContextualBashTool(options?: BashToolOptions) {
const baseTool = createBashToolDefinition(process.cwd(), options);
return {
...baseTool,
@@ -72,7 +85,10 @@ export function createContextualBashTool() {
}
const cwd = await resolveWorkingDirectory(ctx.cwd, params.cwd);
const contextualTool = createBashToolDefinition(cwd);
const contextualTool = createBashToolDefinition(
cwd,
options ?? loadBashOptions(ctx),
);
return contextualTool.execute(
toolCallId,
{ command: params.command, timeout: params.timeout },