diff --git a/extensions/openshell/src/fs-bridge.ts b/extensions/openshell/src/fs-bridge.ts index a66c3ae545d..795d46457b3 100644 --- a/extensions/openshell/src/fs-bridge.ts +++ b/extensions/openshell/src/fs-bridge.ts @@ -6,7 +6,7 @@ import type { SandboxFsStat, SandboxResolvedPath, } from "openclaw/plugin-sdk/sandbox"; -import { resolveWritableRenameTargetsForBridge } from "openclaw/plugin-sdk/sandbox"; +import { createWritableRenameTargetResolver } from "openclaw/plugin-sdk/sandbox"; import type { OpenShellSandboxBackend } from "./backend.js"; import { movePathWithCopyFallback } from "./mirror.js"; @@ -24,19 +24,16 @@ export function createOpenShellFsBridge(params: { } class OpenShellFsBridge implements SandboxFsBridge { + private readonly resolveRenameTargets = createWritableRenameTargetResolver( + (target) => this.resolveTarget(target), + (target, action) => this.ensureWritable(target, action), + ); + constructor( private readonly sandbox: SandboxContext, private readonly backend: OpenShellSandboxBackend, ) {} - private resolveRenameTargets(params: { from: string; to: string; cwd?: string }) { - return resolveWritableRenameTargetsForBridge( - params, - (target) => this.resolveTarget(target), - (target, action) => this.ensureWritable(target, action), - ); - } - resolvePath(params: { filePath: string; cwd?: string }): SandboxResolvedPath { const target = this.resolveTarget(params); return { diff --git a/src/agents/sandbox.ts b/src/agents/sandbox.ts index 83da60465fa..fa9165480cf 100644 --- a/src/agents/sandbox.ts +++ b/src/agents/sandbox.ts @@ -46,6 +46,7 @@ export { uploadDirectoryToSshTarget, } from "./sandbox/ssh.js"; export { createRemoteShellSandboxFsBridge } from "./sandbox/remote-fs-bridge.js"; +export { createWritableRenameTargetResolver } from "./sandbox/fs-bridge-rename-targets.js"; export { resolveWritableRenameTargets } from "./sandbox/fs-bridge-rename-targets.js"; export { resolveWritableRenameTargetsForBridge } from "./sandbox/fs-bridge-rename-targets.js"; diff --git a/src/agents/sandbox/fs-bridge-rename-targets.ts b/src/agents/sandbox/fs-bridge-rename-targets.ts index b2bd072a05b..cfa457689b2 100644 --- a/src/agents/sandbox/fs-bridge-rename-targets.ts +++ b/src/agents/sandbox/fs-bridge-rename-targets.ts @@ -30,3 +30,10 @@ export function resolveWritableRenameTargetsForBridge( + resolveTarget: (params: { filePath: string; cwd?: string }) => T, + ensureWritable: (target: T, action: string) => void, +): (params: { from: string; to: string; cwd?: string }) => { from: T; to: T } { + return (params) => resolveWritableRenameTargetsForBridge(params, resolveTarget, ensureWritable); +} diff --git a/src/agents/sandbox/remote-fs-bridge.ts b/src/agents/sandbox/remote-fs-bridge.ts index 868d76f2ca0..3ac9ea74ddc 100644 --- a/src/agents/sandbox/remote-fs-bridge.ts +++ b/src/agents/sandbox/remote-fs-bridge.ts @@ -2,7 +2,7 @@ import path from "node:path"; import { isPathInside } from "../../infra/path-guards.js"; import type { SandboxBackendCommandParams, SandboxBackendCommandResult } from "./backend.js"; import { SANDBOX_PINNED_MUTATION_PYTHON } from "./fs-bridge-mutation-helper.js"; -import { resolveWritableRenameTargetsForBridge } from "./fs-bridge-rename-targets.js"; +import { createWritableRenameTargetResolver } from "./fs-bridge-rename-targets.js"; import type { SandboxFsBridge, SandboxFsStat, SandboxResolvedPath } from "./fs-bridge.js"; import { isPathInsideContainerRoot, @@ -36,19 +36,16 @@ export function createRemoteShellSandboxFsBridge(params: { } class RemoteShellSandboxFsBridge implements SandboxFsBridge { + private readonly resolveRenameTargets = createWritableRenameTargetResolver( + (target) => this.resolveTarget(target), + (target, action) => this.ensureWritable(target, action), + ); + constructor( private readonly sandbox: SandboxContext, private readonly runtime: RemoteShellSandboxHandle, ) {} - private resolveRenameTargets(params: { from: string; to: string; cwd?: string }) { - return resolveWritableRenameTargetsForBridge( - params, - (target) => this.resolveTarget(target), - (target, action) => this.ensureWritable(target, action), - ); - } - resolvePath(params: { filePath: string; cwd?: string }): SandboxResolvedPath { const target = this.resolveTarget(params); return { diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 8c2f9f28ba8..4127d7d30d7 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -11,12 +11,17 @@ import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js"; import { callGateway } from "../gateway/call.js"; import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js"; import { isValidIPv4 } from "../gateway/net.js"; +import { + detectBrowserOpenSupport, + openUrl, + openUrlInBackground, + resolveBrowserOpenCommand, +} from "../infra/browser-open.js"; import { detectBinary } from "../infra/detect-binary.js"; import { inspectBestEffortPrimaryTailnetIPv4, pickBestEffortPrimaryLanIPv4, } from "../infra/network-discovery-display.js"; -import { isWSL } from "../infra/wsl.js"; import { runCommandWithTimeout } from "../process/exec.js"; import type { RuntimeEnv } from "../runtime.js"; import { stylePromptTitle } from "../terminal/prompt-style.js"; @@ -26,6 +31,7 @@ import { VERSION } from "../version.js"; import type { NodeManagerChoice, OnboardMode, ResetScope } from "./onboard-types.js"; export { detectBinary }; +export { detectBrowserOpenSupport, openUrl, openUrlInBackground, resolveBrowserOpenCommand }; export function guardCancel(value: T | symbol, runtime: RuntimeEnv): T { if (isCancel(value)) { @@ -128,79 +134,6 @@ export function applyWizardMetadata( }; } -type BrowserOpenSupport = { - ok: boolean; - reason?: string; - command?: string; -}; - -type BrowserOpenCommand = { - argv: string[] | null; - reason?: string; - command?: string; - /** - * Whether the URL must be wrapped in quotes when appended to argv. - * Needed for Windows `cmd /c start` where `&` splits commands. - */ - quoteUrl?: boolean; -}; - -export async function resolveBrowserOpenCommand(): Promise { - const platform = process.platform; - const hasDisplay = Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY); - const isSsh = - Boolean(process.env.SSH_CLIENT) || - Boolean(process.env.SSH_TTY) || - Boolean(process.env.SSH_CONNECTION); - - if (isSsh && !hasDisplay && platform !== "win32") { - return { argv: null, reason: "ssh-no-display" }; - } - - if (platform === "win32") { - return { - argv: ["cmd", "/c", "start", ""], - command: "cmd", - quoteUrl: true, - }; - } - - if (platform === "darwin") { - const hasOpen = await detectBinary("open"); - return hasOpen ? { argv: ["open"], command: "open" } : { argv: null, reason: "missing-open" }; - } - - if (platform === "linux") { - const wsl = await isWSL(); - if (!hasDisplay && !wsl) { - return { argv: null, reason: "no-display" }; - } - if (wsl) { - const hasWslview = await detectBinary("wslview"); - if (hasWslview) { - return { argv: ["wslview"], command: "wslview" }; - } - if (!hasDisplay) { - return { argv: null, reason: "wsl-no-wslview" }; - } - } - const hasXdgOpen = await detectBinary("xdg-open"); - return hasXdgOpen - ? { argv: ["xdg-open"], command: "xdg-open" } - : { argv: null, reason: "missing-xdg-open" }; - } - - return { argv: null, reason: "unsupported-platform" }; -} - -export async function detectBrowserOpenSupport(): Promise { - const resolved = await resolveBrowserOpenCommand(); - if (!resolved.argv) { - return { ok: false, reason: resolved.reason }; - } - return { ok: true, command: resolved.command }; -} - export function formatControlUiSshHint(params: { port: number; basePath?: string; @@ -234,57 +167,6 @@ function resolveSshTargetHint(): string { return `${user}@${host}`; } -export async function openUrl(url: string): Promise { - if (shouldSkipBrowserOpenInTests()) { - return false; - } - const resolved = await resolveBrowserOpenCommand(); - if (!resolved.argv) { - return false; - } - const quoteUrl = resolved.quoteUrl === true; - const command = [...resolved.argv]; - if (quoteUrl) { - if (command.at(-1) === "") { - // Preserve the empty title token for `start` when using verbatim args. - command[command.length - 1] = '""'; - } - command.push(`"${url}"`); - } else { - command.push(url); - } - try { - await runCommandWithTimeout(command, { - timeoutMs: 5_000, - windowsVerbatimArguments: quoteUrl, - }); - return true; - } catch { - // ignore; we still print the URL for manual open - return false; - } -} - -export async function openUrlInBackground(url: string): Promise { - if (shouldSkipBrowserOpenInTests()) { - return false; - } - if (process.platform !== "darwin") { - return false; - } - const resolved = await resolveBrowserOpenCommand(); - if (!resolved.argv || resolved.command !== "open") { - return false; - } - const command = ["open", "-g", url]; - try { - await runCommandWithTimeout(command, { timeoutMs: 5_000 }); - return true; - } catch { - return false; - } -} - export async function ensureWorkspaceAndSessions( workspaceDir: string, runtime: RuntimeEnv, @@ -340,13 +222,6 @@ export async function handleReset(scope: ResetScope, workspaceDir: string, runti } } -function shouldSkipBrowserOpenInTests(): boolean { - if (process.env.VITEST) { - return true; - } - return process.env.NODE_ENV === "test"; -} - export async function probeGatewayReachable(params: { url: string; token?: string; diff --git a/src/config/schema-base.ts b/src/config/schema-base.ts index 1aa1d72a641..679c9695669 100644 --- a/src/config/schema-base.ts +++ b/src/config/schema-base.ts @@ -1,6 +1,7 @@ import { VERSION } from "../version.js"; import type { ConfigUiHints } from "./schema.hints.js"; import { buildBaseHints, mapSensitivePaths } from "./schema.hints.js"; +import { asSchemaObject, cloneSchema } from "./schema.shared.js"; import { applyDerivedTags } from "./schema.tags.js"; import { OpenClawSchema } from "./zod-schema.js"; @@ -12,6 +13,9 @@ type JsonSchemaObject = Record & { additionalProperties?: JsonSchemaObject | boolean; }; +const asJsonSchemaObject = (value: unknown): JsonSchemaObject | null => + asSchemaObject(value); + export type BaseConfigSchemaResponse = { schema: ConfigSchema; uiHints: ConfigUiHints; @@ -21,23 +25,9 @@ export type BaseConfigSchemaResponse = { type BaseConfigSchemaStablePayload = Omit; -function cloneSchema(value: T): T { - if (typeof structuredClone === "function") { - return structuredClone(value); - } - return JSON.parse(JSON.stringify(value)) as T; -} - -function asSchemaObject(value: unknown): JsonSchemaObject | null { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return null; - } - return value as JsonSchemaObject; -} - function stripChannelSchema(schema: ConfigSchema): ConfigSchema { const next = cloneSchema(schema); - const root = asSchemaObject(next); + const root = asJsonSchemaObject(next); if (!root || !root.properties) { return next; } @@ -47,7 +37,7 @@ function stripChannelSchema(schema: ConfigSchema): ConfigSchema { if (Array.isArray(root.required)) { root.required = root.required.filter((key) => key !== "$schema"); } - const channelsNode = asSchemaObject(root.properties.channels); + const channelsNode = asJsonSchemaObject(root.properties.channels); if (channelsNode) { channelsNode.properties = {}; channelsNode.required = []; diff --git a/src/config/schema.shared.ts b/src/config/schema.shared.ts index 9eb6f71e052..11e220c5d87 100644 --- a/src/config/schema.shared.ts +++ b/src/config/schema.shared.ts @@ -8,6 +8,20 @@ type JsonSchemaObject = { oneOf?: JsonSchemaObject[]; }; +export function cloneSchema(value: T): T { + if (typeof structuredClone === "function") { + return structuredClone(value); + } + return JSON.parse(JSON.stringify(value)) as T; +} + +export function asSchemaObject(value: unknown): T | null { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return null; + } + return value as T; +} + export function schemaHasChildren(schema: JsonSchemaObject): boolean { if (schema.properties && Object.keys(schema.properties).length > 0) { return true; diff --git a/src/config/schema.ts b/src/config/schema.ts index 3ce1a1b2cc9..3498c673985 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -3,7 +3,12 @@ import { CHANNEL_IDS } from "../channels/registry.js"; import { GENERATED_BASE_CONFIG_SCHEMA } from "./schema.base.generated.js"; import type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js"; import { applySensitiveHints } from "./schema.hints.js"; -import { findWildcardHintMatch, schemaHasChildren } from "./schema.shared.js"; +import { + asSchemaObject, + cloneSchema, + findWildcardHintMatch, + schemaHasChildren, +} from "./schema.shared.js"; import { applyDerivedTags } from "./schema.tags.js"; export type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js"; @@ -20,6 +25,9 @@ type JsonSchemaObject = JsonSchemaNode & { items?: JsonSchemaObject | JsonSchemaObject[]; }; +const asJsonSchemaObject = (value: unknown): JsonSchemaObject | null => + asSchemaObject(value); + const FORBIDDEN_LOOKUP_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]); const LOOKUP_SCHEMA_STRING_KEYS = new Set([ "$id", @@ -53,20 +61,6 @@ const LOOKUP_SCHEMA_BOOLEAN_KEYS = new Set([ ]); const MAX_LOOKUP_PATH_SEGMENTS = 32; -function cloneSchema(value: T): T { - if (typeof structuredClone === "function") { - return structuredClone(value); - } - return JSON.parse(JSON.stringify(value)) as T; -} - -function asSchemaObject(value: unknown): JsonSchemaObject | null { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return null; - } - return value as JsonSchemaObject; -} - function isObjectSchema(schema: JsonSchemaObject): boolean { const type = schema.type; if (type === "object") { @@ -284,14 +278,14 @@ function applyHeartbeatTargetHints( function applyPluginSchemas(schema: ConfigSchema, plugins: PluginUiMetadata[]): ConfigSchema { const next = cloneSchema(schema); - const root = asSchemaObject(next); - const pluginsNode = asSchemaObject(root?.properties?.plugins); - const entriesNode = asSchemaObject(pluginsNode?.properties?.entries); + const root = asJsonSchemaObject(next); + const pluginsNode = asJsonSchemaObject(root?.properties?.plugins); + const entriesNode = asJsonSchemaObject(pluginsNode?.properties?.entries); if (!entriesNode) { return next; } - const entryBase = asSchemaObject(entriesNode.additionalProperties); + const entryBase = asJsonSchemaObject(entriesNode.additionalProperties); const entryProperties = entriesNode.properties ?? {}; entriesNode.properties = entryProperties; @@ -302,9 +296,9 @@ function applyPluginSchemas(schema: ConfigSchema, plugins: PluginUiMetadata[]): const entrySchema = entryBase ? cloneSchema(entryBase) : ({ type: "object" } as JsonSchemaObject); - const entryObject = asSchemaObject(entrySchema) ?? ({ type: "object" } as JsonSchemaObject); - const baseConfigSchema = asSchemaObject(entryObject.properties?.config); - const pluginSchema = asSchemaObject(plugin.configSchema); + const entryObject = asJsonSchemaObject(entrySchema) ?? ({ type: "object" } as JsonSchemaObject); + const baseConfigSchema = asJsonSchemaObject(entryObject.properties?.config); + const pluginSchema = asJsonSchemaObject(plugin.configSchema); const nextConfigSchema = baseConfigSchema && pluginSchema && @@ -325,8 +319,8 @@ function applyPluginSchemas(schema: ConfigSchema, plugins: PluginUiMetadata[]): function applyChannelSchemas(schema: ConfigSchema, channels: ChannelUiMetadata[]): ConfigSchema { const next = cloneSchema(schema); - const root = asSchemaObject(next); - const channelsNode = asSchemaObject(root?.properties?.channels); + const root = asJsonSchemaObject(next); + const channelsNode = asJsonSchemaObject(root?.properties?.channels); if (!channelsNode) { return next; } @@ -337,8 +331,8 @@ function applyChannelSchemas(schema: ConfigSchema, channels: ChannelUiMetadata[] if (!channel.configSchema) { continue; } - const existing = asSchemaObject(channelProps[channel.id]); - const incoming = asSchemaObject(channel.configSchema); + const existing = asJsonSchemaObject(channelProps[channel.id]); + const incoming = asJsonSchemaObject(channel.configSchema); if (existing && incoming && isObjectSchema(existing) && isObjectSchema(incoming)) { channelProps[channel.id] = mergeObjectSchema(existing, incoming); } else { @@ -502,7 +496,7 @@ function resolveLookupChildSchema( const properties = schema.properties; if (properties && Object.hasOwn(properties, segment)) { - return asSchemaObject(properties[segment]); + return asJsonSchemaObject(properties[segment]); } const itemIndex = /^\d+$/.test(segment) ? Number.parseInt(segment, 10) : undefined; @@ -621,7 +615,7 @@ export function lookupConfigSchema( return null; } - let current = asSchemaObject(response.schema); + let current = asJsonSchemaObject(response.schema); if (!current) { return null; } diff --git a/src/infra/browser-open.ts b/src/infra/browser-open.ts new file mode 100644 index 00000000000..3aa04f086c4 --- /dev/null +++ b/src/infra/browser-open.ts @@ -0,0 +1,127 @@ +import { runCommandWithTimeout } from "../process/exec.js"; +import { detectBinary } from "./detect-binary.js"; +import { isWSL } from "./wsl.js"; + +export type BrowserOpenCommand = { + argv: string[] | null; + reason?: string; + command?: string; + quoteUrl?: boolean; +}; + +export type BrowserOpenSupport = { + ok: boolean; + reason?: string; + command?: string; +}; + +function shouldSkipBrowserOpenInTests(): boolean { + if (process.env.VITEST) { + return true; + } + return process.env.NODE_ENV === "test"; +} + +export async function resolveBrowserOpenCommand(): Promise { + const platform = process.platform; + const hasDisplay = Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY); + const isSsh = + Boolean(process.env.SSH_CLIENT) || + Boolean(process.env.SSH_TTY) || + Boolean(process.env.SSH_CONNECTION); + + if (isSsh && !hasDisplay && platform !== "win32") { + return { argv: null, reason: "ssh-no-display" }; + } + + if (platform === "win32") { + return { + argv: ["cmd", "/c", "start", ""], + command: "cmd", + quoteUrl: true, + }; + } + + if (platform === "darwin") { + const hasOpen = await detectBinary("open"); + return hasOpen ? { argv: ["open"], command: "open" } : { argv: null, reason: "missing-open" }; + } + + if (platform === "linux") { + const wsl = await isWSL(); + if (!hasDisplay && !wsl) { + return { argv: null, reason: "no-display" }; + } + if (wsl) { + const hasWslview = await detectBinary("wslview"); + if (hasWslview) { + return { argv: ["wslview"], command: "wslview" }; + } + if (!hasDisplay) { + return { argv: null, reason: "wsl-no-wslview" }; + } + } + const hasXdgOpen = await detectBinary("xdg-open"); + return hasXdgOpen + ? { argv: ["xdg-open"], command: "xdg-open" } + : { argv: null, reason: "missing-xdg-open" }; + } + + return { argv: null, reason: "unsupported-platform" }; +} + +export async function detectBrowserOpenSupport(): Promise { + const resolved = await resolveBrowserOpenCommand(); + if (!resolved.argv) { + return { ok: false, reason: resolved.reason }; + } + return { ok: true, command: resolved.command }; +} + +export async function openUrl(url: string): Promise { + if (shouldSkipBrowserOpenInTests()) { + return false; + } + const resolved = await resolveBrowserOpenCommand(); + if (!resolved.argv) { + return false; + } + const quoteUrl = resolved.quoteUrl === true; + const command = [...resolved.argv]; + if (quoteUrl) { + if (command.at(-1) === "") { + command[command.length - 1] = '""'; + } + command.push(`"${url}"`); + } else { + command.push(url); + } + try { + await runCommandWithTimeout(command, { + timeoutMs: 5_000, + windowsVerbatimArguments: quoteUrl, + }); + return true; + } catch { + return false; + } +} + +export async function openUrlInBackground(url: string): Promise { + if (shouldSkipBrowserOpenInTests()) { + return false; + } + if (process.platform !== "darwin") { + return false; + } + const resolved = await resolveBrowserOpenCommand(); + if (!resolved.argv || resolved.command !== "open") { + return false; + } + try { + await runCommandWithTimeout(["open", "-g", url], { timeoutMs: 5_000 }); + return true; + } catch { + return false; + } +} diff --git a/src/plugin-sdk/sandbox.ts b/src/plugin-sdk/sandbox.ts index 8cc447e870a..0ee9fbdd43b 100644 --- a/src/plugin-sdk/sandbox.ts +++ b/src/plugin-sdk/sandbox.ts @@ -26,6 +26,7 @@ export { buildRemoteCommand, buildSshSandboxArgv, createRemoteShellSandboxFsBridge, + createWritableRenameTargetResolver, createSshSandboxSessionFromConfigText, createSshSandboxSessionFromSettings, disposeSshSandboxSession, diff --git a/src/plugins/setup-browser.ts b/src/plugins/setup-browser.ts index c665e3cef98..1bf9dd37bde 100644 --- a/src/plugins/setup-browser.ts +++ b/src/plugins/setup-browser.ts @@ -1,94 +1,6 @@ -import { isRemoteEnvironment } from "../infra/remote-env.js"; -import { isWSL } from "../infra/wsl.js"; -import { runCommandWithTimeout } from "../process/exec.js"; -import { detectBinary } from "./setup-binary.js"; - +export { + openUrl, + resolveBrowserOpenCommand, + type BrowserOpenCommand, +} from "../infra/browser-open.js"; export { isRemoteEnvironment } from "../infra/remote-env.js"; - -function shouldSkipBrowserOpenInTests(): boolean { - if (process.env.VITEST) { - return true; - } - return process.env.NODE_ENV === "test"; -} - -type BrowserOpenCommand = { - argv: string[] | null; - command?: string; - quoteUrl?: boolean; -}; - -async function resolveBrowserOpenCommand(): Promise { - const platform = process.platform; - const hasDisplay = Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY); - const isSsh = - Boolean(process.env.SSH_CLIENT) || - Boolean(process.env.SSH_TTY) || - Boolean(process.env.SSH_CONNECTION); - - if (isSsh && !hasDisplay && platform !== "win32") { - return { argv: null }; - } - - if (platform === "win32") { - return { - argv: ["cmd", "/c", "start", ""], - command: "cmd", - quoteUrl: true, - }; - } - - if (platform === "darwin") { - const hasOpen = await detectBinary("open"); - return hasOpen ? { argv: ["open"], command: "open" } : { argv: null }; - } - - if (platform === "linux") { - const wsl = await isWSL(); - if (!hasDisplay && !wsl) { - return { argv: null }; - } - if (wsl) { - const hasWslview = await detectBinary("wslview"); - if (hasWslview) { - return { argv: ["wslview"], command: "wslview" }; - } - if (!hasDisplay) { - return { argv: null }; - } - } - const hasXdgOpen = await detectBinary("xdg-open"); - return hasXdgOpen ? { argv: ["xdg-open"], command: "xdg-open" } : { argv: null }; - } - - return { argv: null }; -} - -export async function openUrl(url: string): Promise { - if (shouldSkipBrowserOpenInTests()) { - return false; - } - const resolved = await resolveBrowserOpenCommand(); - if (!resolved.argv) { - return false; - } - const quoteUrl = resolved.quoteUrl === true; - const command = [...resolved.argv]; - if (quoteUrl) { - if (command.at(-1) === "") { - command[command.length - 1] = '""'; - } - command.push(`"${url}"`); - } else { - command.push(url); - } - try { - await runCommandWithTimeout(command, { - timeoutMs: 5_000, - windowsVerbatimArguments: quoteUrl, - }); - return true; - } catch { - return false; - } -}