From 56379f9773ed8cf0131713bd2fce40dac233c8bb Mon Sep 17 00:00:00 2001
From: Parsifa1
Date: Thu, 16 Jul 2026 09:38:28 +0000
Subject: [PATCH 1/2] fix(tools): fix hub tools env selection
fix command failed when using non-POSIX shell
---
packages/coding-agent/CHANGELOG.md | 2 ++
packages/coding-agent/src/launch/broker.ts | 5 +++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 0e753ced7..f947c82d5 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -2,6 +2,8 @@
## [Unreleased]
+- 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
### Changed
diff --git a/packages/coding-agent/src/launch/broker.ts b/packages/coding-agent/src/launch/broker.ts
index c135d0656..13fb3469d 100644
--- a/packages/coding-agent/src/launch/broker.ts
+++ b/packages/coding-agent/src/launch/broker.ts
@@ -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.resolveBasicShell() ?? "sh";
+ run = session.start({ command, shell, ...options }, onChunk);
}
void run
.then(result => this.#onPtyExit(record, generation, result))
From a86fbaa8d1c5431d49c5ff7b74eca732cb0d8f86 Mon Sep 17 00:00:00 2001
From: can1357
Date: Fri, 17 Jul 2026 04:10:15 +0200
Subject: [PATCH 2/2] fix(launch): preserve compatible PTY shells
---
packages/coding-agent/src/launch/broker.ts | 2 +-
.../coding-agent/test/tools/launch.test.ts | 98 +++++++++++++++++++
2 files changed, 99 insertions(+), 1 deletion(-)
diff --git a/packages/coding-agent/src/launch/broker.ts b/packages/coding-agent/src/launch/broker.ts
index 13fb3469d..264f753b4 100644
--- a/packages/coding-agent/src/launch/broker.ts
+++ b/packages/coding-agent/src/launch/broker.ts
@@ -555,7 +555,7 @@ class DaemonBroker {
`printf '%s' "$$" > ${quoteShellArg(pidPath)}`,
`exec ${argv.map(quoteShellArg).join(" ")}`,
].join("; ");
- const shell = procmgr.resolveBasicShell() ?? "sh";
+ const shell = procmgr.getShellConfig().shell;
run = session.start({ command, shell, ...options }, onChunk);
}
void run
diff --git a/packages/coding-agent/test/tools/launch.test.ts b/packages/coding-agent/test/tools/launch.test.ts
index df0bb0bda..3a05a40ed 100644
--- a/packages/coding-agent/test/tools/launch.test.ts
+++ b/packages/coding-agent/test/tools/launch.test.ts
@@ -42,6 +42,86 @@ async function shutdown(client: DaemonBrokerClient): Promise {
client.close();
}
+async function startPtyDaemonWithShell(shell: string, initialMarker: string, expectedMarker: string): Promise {
+ 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-");