merge PR #5682 via eval/pr-5682: fix(tools): fix hub tools env selection

This commit is contained in:
can1357
2026-07-17 04:37:12 +02:00
3 changed files with 102 additions and 2 deletions
+1
View File
@@ -22,6 +22,7 @@
### Fixed
- Fixed `startup.quiet` still rendering the `xdev: xd://: mounted …` status line when MCP tools connect; quiet startup now suppresses only the user-visible mount notice while retaining the hidden model-facing device update ([#5670](https://github.com/can1357/oh-my-pi/issues/5670)).
- Fixed command error in `hub` tool with a non-POSIX shell ([#5682](https://github.com/can1357/oh-my-pi/pull/5682))
## [17.0.1] - 2026-07-16
+3 -2
View File
@@ -3,7 +3,7 @@ import * as net from "node:net";
import * as os from "node:os";
import * as path from "node:path";
import { Process, type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
import { isEexist, isEnoent, logger, postmortem, sanitizeText } from "@oh-my-pi/pi-utils";
import { isEexist, isEnoent, logger, postmortem, procmgr, sanitizeText } from "@oh-my-pi/pi-utils";
import { truncateHead, truncateHeadBytes, truncateTail, truncateTailBytes } from "../session/streaming-output";
import { workerEnvFromParent } from "../subprocess/worker-client";
import { daemonBrokerEndpoint } from "./paths";
@@ -555,7 +555,8 @@ class DaemonBroker {
`printf '%s' "$$" > ${quoteShellArg(pidPath)}`,
`exec ${argv.map(quoteShellArg).join(" ")}`,
].join("; ");
run = session.start({ command, shell: process.env.SHELL, ...options }, onChunk);
const shell = procmgr.getShellConfig().shell;
run = session.start({ command, shell, ...options }, onChunk);
}
void run
.then(result => this.#onPtyExit(record, generation, result))
@@ -42,6 +42,86 @@ async function shutdown(client: DaemonBrokerClient): Promise<void> {
client.close();
}
async function startPtyDaemonWithShell(shell: string, initialMarker: string, expectedMarker: string): Promise<void> {
const projectDir = await tempDir("omp-daemon-shell-project-");
const runtimeDir = await tempDir("omp-daemon-shell-runtime-");
const runner = `
import { createDaemonBrokerClient } from "./src/launch/client";
const projectDir = ${JSON.stringify(projectDir)};
const runtimeDir = ${JSON.stringify(runtimeDir)};
const expectedMarker = ${JSON.stringify(expectedMarker)};
const client = await createDaemonBrokerClient(projectDir, {
runtimeDir,
idleGraceMs: 5_000,
});
try {
const started = await client.request({
op: "start",
spec: {
name: "shell",
application: process.execPath,
args: [
"-e",
"process.stdout.write(process.env.OMP_TEST_SHELL_MARKER); process.stdout.write(String.fromCharCode(10)); process.stdin.resume();",
],
env: {},
cwd: projectDir,
pty: true,
ready: { log: expectedMarker, timeoutMs: 5_000 },
restart: "no",
persist: false,
detached: false,
},
owner: "shell-test",
});
if (started.op !== "start") throw new Error("unexpected start response");
if (started.daemon.state !== "ready") {
const logs = await client.request({
op: "logs",
name: "shell",
lines: 20,
head: false,
follow: false,
timeoutMs: 1_000,
});
throw new Error(
"daemon did not become ready: " +
(started.daemon.exitReason ?? "unknown error") +
"; logs: " +
(logs.op === "logs" ? logs.text : "unavailable"),
);
}
process.stdout.write(JSON.stringify({ state: started.daemon.state, readyTimedOut: started.readyTimedOut }));
await client.request({ op: "stop", name: "shell", timeoutMs: 2_000 });
} finally {
try {
await client.request({ op: "shutdown" });
} catch {
// A last-client shutdown may already have closed the broker.
}
client.close();
}
`;
const child = Bun.spawn([process.execPath, "--eval", runner], {
cwd: path.resolve(import.meta.dir, "../.."),
env: {
...process.env,
SHELL: shell,
OMP_TEST_SHELL_MARKER: initialMarker,
},
stdout: "pipe",
stderr: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
expect({ exitCode, stderr }).toEqual({ exitCode: 0, stderr: "" });
expect(JSON.parse(stdout)).toEqual({ state: "ready", readyTimedOut: false });
}
afterEach(async () => {
while (cleanupDirs.length > 0) {
const dir = cleanupDirs.pop();
@@ -133,6 +213,24 @@ setInterval(() => {}, 1000);
}
}, 20_000);
it("uses a basic shell when the login shell cannot run POSIX commands", async () => {
if (process.platform === "win32") return;
const shellPath = path.join(await tempDir("omp-daemon-nonposix-shell-"), "csh");
await Bun.write(shellPath, "#!/bin/sh\nexit 1\n");
await fs.chmod(shellPath, 0o755);
await startPtyDaemonWithShell(shellPath, "basic-shell", "basic-shell");
}, 20_000);
it("preserves compatible login shells for PTY daemons", async () => {
if (process.platform === "win32") return;
const shellPath = path.join(await tempDir("omp-daemon-posix-shell-"), "zsh");
await Bun.write(shellPath, '#!/bin/sh\nexport OMP_TEST_SHELL_MARKER="compatible-shell"\nexec /bin/sh "$@"\n');
await fs.chmod(shellPath, 0o755);
await startPtyDaemonWithShell(shellPath, "basic-shell", "compatible-shell");
}, 20_000);
it("stops non-persistent daemons after the last project omp exits", async () => {
const projectDir = await tempDir("omp-daemon-exit-project-");
const runtimeDir = await tempDir("omp-daemon-exit-runtime-");