Merge remote-tracking branch 'origin/main' into dev

# Conflicts:
#	packages/server/src/server/session.ts
This commit is contained in:
Mohamed Boudra
2026-04-16 19:37:29 +07:00
27 changed files with 823 additions and 138 deletions

View File

@@ -105,6 +105,7 @@ jobs:
run: >
npx vitest run
src/utils/executable.test.ts
src/utils/spawn.launch-regression.test.ts
src/utils/spawn.test.ts
src/utils/run-git-command.test.ts
src/utils/checkout-git-rev-parse.test.ts
@@ -112,6 +113,7 @@ jobs:
src/server/agent/provider-launch-config.test.ts
src/server/agent/provider-snapshot-manager.test.ts
src/server/agent/providers/claude-agent.spawn.test.ts
src/server/agent/providers/provider-windows-launch.test.ts
src/server/agent/providers/provider-availability.test.ts
src/server/workspace-registry-model.test.ts
src/server/persisted-config.test.ts

View File

@@ -1,5 +1,33 @@
# Changelog
## 0.1.58 - 2026-04-16
### Added
- Markdown files render as formatted markdown in the file pane. ([#427](https://github.com/getpaseo/paseo/pull/427) by [@aaronflorey](https://github.com/aaronflorey))
- Cmd+L (Ctrl+L on Windows/Linux) focuses the agent message input.
- Provider models refresh on a freshness TTL; Settings shows last-updated time and any fetch errors. ([#426](https://github.com/getpaseo/paseo/pull/426))
- `disallowedTools` option in provider config to block specific tools from an agent.
### Improved
- Windows: agents launch reliably from npm `.cmd` shims, paths with spaces, and JSON config args — fixes `spawn EINVAL` startup errors. ([#454](https://github.com/getpaseo/paseo/pull/454))
- OpenCode permission prompts include the requesting tool's context. ([#398](https://github.com/getpaseo/paseo/pull/398) by [@aaronflorey](https://github.com/aaronflorey))
- OpenCode todo and compaction events render in the timeline. ([#429](https://github.com/getpaseo/paseo/pull/429) by [@aaronflorey](https://github.com/aaronflorey))
- OpenCode sessions archive cleanly when closed. ([#408](https://github.com/getpaseo/paseo/pull/408) by [@aaronflorey](https://github.com/aaronflorey))
- OpenCode slash commands recover from SSE timeouts. ([#407](https://github.com/getpaseo/paseo/pull/407) by [@aaronflorey](https://github.com/aaronflorey))
- Paseo MCP tools work against archived agents, matching the CLI. ([#423](https://github.com/getpaseo/paseo/pull/423))
- Native scrollbars match the active theme across all web views. ([#399](https://github.com/getpaseo/paseo/pull/399) by [@ethersh](https://github.com/ethersh))
### Fixed
- Code file previews can be selected and copied on iOS. ([#447](https://github.com/getpaseo/paseo/pull/447) by [@muzhi1991](https://github.com/muzhi1991))
- File preview no longer shows stale content when reopening the same file. ([#411](https://github.com/getpaseo/paseo/pull/411) by [@muzhi1991](https://github.com/muzhi1991))
- File explorer reinitialises when the client reconnects after a page refresh. ([#442](https://github.com/getpaseo/paseo/pull/442) by [@1996fanrui](https://github.com/1996fanrui))
- Generic ACP providers no longer receive duplicated command arguments. ([#444](https://github.com/getpaseo/paseo/pull/444) by [@edvardchen](https://github.com/edvardchen))
- Workspace headers no longer show a branch icon for non-git workspaces.
- Branch switcher layout is stable on mobile.
- Model names no longer truncate mid-word in the picker rows.
- Messages appear in the correct order after reconnecting on mobile.
- Clearing agent attention no longer throws on timeout.
## 0.1.56 - 2026-04-14
### Fixed

View File

@@ -139,6 +139,16 @@ The changelog is shown on the Paseo homepage. Write it for **end users**, not de
- **Only list changes relative to the previous stable release.** The diff is `v(previous)..HEAD`. If something was introduced and fixed between those two tags, it never shipped — don't mention the fix.
- **Cut low-signal entries.** "Toolbar buttons have consistent sizing" is too granular. Combine small polish items or drop them.
## Changelog conciseness
Every bullet must be scannable at a glance. The changelog is not release documentation — it's a list.
- **One line per bullet.** If a bullet wraps to three lines in a narrow column, it's too long.
- **Split bullets that pack multiple distinct changes.** If a bullet uses "and", "plus", a comma list, or an em-dash to chain several independent improvements, break them into separate bullets — even when they share a theme or author. One bullet = one user-facing change.
- **Trim qualifying clauses.** Drop "with a hint shown when…", "matching the CLI's behaviour", "across common install shapes". If the detail doesn't change whether a user cares, cut it.
- **Lead with the outcome.** "Windows: agents launch reliably from npm `.cmd` shims…" is better than "Windows: agents launch reliably across common install shapes. Claude, Codex, and OpenCode now start correctly…".
- **Attribution follows the split.** When you split a dense bullet, move each PR/author to the bullet it belongs to. Never duplicate the same PR across multiple bullets.
## Changelog attribution
Every changelog bullet must credit contributors and link to the PR(s) that delivered the change. This is not one-PR-per-line — a single bullet describes a user-facing change and may reference multiple PRs.

View File

@@ -42,7 +42,7 @@ buildNpmPackage rec {
# To update: run `nix build` with lib.fakeHash, copy the `got:` hash.
# CI auto-updates this when package-lock.json changes (see .github/workflows/).
npmDepsHash = "sha256-Wn7g+VXuUe0LI733MUj+WRX/fJQHOO/HFEkqyHrWTf0=";
npmDepsHash = "sha256-rpXng5y99bDt7a3f6vI+EWrVahlXINdHzCf7ml+ZnFs=";
# Prevent onnxruntime-node's install script from running during automatic
# npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox).

38
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "paseo",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "paseo",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"hasInstallScript": true,
"license": "AGPL-3.0-or-later",
"workspaces": [
@@ -34854,16 +34854,16 @@
},
"packages/app": {
"name": "@getpaseo/app",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@expo/vector-icons": "^15.0.2",
"@floating-ui/react-native": "^0.10.7",
"@getpaseo/expo-two-way-audio": "0.1.57-rc.1",
"@getpaseo/highlight": "0.1.57-rc.1",
"@getpaseo/server": "0.1.57-rc.1",
"@getpaseo/expo-two-way-audio": "0.1.58",
"@getpaseo/highlight": "0.1.58",
"@getpaseo/server": "0.1.58",
"@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0",
@@ -35004,11 +35004,11 @@
},
"packages/cli": {
"name": "@getpaseo/cli",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.57-rc.1",
"@getpaseo/server": "0.1.57-rc.1",
"@getpaseo/relay": "0.1.58",
"@getpaseo/server": "0.1.58",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
@@ -35049,11 +35049,11 @@
},
"packages/desktop": {
"name": "@getpaseo/desktop",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@getpaseo/cli": "0.1.57-rc.1",
"@getpaseo/server": "0.1.57-rc.1",
"@getpaseo/cli": "0.1.58",
"@getpaseo/server": "0.1.58",
"electron-log": "^5.4.3",
"electron-updater": "^6.6.2",
"ws": "^8.14.2"
@@ -35087,7 +35087,7 @@
},
"packages/expo-two-way-audio": {
"name": "@getpaseo/expo-two-way-audio",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"license": "MIT",
"devDependencies": {
"@biomejs/biome": "1.9.4",
@@ -35288,7 +35288,7 @@
},
"packages/highlight": {
"name": "@getpaseo/highlight",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"dependencies": {
"@lezer/common": "^1.5.0",
"@lezer/cpp": "^1.1.5",
@@ -35314,7 +35314,7 @@
},
"packages/relay": {
"name": "@getpaseo/relay",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"dependencies": {
"base64-js": "^1.5.1",
"tweetnacl": "^1.0.3",
@@ -35330,14 +35330,14 @@
},
"packages/server": {
"name": "@getpaseo/server",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"dependencies": {
"@agentclientprotocol/sdk": "^0.17.1",
"@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.57-rc.1",
"@getpaseo/relay": "0.1.57-rc.1",
"@getpaseo/highlight": "0.1.58",
"@getpaseo/relay": "0.1.58",
"@isaacs/ttlcache": "^2.1.4",
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.2.6",
@@ -35790,7 +35790,7 @@
},
"packages/website": {
"name": "@getpaseo/website",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"dependencies": {
"@cloudflare/vite-plugin": "^1.20.3",
"@cloudflare/workers-types": "^4.20260114.0",

View File

@@ -1,6 +1,6 @@
{
"name": "paseo",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"private": true,
"workspaces": [
"packages/expo-two-way-audio",

View File

@@ -1,7 +1,7 @@
{
"name": "@getpaseo/app",
"main": "index.ts",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"private": true,
"scripts": {
"start": "expo start",
@@ -31,9 +31,9 @@
"@dnd-kit/utilities": "^3.2.2",
"@expo/vector-icons": "^15.0.2",
"@floating-ui/react-native": "^0.10.7",
"@getpaseo/expo-two-way-audio": "0.1.57-rc.1",
"@getpaseo/highlight": "0.1.57-rc.1",
"@getpaseo/server": "0.1.57-rc.1",
"@getpaseo/expo-two-way-audio": "0.1.58",
"@getpaseo/highlight": "0.1.58",
"@getpaseo/server": "0.1.58",
"@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@getpaseo/cli",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"description": "Paseo CLI - control your AI coding agents from the command line",
"type": "module",
"files": [
@@ -24,8 +24,8 @@
},
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.57-rc.1",
"@getpaseo/server": "0.1.57-rc.1",
"@getpaseo/relay": "0.1.58",
"@getpaseo/server": "0.1.58",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",

View File

@@ -1,6 +1,6 @@
{
"name": "@getpaseo/desktop",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"private": true,
"description": "Paseo desktop app (Electron wrapper)",
"main": "dist/main.js",
@@ -13,8 +13,8 @@
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@getpaseo/cli": "0.1.57-rc.1",
"@getpaseo/server": "0.1.57-rc.1",
"@getpaseo/cli": "0.1.58",
"@getpaseo/server": "0.1.58",
"electron-log": "^5.4.3",
"electron-updater": "^6.6.2",
"ws": "^8.14.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@getpaseo/expo-two-way-audio",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"description": "Native module for two way audio streaming",
"main": "build/index.js",
"types": "build/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@getpaseo/highlight",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"type": "module",
"publishConfig": {
"access": "public"

View File

@@ -1,6 +1,6 @@
{
"name": "@getpaseo/relay",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"description": "Paseo relay for bridging daemon and client connections",
"type": "module",
"publishConfig": {

View File

@@ -1,6 +1,6 @@
{
"name": "@getpaseo/server",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"description": "Paseo backend server",
"type": "module",
"publishConfig": {
@@ -60,8 +60,8 @@
"@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.57-rc.1",
"@getpaseo/relay": "0.1.57-rc.1",
"@getpaseo/highlight": "0.1.58",
"@getpaseo/relay": "0.1.58",
"@isaacs/ttlcache": "^2.1.4",
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.2.6",

View File

@@ -2849,7 +2849,7 @@ export class DaemonClient {
providers: options?.providers,
},
responseType: "refresh_providers_snapshot_response",
timeout: 5000,
timeout: 60000,
});
}

View File

@@ -95,7 +95,7 @@ export function toAgentPayload(
model: agent.config.model ?? null,
thinkingOptionId,
effectiveThinkingOptionId,
runtimeInfo,
...(runtimeInfo ? { runtimeInfo } : {}),
createdAt: agent.createdAt.toISOString(),
updatedAt: agent.updatedAt.toISOString(),
lastUserMessageAt: agent.lastUserMessageAt ? agent.lastUserMessageAt.toISOString() : null,
@@ -193,8 +193,6 @@ export function buildStoredAgentPayload(
availableModes: [],
pendingPermissions: [],
persistence: toAgentPersistenceHandle(logger, providerRegistry, record.persistence),
lastUsage: undefined,
lastError: undefined,
title: record.title ?? record.config?.title ?? null,
requiresAttention: record.requiresAttention ?? false,
attentionReason: record.attentionReason ?? null,

View File

@@ -2,12 +2,14 @@ import { describe, expect, it, vi } from "vitest";
import { mkdtemp, mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { z } from "zod";
import { createTestLogger } from "../../test-utils/test-logger.js";
import { createAgentMcpServer } from "./mcp-server.js";
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
import type { ProviderDefinition } from "./provider-registry.js";
import { AgentSnapshotPayloadSchema } from "../../shared/messages.js";
type TestDeps = {
agentManager: AgentManager;
@@ -702,6 +704,59 @@ describe("agent snapshot MCP serialization", () => {
]);
});
it("emits list_agents payloads that satisfy the declared output schema", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const liveAgent = {
id: "live-agent",
provider: "claude",
cwd: "/tmp/live-project",
config: {},
runtimeInfo: undefined,
createdAt: new Date("2026-04-11T00:00:00.000Z"),
updatedAt: new Date("2026-04-11T00:00:00.000Z"),
lastUserMessageAt: null,
lifecycle: "idle",
capabilities: {
supportsStreaming: false,
supportsSessionPersistence: false,
supportsDynamicModes: false,
supportsMcpServers: true,
supportsReasoningStream: false,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
features: [],
pendingPermissions: new Map(),
persistence: null,
labels: {},
attention: { requiresAttention: false },
} as unknown as ManagedAgent;
spies.agentManager.listAgents.mockReturnValue([liveAgent]);
spies.agentStorage.list.mockResolvedValue([
createStoredRecord({ id: "stored-non-archived", archivedAt: null }),
createStoredRecord({ id: "stored-archived", archivedAt: "2026-04-12T00:00:00.000Z" }),
]);
const server = await createAgentMcpServer({
agentManager,
agentStorage,
logger,
providerRegistry: {
claude: createProviderDefinition({}),
} as any,
});
const tool = (server as any)._registeredTools["list_agents"];
const response = await tool.callback({ includeArchived: true });
const parsed = z.array(AgentSnapshotPayloadSchema).safeParse(response.structuredContent.agents);
if (!parsed.success) {
throw new Error(
`list_agents response failed AgentSnapshotPayloadSchema: ${JSON.stringify(parsed.error.issues, null, 2)}`,
);
}
});
it("loads archived agents before reading get_agent_activity", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const record = createStoredRecord({ id: "archived-activity-agent" });

View File

@@ -40,6 +40,7 @@ import {
parseDurationString,
resolveProviderAndModel,
sanitizePermissionRequest,
sendPromptToAgent,
setupFinishNotification,
serializeSnapshotWithMetadata,
startAgentRun,
@@ -772,30 +773,20 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
},
async ({ agentId, prompt, sessionMode, background = false, notifyOnFinish = false }) => {
const snapshot = agentManager.getAgent(agentId);
if (!snapshot) {
throw new Error(`Agent ${agentId} not found`);
}
if (agentManager.hasInFlightRun(agentId)) {
waitTracker.cancel(agentId, "Agent run interrupted by new prompt");
}
if (sessionMode) {
await agentManager.setAgentMode(agentId, sessionMode);
}
try {
agentManager.recordUserMessage(agentId, prompt, {
emitState: false,
});
} catch (error) {
childLogger.error({ err: error, agentId }, "Failed to record user message");
}
startAgentRun(agentManager, agentId, prompt, childLogger, {
replaceRunning: true,
await sendPromptToAgent({
agentManager,
agentStorage,
agentId,
userMessageText: prompt,
prompt,
sessionMode,
logger: childLogger,
});
if (notifyOnFinish && callerAgentId) {
setupFinishNotification({
agentManager,

View File

@@ -1,10 +1,15 @@
import { z } from "zod";
import type { Logger } from "pino";
import type { AgentPromptInput, AgentPermissionRequest } from "./agent-sdk-types.js";
import type {
AgentPromptInput,
AgentPermissionRequest,
AgentRunOptions,
} from "./agent-sdk-types.js";
import type { AgentManager, ManagedAgent, WaitForAgentResult } from "./agent-manager.js";
import { curateAgentActivity } from "./activity-curator.js";
import type { AgentStorage } from "./agent-storage.js";
import { ensureAgentLoaded } from "./agent-loading.js";
import { serializeAgentSnapshot } from "../messages.js";
import { StoredScheduleSchema } from "../schedule/types.js";
import type { AgentProvider } from "./agent-sdk-types.js";
@@ -94,6 +99,7 @@ export function resolveProviderAndModel(params: {
export type StartAgentRunOptions = {
replaceRunning?: boolean;
runOptions?: AgentRunOptions;
};
/**
@@ -171,9 +177,10 @@ export function startAgentRun(
options?: StartAgentRunOptions,
): void {
const shouldReplace = Boolean(options?.replaceRunning && agentManager.hasInFlightRun(agentId));
const runOptions = options?.runOptions;
const iterator = shouldReplace
? agentManager.replaceAgentRun(agentId, prompt)
: agentManager.streamAgent(agentId, prompt);
? agentManager.replaceAgentRun(agentId, prompt, runOptions)
: agentManager.streamAgent(agentId, prompt, runOptions);
void (async () => {
try {
for await (const _ of iterator) {
@@ -185,6 +192,92 @@ export function startAgentRun(
})();
}
/**
* Clear the archived flag from a stored agent record.
* Shared across Session (app/WS), MCP, and CLI so every surface that acts on
* an archived agent unarchives it the same way.
*/
export async function unarchiveAgentState(
agentStorage: AgentStorage,
agentManager: AgentManager,
agentId: string,
): Promise<boolean> {
const record = await agentStorage.get(agentId);
if (!record || !record.archivedAt) {
return false;
}
const updatedAt = new Date().toISOString();
await agentStorage.upsert({
...record,
archivedAt: null,
updatedAt,
});
agentManager.notifyAgentState(agentId);
return true;
}
export interface SendPromptToAgentParams {
agentManager: AgentManager;
agentStorage: AgentStorage;
agentId: string;
/** Raw user text to record in the timeline. */
userMessageText: string;
/** Prompt to dispatch to the provider (may include image blocks or wrapped text). */
prompt: AgentPromptInput;
messageId?: string;
runOptions?: AgentRunOptions;
/** Optional mode to set on the agent before the run starts. */
sessionMode?: string;
logger: Logger;
}
/**
* Full send-prompt orchestration: unarchive → load → (optional mode change) →
* record user message → start run.
*
* Every surface that sends a prompt to an agent (Session/WS, MCP, CLI-through-MCP)
* MUST go through this so behavior can never drift between them.
*/
export async function sendPromptToAgent(params: SendPromptToAgentParams): Promise<void> {
const {
agentManager,
agentStorage,
agentId,
userMessageText,
prompt,
messageId,
runOptions,
sessionMode,
logger,
} = params;
await unarchiveAgentState(agentStorage, agentManager, agentId);
await ensureAgentLoaded(agentId, {
agentManager,
agentStorage,
logger,
});
if (sessionMode) {
await agentManager.setAgentMode(agentId, sessionMode);
}
try {
agentManager.recordUserMessage(agentId, userMessageText, {
messageId,
emitState: false,
});
} catch (error) {
logger.error({ err: error, agentId }, "Failed to record user message");
}
startAgentRun(agentManager, agentId, prompt, logger, {
replaceRunning: true,
runOptions,
});
}
interface SetupFinishNotificationParams {
agentManager: AgentManager;
agentStorage: AgentStorage;

View File

@@ -1145,6 +1145,7 @@ export class ClaudeAgentClient implements AgentClient {
const resolvedBinary = (await findExecutable("claude")) ?? "not found";
const available = await this.isAvailable();
const version = await resolveClaudeVersion(this.runtimeSettings);
const auth = available ? await resolveClaudeAuth(this.runtimeSettings) : null;
let modelsValue = "Not checked";
let status = formatDiagnosticStatus(available);
@@ -1165,6 +1166,7 @@ export class ClaudeAgentClient implements AgentClient {
diagnostic: formatProviderDiagnostic("Claude Code", [
{ label: "Binary", value: resolvedBinary },
...(version ? [{ label: "Version", value: version }] : []),
...(auth ? [{ label: "Auth", value: auth }] : []),
{ label: "Models", value: modelsValue },
{ label: "Status", value: status },
]),
@@ -1211,6 +1213,48 @@ async function resolveClaudeVersion(
}
}
async function resolveClaudeAuth(
runtimeSettings?: ProviderRuntimeSettings,
): Promise<string | null> {
const command = runtimeSettings?.command;
const run = async (
executable: string,
args: string[],
): Promise<{ stdout: string; stderr: string }> => {
try {
return await execCommand(executable, args, { timeout: 5_000 });
} catch (error) {
const err = error as { stdout?: string; stderr?: string; message?: string };
return {
stdout: err.stdout ?? "",
stderr: err.stderr ?? err.message ?? "",
};
}
};
try {
let result: { stdout: string; stderr: string };
if (command?.mode === "replace") {
result = await run(command.argv[0]!, [...command.argv.slice(1), "auth", "status"]);
} else {
const executable = await findExecutable("claude");
if (!executable) {
return null;
}
result = await run(executable, ["auth", "status"]);
}
const combined = [result.stdout, result.stderr]
.map((s) => s.trim())
.filter((s) => s.length > 0)
.join("\n");
return combined || null;
} catch {
return null;
}
}
function extractContextWindowSize(modelUsage: unknown): number | undefined {
if (!modelUsage || typeof modelUsage !== "object") {
return undefined;

View File

@@ -0,0 +1,217 @@
import type { ChildProcess } from "node:child_process";
import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import { findExecutable } from "../../../utils/executable.js";
import { spawnProcess } from "../../../utils/spawn.js";
type SpawnResult = {
code: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
error: Error | null;
};
type ProviderLaunchCase = {
provider: "claude" | "codex" | "opencode" | "generic-acp";
binaryName: string;
args: string[];
shell?: boolean;
};
const JSON_ARG = '{"mcpServers":{"paseo":{"type":"http","url":"http://127.0.0.1:6767/mcp"}}}';
const tempDirs: string[] = [];
function makeFixture(binaryName: string, expectedArgs: string[]) {
const root = mkdtempSync(path.join(tmpdir(), `paseo ${binaryName} launch `));
tempDirs.push(root);
const fakeDaemonNode = path.join(root, "Fake Paseo.exe");
copyFileSync(process.execPath, fakeDaemonNode);
const assertScript = path.join(root, "assert-argv.js");
writeFileSync(
assertScript,
`
if (process.argv.includes("--version")) {
console.log("fake-provider 1.0.0");
process.exit(0);
}
const expected = JSON.parse(process.env.PASEO_EXPECTED_ARGV_JSON);
const actual = process.argv.slice(2);
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
console.error("ARGV_MISMATCH");
console.error(JSON.stringify({ expected, actual }));
process.exit(42);
}
console.log("ARGV_OK");
`,
);
const shim = path.join(root, `${binaryName}.cmd`);
writeFileSync(
shim,
["@echo off", "setlocal", `\"${fakeDaemonNode}\" \"${assertScript}\" %*`, ""].join("\r\n"),
);
return { root, shim, expectedArgs };
}
function collectChild(child: ChildProcess, timeoutMs = 10_000): Promise<SpawnResult> {
return new Promise((resolve) => {
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];
let error: Error | null = null;
let settled = false;
const settle = (result: Pick<SpawnResult, "code" | "signal">) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve({
...result,
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
stderr: Buffer.concat(stderrChunks).toString("utf8"),
error,
});
};
const timer = setTimeout(() => {
child.kill("SIGKILL");
settle({ code: null, signal: "SIGKILL" });
}, timeoutMs);
timer.unref?.();
child.stdout?.on("data", (chunk: Buffer | string) => {
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
child.stderr?.on("data", (chunk: Buffer | string) => {
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
child.once("error", (err) => {
error = err;
settle({ code: null, signal: null });
});
child.once("exit", (code, signal) => {
settle({ code, signal });
});
});
}
async function runProviderFixture(params: {
command: string;
args: string[];
expectedArgs: string[];
shell?: boolean;
}): Promise<SpawnResult> {
const child = spawnProcess(params.command, params.args, {
env: {
...process.env,
PASEO_EXPECTED_ARGV_JSON: JSON.stringify(params.expectedArgs),
},
stdio: ["ignore", "pipe", "pipe"],
...(params.shell === undefined ? {} : { shell: params.shell }),
});
return collectChild(child);
}
function withPathEntry<T>(dir: string, run: () => Promise<T>): Promise<T> {
const pathKey =
process.platform === "win32"
? (Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "Path")
: "PATH";
const previousPath = process.env[pathKey];
process.env[pathKey] = previousPath ? `${dir}${path.delimiter}${previousPath}` : dir;
return run().finally(() => {
if (previousPath === undefined) {
delete process.env[pathKey];
} else {
process.env[pathKey] = previousPath;
}
});
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
const providerLaunchCases: ProviderLaunchCase[] = [
{
provider: "claude",
binaryName: "claude",
args: ["--mcp-config", JSON_ARG],
shell: false,
},
{
provider: "codex",
binaryName: "codex",
args: ["app-server", "--config", JSON_ARG],
},
{
provider: "opencode",
binaryName: "opencode",
args: ["serve", "--port", "49271", "--config", JSON_ARG],
},
{
provider: "generic-acp",
binaryName: "generic-acp",
args: ["--mcp-config", JSON_ARG],
},
];
describe.runIf(process.platform === "win32")("Windows provider launch parity", () => {
test("detected claude.cmd can be launched with Claude's JSON-safe spawn shape", async () => {
const args = ["--mcp-config", JSON_ARG];
const fixture = makeFixture("claude", args);
await withPathEntry(fixture.root, async () => {
const detected = await findExecutable("claude");
expect(detected?.toLowerCase()).toBe(fixture.shim.toLowerCase());
const result = await runProviderFixture({
command: detected!,
args,
expectedArgs: args,
shell: false,
});
expect(result.error).toBeNull();
expect(result.code).toBe(0);
expect(result.signal).toBeNull();
expect(result.stderr).toBe("");
expect(result.stdout.trim()).toBe("ARGV_OK");
});
});
test.each(
providerLaunchCases,
)("$provider launches a cmd shim from a path with spaces through spawnProcess", async ({
binaryName,
args,
shell,
}) => {
const fixture = makeFixture(binaryName, args);
const result = await runProviderFixture({
command: fixture.shim,
args: fixture.expectedArgs,
expectedArgs: fixture.expectedArgs,
shell,
});
expect(result.error).toBeNull();
expect(result.code).toBe(0);
expect(result.signal).toBeNull();
expect(result.stderr).toBe("");
expect(result.stdout.trim()).toBe("ARGV_OK");
});
});

View File

@@ -170,7 +170,7 @@ export function toAgentPersistenceHandle(
return {
provider,
sessionId: handle.sessionId,
nativeHandle: handle.nativeHandle,
metadata: handle.metadata,
...(handle.nativeHandle !== undefined ? { nativeHandle: handle.nativeHandle } : {}),
...(handle.metadata !== undefined ? { metadata: handle.metadata } : {}),
} satisfies AgentPersistenceHandle;
}

View File

@@ -68,6 +68,7 @@ import {
toAgentPersistenceHandle,
} from "./persistence-hooks.js";
import { ensureAgentLoaded } from "./agent/agent-loading.js";
import { sendPromptToAgent, unarchiveAgentState } from "./agent/mcp-shared.js";
import { experimental_createMCPClient } from "ai";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js";
@@ -298,7 +299,7 @@ type ProcessingPhase = "idle" | "transcribing";
type WorkspaceGitWatchTarget = {
cwd: string;
watchers: FSWatcher[];
debounceTimer: NodeJS.Timeout | null;
debounceTimer: ReturnType<typeof setTimeout> | null;
refreshPromise: Promise<void> | null;
refreshQueued: boolean;
latestFingerprint: string | null;
@@ -600,7 +601,7 @@ export class Session {
private isVoiceMode = false;
private speechInProgress = false;
private pendingVoiceSpeechStartAt: number | null = null;
private pendingVoiceSpeechTimer: NodeJS.Timeout | null = null;
private pendingVoiceSpeechTimer: ReturnType<typeof setTimeout> | null = null;
private readonly dictationStreamManager: DictationStreamManager;
private readonly resolveVoiceTurnDetection: () => TurnDetectionProvider | null;
@@ -611,7 +612,7 @@ export class Session {
// Audio buffering for interruption handling
private pendingAudioSegments: Array<{ audio: Buffer; format: string }> = [];
private bufferTimeout: NodeJS.Timeout | null = null;
private bufferTimeout: ReturnType<typeof setTimeout> | null = null;
private audioBuffer: AudioBufferState | null = null;
// Optional TTS debug capture (persisted per utterance)
@@ -2224,12 +2225,17 @@ export class Session {
});
}
private async unarchiveAgentState(agentId: string): Promise<boolean> {
return this.agentManager.unarchiveSnapshot(agentId);
}
private async unarchiveAgentByHandle(handle: AgentPersistenceHandle): Promise<void> {
await this.agentManager.unarchiveSnapshotByHandle(handle);
const records = await this.agentStorage.list();
const matched = records.find(
(record) =>
record.persistence?.provider === handle.provider &&
record.persistence?.sessionId === handle.sessionId,
);
if (!matched) {
return;
}
await unarchiveAgentState(this.agentStorage, this.agentManager, matched.id);
}
private async handleUpdateAgentRequest(
@@ -2723,38 +2729,28 @@ export class Session {
}`,
);
await this.unarchiveAgentState(agentId);
const promptText = options?.spokenInput ? wrapSpokenInput(text) : text;
const prompt = this.buildAgentPrompt(promptText, images, attachments);
try {
await ensureAgentLoaded(agentId, {
await sendPromptToAgent({
agentManager: this.agentManager,
agentStorage: this.agentStorage,
agentId,
userMessageText: text,
prompt,
messageId,
runOptions,
logger: this.sessionLogger,
});
return { ok: true };
} catch (error) {
this.handleAgentRunError(agentId, error, "Failed to initialize agent before sending prompt");
this.handleAgentRunError(agentId, error, "Failed to send agent message");
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
};
}
try {
this.agentManager.recordUserMessage(agentId, text, {
messageId,
emitState: false,
});
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId },
`Failed to record user message for agent ${agentId}`,
);
}
const promptText = options?.spokenInput ? wrapSpokenInput(text) : text;
const prompt = this.buildAgentPrompt(promptText, images, attachments);
return this.startAgentStream(agentId, prompt, runOptions);
}
/**
@@ -2931,7 +2927,7 @@ export class Session {
try {
await this.unarchiveAgentByHandle(handle);
const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
await this.unarchiveAgentState(snapshot.id);
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
await this.forwardAgentUpdate(snapshot);
const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
@@ -2969,7 +2965,7 @@ export class Session {
this.sessionLogger.info({ agentId }, `Refreshing agent ${agentId} from persistence`);
try {
await this.unarchiveAgentState(agentId);
await unarchiveAgentState(this.agentStorage, this.agentManager, agentId);
let snapshot: ManagedAgent;
const existing = this.agentManager.getAgent(agentId);
if (existing) {
@@ -6751,44 +6747,32 @@ export class Session {
try {
const agentId = resolved.agentId;
await this.unarchiveAgentState(agentId);
await ensureAgentLoaded(agentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
});
this.sessionLogger.trace(
{ agentId, messageId: msg.messageId, textPrefix: msg.text.slice(0, 80) },
"send_agent_message_request: recording user message",
);
try {
this.agentManager.recordUserMessage(agentId, msg.text, {
messageId: msg.messageId,
emitState: false,
});
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId },
"Failed to record user message for send_agent_message_request",
);
}
const prompt = this.buildAgentPrompt(msg.text, msg.images, msg.attachments);
this.sessionLogger.trace(
{ agentId, messageId: msg.messageId },
"send_agent_message_request: starting agent stream",
{ agentId, messageId: msg.messageId, textPrefix: msg.text.slice(0, 80) },
"send_agent_message_request: dispatching shared sendPromptToAgent",
);
const started = this.startAgentStream(agentId, prompt);
if (!started.ok) {
try {
await sendPromptToAgent({
agentManager: this.agentManager,
agentStorage: this.agentStorage,
agentId,
userMessageText: msg.text,
prompt,
messageId: msg.messageId,
logger: this.sessionLogger,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.handleAgentRunError(agentId, error, "Failed to send agent message");
this.emit({
type: "send_agent_message_response",
payload: {
requestId: msg.requestId,
agentId,
accepted: false,
error: started.error,
error: message,
},
});
return;
@@ -7681,9 +7665,9 @@ export class Session {
this.workspaceGitSubscriptions.clear();
}
// ============================================================================
// ----------------------------------------------------------------------------
// Terminal Handlers
// ============================================================================
// ----------------------------------------------------------------------------
private ensureTerminalExitSubscription(terminal: TerminalSession): void {
if (this.terminalExitSubscriptions.has(terminal.id)) {

View File

@@ -138,7 +138,7 @@ describe("executableExists", () => {
expect(executableExists("/usr/local/bin/codex", exists)).toBe("/usr/local/bin/codex");
});
test("on Windows, falls back to .exe, .cmd, then .ps1 for extensionless paths", () => {
test("on Windows, falls back to .exe, then .cmd for extensionless paths", () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", { value: "win32", writable: true });
try {
@@ -150,6 +150,18 @@ describe("executableExists", () => {
}
});
test("on Windows, ignores PowerShell scripts for extensionless paths", () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", { value: "win32", writable: true });
try {
const exists = (candidate: string) => candidate === "C:\\tools\\codex.ps1";
expect(executableExists("C:\\tools\\codex", exists)).toBeNull();
} finally {
Object.defineProperty(process, "platform", { value: originalPlatform, writable: true });
}
});
test("returns null when no matching path exists", () => {
expect(executableExists("/missing/codex", () => false)).toBeNull();
});

View File

@@ -35,7 +35,7 @@ async function enumerateCandidates(name: string): Promise<string[]> {
});
}
function isWindowsCommandScript(executablePath: string): boolean {
export function isWindowsCommandScript(executablePath: string): boolean {
const extension = extname(executablePath).toLowerCase();
return process.platform === "win32" && (extension === ".cmd" || extension === ".bat");
}
@@ -102,7 +102,7 @@ export function executableExists(
): string | null {
if (exists(executablePath)) return executablePath;
if (process.platform === "win32" && !extname(executablePath)) {
for (const ext of [".exe", ".cmd", ".ps1"]) {
for (const ext of [".exe", ".cmd"]) {
const candidate = executablePath + ext;
if (exists(candidate)) return candidate;
}
@@ -141,8 +141,11 @@ function escapeWindowsCmdValue(value: string): string {
const unquoted = isQuoted ? value.slice(1, -1) : value;
const escaped = unquoted.replace(/%/g, "%%").replace(/([&|^<>()!])/g, "^$1");
if (isQuoted || escaped.includes(" ")) {
return `"${escaped}"`;
if (isQuoted || /[\s"]/u.test(unquoted)) {
const quoted = escaped
.replace(/(\\*)"/g, (_match, slashes: string) => `${slashes}${slashes}\\"`)
.replace(/\\+$/u, (slashes) => `${slashes}${slashes}`);
return `"${quoted}"`;
}
return escaped;

View File

@@ -0,0 +1,244 @@
import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import type { ChildProcess } from "node:child_process";
import { afterEach, describe, expect, test } from "vitest";
import { findExecutable } from "./executable.js";
import { spawnProcess } from "./spawn.js";
type SpawnResult = {
code: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
error: Error | null;
};
const tempDirs: string[] = [];
const JSON_ARG = '{"key":"value with spaces","nested":{"quote":"\\"yes\\""}}';
function makeFixture(options?: { includeCmdShim?: boolean }): {
root: string;
fakeDaemonNode: string;
shim: string;
powerShellShim: string;
assertScript: string;
expectedArgs: string[];
} {
const root = mkdtempSync(path.join(tmpdir(), "paseo spawn regression "));
tempDirs.push(root);
const fakeDaemonNode = path.join(root, "Fake Paseo.exe");
copyFileSync(process.execPath, fakeDaemonNode);
const expectedArgs = ["--config", JSON_ARG];
const assertScript = path.join(root, "assert-argv.js");
writeFileSync(
assertScript,
`
if (process.argv.includes("--version")) {
console.log("fake-provider 1.0.0");
process.exit(0);
}
const expected = JSON.parse(process.env.PASEO_EXPECTED_ARGV_JSON);
const actual = process.argv.slice(2);
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
console.error("ARGV_MISMATCH");
console.error(JSON.stringify({ expected, actual }));
process.exit(42);
}
console.log("ARGV_OK");
`,
);
const shim = path.join(root, "claude.cmd");
if (options?.includeCmdShim !== false) {
writeFileSync(
shim,
["@echo off", "setlocal", `\"${fakeDaemonNode}\" \"${assertScript}\" %*`, ""].join("\r\n"),
);
}
const powerShellShim = path.join(root, "claude.ps1");
writeFileSync(
powerShellShim,
[
"$ErrorActionPreference = 'Stop'",
`& '${fakeDaemonNode.replace(/'/g, "''")}' '${assertScript.replace(/'/g, "''")}' @args`,
"exit $LASTEXITCODE",
"",
].join("\r\n"),
);
return { root, fakeDaemonNode, shim, powerShellShim, assertScript, expectedArgs };
}
function collectChild(child: ChildProcess, timeoutMs = 10_000): Promise<SpawnResult> {
return new Promise((resolve) => {
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];
let error: Error | null = null;
let settled = false;
const settle = (result: Pick<SpawnResult, "code" | "signal">) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve({
...result,
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
stderr: Buffer.concat(stderrChunks).toString("utf8"),
error,
});
};
const timer = setTimeout(() => {
child.kill("SIGKILL");
settle({ code: null, signal: "SIGKILL" });
}, timeoutMs);
timer.unref?.();
child.stdout?.on("data", (chunk: Buffer | string) => {
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
child.stderr?.on("data", (chunk: Buffer | string) => {
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
child.once("error", (err) => {
error = err;
settle({ code: null, signal: null });
});
child.once("exit", (code, signal) => {
settle({ code, signal });
});
});
}
async function runFixture(params: {
command: string;
args: string[];
shell?: boolean;
}): Promise<SpawnResult> {
const child = spawnProcess(params.command, params.args, {
env: {
...process.env,
PASEO_EXPECTED_ARGV_JSON: JSON.stringify(["--config", JSON_ARG]),
},
stdio: ["ignore", "pipe", "pipe"],
...(params.shell === undefined ? {} : { shell: params.shell }),
});
return collectChild(child);
}
function withWindowsPathEntry<T>(dir: string, run: () => Promise<T>): Promise<T> {
const pathKey =
process.platform === "win32"
? (Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "Path")
: "PATH";
const previousPath = process.env[pathKey];
const previousPathExt = process.env.PATHEXT;
process.env[pathKey] = previousPath ? `${dir}${path.delimiter}${previousPath}` : dir;
process.env.PATHEXT = previousPathExt?.toLowerCase().includes(".ps1")
? previousPathExt
: `${previousPathExt ?? ""};.PS1`;
return run().finally(() => {
if (previousPath === undefined) {
delete process.env[pathKey];
} else {
process.env[pathKey] = previousPath;
}
if (previousPathExt === undefined) {
delete process.env.PATHEXT;
} else {
process.env.PATHEXT = previousPathExt;
}
});
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
describe.runIf(process.platform === "win32")("Windows spawn launch regression", () => {
test("launches a cmd shim from a path with spaces without corrupting JSON args", async () => {
const fixture = makeFixture();
const result = await runFixture({
command: fixture.shim,
args: fixture.expectedArgs,
});
expect(result.error).toBeNull();
expect(result.code).toBe(0);
expect(result.signal).toBeNull();
expect(result.stderr).toBe("");
expect(result.stdout.trim()).toBe("ARGV_OK");
});
test("launches a cmd shim even when the caller explicitly disables shell", async () => {
const fixture = makeFixture();
const result = await runFixture({
command: fixture.shim,
args: fixture.expectedArgs,
shell: false,
});
expect(result.error).toBeNull();
expect(result.code).toBe(0);
expect(result.signal).toBeNull();
expect(result.stderr).toBe("");
expect(result.stdout.trim()).toBe("ARGV_OK");
});
test("direct launch with a space-containing executable preserves JSON args", async () => {
const fixture = makeFixture();
const result = await runFixture({
command: fixture.fakeDaemonNode,
args: [fixture.assertScript, ...fixture.expectedArgs],
shell: false,
});
expect(result.error).toBeNull();
expect(result.code).toBe(0);
expect(result.signal).toBeNull();
expect(result.stderr).toBe("");
expect(result.stdout.trim()).toBe("ARGV_OK");
});
test("does not detect a PowerShell shim from PATH", async () => {
const fixture = makeFixture({ includeCmdShim: false });
await withWindowsPathEntry(fixture.root, async () => {
const detected = await findExecutable("claude");
expect(detected).toBeNull();
});
});
});
describe.skipIf(process.platform === "win32")("spawn launch regression smoke", () => {
test("direct launch with a space-containing executable works on this platform", async () => {
const fixture = makeFixture();
const result = await runFixture({
command: fixture.fakeDaemonNode,
args: [fixture.assertScript, ...fixture.expectedArgs],
shell: false,
});
expect(result.error).toBeNull();
expect(result.code).toBe(0);
expect(result.stderr).toBe("");
expect(result.stdout.trim()).toBe("ARGV_OK");
});
});

View File

@@ -1,7 +1,7 @@
import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
import { promisify } from "node:util";
import { quoteWindowsArgument, quoteWindowsCommand } from "./executable.js";
import { isWindowsCommandScript, quoteWindowsArgument, quoteWindowsCommand } from "./executable.js";
const execFileAsync = promisify(execFile);
@@ -24,13 +24,15 @@ export function spawnProcess(
options?: SpawnOptions,
): ChildProcess {
const isWindows = process.platform === "win32";
const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows);
const resolvedCommand = isWindows ? quoteWindowsCommand(command) : command;
const resolvedArgs = isWindows ? args.map(quoteWindowsArgument) : args;
const shouldQuoteForShell = isWindows && shell !== false;
const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command;
const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args;
return spawn(resolvedCommand, resolvedArgs, {
...options,
shell: options?.shell ?? isWindows,
shell,
windowsHide: true,
});
}
@@ -41,8 +43,10 @@ export async function execCommand(
options?: ExecCommandOptions,
): Promise<ExecCommandResult> {
const isWindows = process.platform === "win32";
const resolvedCommand = isWindows ? quoteWindowsCommand(command) : command;
const resolvedArgs = isWindows ? args.map(quoteWindowsArgument) : args;
const shell = isWindowsCommandScript(command) ? true : isWindows;
const shouldQuoteForShell = isWindows && shell !== false;
const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command;
const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args;
return execFileAsync(resolvedCommand, resolvedArgs, {
cwd: options?.cwd,
@@ -50,7 +54,7 @@ export async function execCommand(
encoding: options?.encoding ?? "utf8",
timeout: options?.timeout,
maxBuffer: options?.maxBuffer,
shell: isWindows,
shell,
windowsHide: true,
}) as Promise<ExecCommandResult>;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@getpaseo/website",
"version": "0.1.57-rc.1",
"version": "0.1.58",
"private": true,
"type": "module",
"scripts": {