/** * Open - Browser/editor launch service interface. * * Owns process launch helpers for opening URLs in a browser, workspace paths in * a configured editor, and generic external targets through the platform's * default opener. * * @module Open */import os from "node:os"; import { EDITORS, type EditorId } from "@t3tools/contracts";import { Effect, Exit, FileSystem, Layer, Option, Path, Schema, Scope, ServiceMap } from "effect";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; export class OpenError extends Schema.TaggedErrorClass<OpenError>()("OpenError", { message: Schema.String, cause: Schema.optional(Schema.Defect),}) {} export interface OpenInEditorInput { readonly cwd: string; readonly editor: EditorId;} export interface OpenApplicationInput { readonly name: string | ReadonlyArray<string>; readonly arguments?: ReadonlyArray<string>;} export interface OpenExternalInput { readonly target: string; readonly wait?: boolean; readonly background?: boolean; readonly newInstance?: boolean; readonly allowNonzeroExitCode?: boolean; readonly app?: OpenApplicationInput | ReadonlyArray<OpenApplicationInput>;} export interface OpenRuntimeOptions { readonly platform?: NodeJS.Platform; readonly env?: NodeJS.ProcessEnv; readonly isWsl?: boolean; readonly isInsideContainer?: boolean; readonly powerShellCommand?: string;} interface OpenRuntime { readonly platform: NodeJS.Platform; readonly env: NodeJS.ProcessEnv; readonly isWsl: boolean; readonly isInsideContainer: boolean; readonly powerShellCandidates: ReadonlyArray<string>; readonly windowsPathExtensions: ReadonlyArray<string>;} interface LaunchPlan { readonly command: string; readonly args: ReadonlyArray<string>; readonly wait: boolean; readonly allowNonzeroExitCode: boolean; readonly detached?: boolean; readonly shell?: boolean; readonly stdio?: "ignore";} interface ResolvedCommand { readonly path: string; readonly usesCmdWrapper: boolean;} const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/;const WINDOWS_POWERSHELL_CANDIDATES = ["powershell.exe", "powershell", "pwsh.exe", "pwsh"] as const;const WSL_POWERSHELL_CANDIDATES = [ "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe", "/mnt/c/Program Files/PowerShell/7/pwsh.exe", "powershell.exe", "pwsh.exe",] as const;const WINDOWS_BATCH_EXTENSIONS = [".CMD", ".BAT"] as const; function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target);} function stripWrappingQuotes(value: string): string { return value.replace(/^"+|"+$/g, "");} function resolvePathEnvironmentVariable(env: NodeJS.ProcessEnv): string { return env.PATH ?? env.Path ?? env.path ?? "";} function resolveWindowsPathExtensions(env: NodeJS.ProcessEnv): ReadonlyArray<string> { const rawValue = env.PATHEXT; const fallback = [".COM", ".EXE", ".BAT", ".CMD"]; if (!rawValue) return fallback; const parsed = rawValue .split(";") .map((entry) => entry.trim()) .filter((entry) => entry.length > 0) .map((entry) => (entry.startsWith(".") ? entry.toUpperCase() : `.${entry.toUpperCase()}`)); return parsed.length > 0 ? Array.from(new Set(parsed)) : fallback;} function resolveWindowsCommandShell(env: NodeJS.ProcessEnv): string { return env.ComSpec ?? env.COMSPEC ?? "cmd.exe";} function resolveCommandCandidates( command: string, runtime: OpenRuntime, pathService: Path.Path,): ReadonlyArray<string> { if (runtime.platform !== "win32") return [command]; const extension = pathService.extname(command); const normalizedExtension = extension.toUpperCase(); if (extension.length > 0 && runtime.windowsPathExtensions.includes(normalizedExtension)) { const commandWithoutExtension = command.slice(0, -extension.length); return Array.from( new Set([ command, `${commandWithoutExtension}${normalizedExtension}`, `${commandWithoutExtension}${normalizedExtension.toLowerCase()}`, ]), ); } const candidates: string[] = []; for (const extensionName of runtime.windowsPathExtensions) { candidates.push(`${command}${extensionName}`); candidates.push(`${command}${extensionName.toLowerCase()}`); } return Array.from(new Set(candidates));} function resolvePathDelimiter(platform: NodeJS.Platform): string { return platform === "win32" ? ";" : ":";} function detectWsl(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): boolean { if (platform !== "linux") return false; if (typeof env.WSL_DISTRO_NAME === "string" || typeof env.WSL_INTEROP === "string") { return true; } return os.release().toLowerCase().includes("microsoft");} function quotePowerShellValue(value: string): string { return `'${value.replaceAll("'", "''")}'`;} function encodePowerShellCommand(command: string): string { return Buffer.from(command, "utf16le").toString("base64");} function normalizeAppCandidates( app: OpenExternalInput["app"],): ReadonlyArray<{ readonly name: string; readonly arguments: ReadonlyArray<string> } | undefined> { if (!app) return [undefined]; const apps = Array.isArray(app) ? app : [app]; const candidates: Array<{ readonly name: string; readonly arguments: ReadonlyArray<string> }> = []; for (const appDef of apps) { const names = Array.isArray(appDef.name) ? appDef.name : [appDef.name]; for (const name of names) { candidates.push({ name, arguments: appDef.arguments ?? [] }); } } return candidates;} function isUriLikeTarget(target: string): boolean { return /^[A-Za-z][A-Za-z\d+.-]*:/.test(target);} function shouldPreferWindowsOpenerOnWsl(input: OpenExternalInput, runtime: OpenRuntime): boolean { return runtime.isWsl && !runtime.isInsideContainer && isUriLikeTarget(input.target);} function makeLaunchPlan( command: string, args: ReadonlyArray<string>, options: { readonly wait: boolean; readonly allowNonzeroExitCode: boolean; readonly detached?: boolean; readonly shell?: boolean; readonly stdio?: "ignore"; },): LaunchPlan { return { command, args, wait: options.wait, allowNonzeroExitCode: options.allowNonzeroExitCode, shell: options.shell ?? false, ...(options.detached !== undefined ? { detached: options.detached } : {}), ...(options.stdio !== undefined ? { stdio: options.stdio } : {}), };} function makeDarwinDefaultPlan(input: OpenExternalInput): LaunchPlan { const args: string[] = []; const wait = input.wait ?? false; if (wait) args.push("--wait-apps"); if (input.background) args.push("--background"); if (input.newInstance) args.push("--new"); args.push(input.target); return makeLaunchPlan("open", args, { wait, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, shell: false, });} function makeDarwinApplicationPlan( input: OpenExternalInput, app: { readonly name: string; readonly arguments: ReadonlyArray<string> },): LaunchPlan { const args: string[] = []; const wait = input.wait ?? false; if (wait) args.push("--wait-apps"); if (input.background) args.push("--background"); if (input.newInstance) args.push("--new"); args.push("-a", app.name); args.push(input.target); if (app.arguments.length > 0) { args.push("--args", ...app.arguments); } return makeLaunchPlan("open", args, { wait, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, shell: false, });} function makePowerShellPlan(input: OpenExternalInput, powerShellCommand: string): LaunchPlan { const encodedParts = ["Start"]; const wait = input.wait ?? false; if (wait) encodedParts.push("-Wait"); encodedParts.push(quotePowerShellValue(input.target)); return makeLaunchPlan( powerShellCommand, [ "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encodePowerShellCommand(encodedParts.join(" ")), ], { wait, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, shell: false, }, );} function makeLinuxDefaultPlan(input: OpenExternalInput): LaunchPlan { const wait = input.wait ?? false; return makeLaunchPlan("xdg-open", [input.target], { wait, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, detached: !wait, ...(wait ? {} : { stdio: "ignore" as const }), shell: false, });} function makeWindowsExplorerPlan(input: OpenExternalInput): LaunchPlan { return makeLaunchPlan("explorer", [input.target], { wait: false, allowNonzeroExitCode: false, shell: false, });} function makeDirectApplicationPlan( input: OpenExternalInput, app: { readonly name: string; readonly arguments: ReadonlyArray<string> },): LaunchPlan { return makeLaunchPlan(app.name, [...app.arguments, input.target], { wait: input.wait ?? false, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, shell: false, });} function resolveExternalPlans( input: OpenExternalInput, runtime: OpenRuntime,): ReadonlyArray<LaunchPlan> { const appCandidates = normalizeAppCandidates(input.app); const plans: LaunchPlan[] = []; const preferWindowsOpenerOnWsl = shouldPreferWindowsOpenerOnWsl(input, runtime); for (const app of appCandidates) { if (app) { if (runtime.platform === "darwin") { plans.push(makeDarwinApplicationPlan(input, app)); } else { plans.push(makeDirectApplicationPlan(input, app)); } continue; } if (runtime.platform === "darwin") { plans.push(makeDarwinDefaultPlan(input)); continue; } if (runtime.platform === "win32" || preferWindowsOpenerOnWsl) { for (const powerShellCommand of runtime.powerShellCandidates) { plans.push(makePowerShellPlan(input, powerShellCommand)); } } if (runtime.platform === "win32") { if (!(input.wait ?? false)) { plans.push(makeWindowsExplorerPlan(input)); } continue; } plans.push(makeLinuxDefaultPlan(input)); } return plans;} function toOpenError(message: string, cause: unknown): OpenError { return new OpenError({ message, cause });} function isWindowsBatchShim(pathService: Path.Path, filePath: string): boolean { return WINDOWS_BATCH_EXTENSIONS.includes(pathService.extname(filePath).toUpperCase() as never);} function quoteForWindowsCmd(value: string): string { return `"${value.replaceAll("%", "%%").replaceAll('"', '""')}"`;} function makeWindowsCmdCommandLine(commandPath: string, args: ReadonlyArray<string>): string { return `"${[commandPath, ...args].map(quoteForWindowsCmd).join(" ")}"`;} export interface OpenShape { readonly getAvailableEditors: Effect.Effect<ReadonlyArray<EditorId>, OpenError>; readonly openExternal: (input: OpenExternalInput) => Effect.Effect<void, OpenError>; readonly openBrowser: ( target: string, options?: Omit<OpenExternalInput, "target">, ) => Effect.Effect<void, OpenError>; readonly openInEditor: (input: OpenInEditorInput) => Effect.Effect<void, OpenError>;} export class Open extends ServiceMap.Service<Open, OpenShape>()("t3/open") {} const makeOpen = (options: OpenRuntimeOptions = {}) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const isInsideContainer = options.isInsideContainer ?? (typeof options.env?.CONTAINER === "string" || typeof options.env?.container === "string" || typeof options.env?.KUBERNETES_SERVICE_HOST === "string" ? true : yield* fileSystem.exists("/.dockerenv").pipe(Effect.catch(() => Effect.succeed(false)))); const runtime: OpenRuntime = { platform: options.platform ?? process.platform, env: options.env ?? process.env, isWsl: options.isWsl ?? detectWsl(options.platform ?? process.platform, options.env ?? process.env), isInsideContainer, powerShellCandidates: options.powerShellCommand !== undefined ? [options.powerShellCommand] : (options.isWsl ?? detectWsl(options.platform ?? process.platform, options.env ?? process.env)) ? WSL_POWERSHELL_CANDIDATES : WINDOWS_POWERSHELL_CANDIDATES, windowsPathExtensions: resolveWindowsPathExtensions(options.env ?? process.env), }; const resolveCommand = Effect.fn(function* ( command: string, ): Effect.fn.Return<Option.Option<ResolvedCommand>, never> { const candidates = resolveCommandCandidates(command, runtime, pathService); const resolveExecutableFile = Effect.fn(function* ( filePath: string, ): Effect.fn.Return<Option.Option<ResolvedCommand>, never> { const info = yield* fileSystem.stat(filePath).pipe(Effect.option); if (Option.isNone(info) || info.value.type !== "File") return Option.none(); if (runtime.platform === "win32") { const extension = pathService.extname(filePath); if ( extension.length === 0 || !runtime.windowsPathExtensions.includes(extension.toUpperCase()) ) { return Option.none(); } return Option.some({ path: filePath, usesCmdWrapper: isWindowsBatchShim(pathService, filePath), } satisfies ResolvedCommand); } return (info.value.mode & 0o111) !== 0 ? Option.some({ path: filePath, usesCmdWrapper: false, } satisfies ResolvedCommand) : Option.none(); }); if (command.includes("/") || command.includes("\\")) { for (const candidate of candidates) { const resolved = yield* resolveExecutableFile(candidate); if (Option.isSome(resolved)) return resolved; } return Option.none(); } const pathValue = resolvePathEnvironmentVariable(runtime.env); if (pathValue.length === 0) return Option.none(); const pathEntries = pathValue .split(resolvePathDelimiter(runtime.platform)) .map((entry) => stripWrappingQuotes(entry.trim())) .filter((entry) => entry.length > 0); for (const pathEntry of pathEntries) { for (const candidate of candidates) { const resolved = yield* resolveExecutableFile(pathService.join(pathEntry, candidate)); if (Option.isSome(resolved)) { return resolved; } } } return Option.none(); }); const commandAvailable = (command: string) => resolveCommand(command).pipe(Effect.map(Option.isSome)); const fileManagerAvailable = Effect.gen(function* () { const candidates = runtime.platform === "darwin" ? ["open"] : runtime.platform === "win32" ? [...runtime.powerShellCandidates, "explorer"] : ["xdg-open"]; for (const candidate of candidates) { if (yield* commandAvailable(candidate)) { return true; } } return false; }); const getAvailableEditors = Effect.gen(function* () { const available: EditorId[] = []; for (const editor of EDITORS) { if (editor.id === "file-manager") { if (yield* fileManagerAvailable) { available.push(editor.id); } continue; } if (editor.command && (yield* commandAvailable(editor.command))) { available.push(editor.id); } }Lines 1–500 of 678. Load more
/** * Open - Browser/editor launch service interface. * * Owns process launch helpers for opening URLs in a browser, workspace paths in * a configured editor, and generic external targets through the platform's * default opener. * * @module Open */ import os from "node:os"; import { EDITORS, type EditorId } from "@t3tools/contracts"; import { Effect, Exit, FileSystem, Layer, Option, Path, Schema, Scope, ServiceMap } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; export class OpenError extends Schema.TaggedErrorClass<OpenError>()("OpenError", { message: Schema.String, cause: Schema.optional(Schema.Defect), }) {} export interface OpenInEditorInput { readonly cwd: string; readonly editor: EditorId; } export interface OpenApplicationInput { readonly name: string | ReadonlyArray<string>; readonly arguments?: ReadonlyArray<string>; } export interface OpenExternalInput { readonly target: string; readonly wait?: boolean; readonly background?: boolean; readonly newInstance?: boolean; readonly allowNonzeroExitCode?: boolean; readonly app?: OpenApplicationInput | ReadonlyArray<OpenApplicationInput>; } export interface OpenRuntimeOptions { readonly platform?: NodeJS.Platform; readonly env?: NodeJS.ProcessEnv; readonly isWsl?: boolean; readonly isInsideContainer?: boolean; readonly powerShellCommand?: string; } interface OpenRuntime { readonly platform: NodeJS.Platform; readonly env: NodeJS.ProcessEnv; readonly isWsl: boolean; readonly isInsideContainer: boolean; readonly powerShellCandidates: ReadonlyArray<string>; readonly windowsPathExtensions: ReadonlyArray<string>; } interface LaunchPlan { readonly command: string; readonly args: ReadonlyArray<string>; readonly wait: boolean; readonly allowNonzeroExitCode: boolean; readonly detached?: boolean; readonly shell?: boolean; readonly stdio?: "ignore"; } interface ResolvedCommand { readonly path: string; readonly usesCmdWrapper: boolean; } const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; const WINDOWS_POWERSHELL_CANDIDATES = ["powershell.exe", "powershell", "pwsh.exe", "pwsh"] as const; const WSL_POWERSHELL_CANDIDATES = [ "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe", "/mnt/c/Program Files/PowerShell/7/pwsh.exe", "powershell.exe", "pwsh.exe", ] as const; const WINDOWS_BATCH_EXTENSIONS = [".CMD", ".BAT"] as const; function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target); } function stripWrappingQuotes(value: string): string { return value.replace(/^"+|"+$/g, ""); } function resolvePathEnvironmentVariable(env: NodeJS.ProcessEnv): string { return env.PATH ?? env.Path ?? env.path ?? ""; } function resolveWindowsPathExtensions(env: NodeJS.ProcessEnv): ReadonlyArray<string> { const rawValue = env.PATHEXT; const fallback = [".COM", ".EXE", ".BAT", ".CMD"]; if (!rawValue) return fallback; const parsed = rawValue .split(";") .map((entry) => entry.trim()) .filter((entry) => entry.length > 0) .map((entry) => (entry.startsWith(".") ? entry.toUpperCase() : `.${entry.toUpperCase()}`)); return parsed.length > 0 ? Array.from(new Set(parsed)) : fallback; } function resolveWindowsCommandShell(env: NodeJS.ProcessEnv): string { return env.ComSpec ?? env.COMSPEC ?? "cmd.exe"; } function resolveCommandCandidates( command: string, runtime: OpenRuntime, pathService: Path.Path, ): ReadonlyArray<string> { if (runtime.platform !== "win32") return [command]; const extension = pathService.extname(command); const normalizedExtension = extension.toUpperCase(); if (extension.length > 0 && runtime.windowsPathExtensions.includes(normalizedExtension)) { const commandWithoutExtension = command.slice(0, -extension.length); return Array.from( new Set([ command, `${commandWithoutExtension}${normalizedExtension}`, `${commandWithoutExtension}${normalizedExtension.toLowerCase()}`, ]), ); } const candidates: string[] = []; for (const extensionName of runtime.windowsPathExtensions) { candidates.push(`${command}${extensionName}`); candidates.push(`${command}${extensionName.toLowerCase()}`); } return Array.from(new Set(candidates)); } function resolvePathDelimiter(platform: NodeJS.Platform): string { return platform === "win32" ? ";" : ":"; } function detectWsl(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): boolean { if (platform !== "linux") return false; if (typeof env.WSL_DISTRO_NAME === "string" || typeof env.WSL_INTEROP === "string") { return true; } return os.release().toLowerCase().includes("microsoft"); } function quotePowerShellValue(value: string): string { return `'${value.replaceAll("'", "''")}'`; } function encodePowerShellCommand(command: string): string { return Buffer.from(command, "utf16le").toString("base64"); } function normalizeAppCandidates( app: OpenExternalInput["app"], ): ReadonlyArray<{ readonly name: string; readonly arguments: ReadonlyArray<string> } | undefined> { if (!app) return [undefined]; const apps = Array.isArray(app) ? app : [app]; const candidates: Array<{ readonly name: string; readonly arguments: ReadonlyArray<string> }> = []; for (const appDef of apps) { const names = Array.isArray(appDef.name) ? appDef.name : [appDef.name]; for (const name of names) { candidates.push({ name, arguments: appDef.arguments ?? [] }); } } return candidates; } function isUriLikeTarget(target: string): boolean { return /^[A-Za-z][A-Za-z\d+.-]*:/.test(target); } function shouldPreferWindowsOpenerOnWsl(input: OpenExternalInput, runtime: OpenRuntime): boolean { return runtime.isWsl && !runtime.isInsideContainer && isUriLikeTarget(input.target); } function makeLaunchPlan( command: string, args: ReadonlyArray<string>, options: { readonly wait: boolean; readonly allowNonzeroExitCode: boolean; readonly detached?: boolean; readonly shell?: boolean; readonly stdio?: "ignore"; }, ): LaunchPlan { return { command, args, wait: options.wait, allowNonzeroExitCode: options.allowNonzeroExitCode, shell: options.shell ?? false, ...(options.detached !== undefined ? { detached: options.detached } : {}), ...(options.stdio !== undefined ? { stdio: options.stdio } : {}), }; } function makeDarwinDefaultPlan(input: OpenExternalInput): LaunchPlan { const args: string[] = []; const wait = input.wait ?? false; if (wait) args.push("--wait-apps"); if (input.background) args.push("--background"); if (input.newInstance) args.push("--new"); args.push(input.target); return makeLaunchPlan("open", args, { wait, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, shell: false, }); } function makeDarwinApplicationPlan( input: OpenExternalInput, app: { readonly name: string; readonly arguments: ReadonlyArray<string> }, ): LaunchPlan { const args: string[] = []; const wait = input.wait ?? false; if (wait) args.push("--wait-apps"); if (input.background) args.push("--background"); if (input.newInstance) args.push("--new"); args.push("-a", app.name); args.push(input.target); if (app.arguments.length > 0) { args.push("--args", ...app.arguments); } return makeLaunchPlan("open", args, { wait, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, shell: false, }); } function makePowerShellPlan(input: OpenExternalInput, powerShellCommand: string): LaunchPlan { const encodedParts = ["Start"]; const wait = input.wait ?? false; if (wait) encodedParts.push("-Wait"); encodedParts.push(quotePowerShellValue(input.target)); return makeLaunchPlan( powerShellCommand, [ "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encodePowerShellCommand(encodedParts.join(" ")), ], { wait, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, shell: false, }, ); } function makeLinuxDefaultPlan(input: OpenExternalInput): LaunchPlan { const wait = input.wait ?? false; return makeLaunchPlan("xdg-open", [input.target], { wait, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, detached: !wait, ...(wait ? {} : { stdio: "ignore" as const }), shell: false, }); } function makeWindowsExplorerPlan(input: OpenExternalInput): LaunchPlan { return makeLaunchPlan("explorer", [input.target], { wait: false, allowNonzeroExitCode: false, shell: false, }); } function makeDirectApplicationPlan( input: OpenExternalInput, app: { readonly name: string; readonly arguments: ReadonlyArray<string> }, ): LaunchPlan { return makeLaunchPlan(app.name, [...app.arguments, input.target], { wait: input.wait ?? false, allowNonzeroExitCode: input.allowNonzeroExitCode ?? false, shell: false, }); } function resolveExternalPlans( input: OpenExternalInput, runtime: OpenRuntime, ): ReadonlyArray<LaunchPlan> { const appCandidates = normalizeAppCandidates(input.app); const plans: LaunchPlan[] = []; const preferWindowsOpenerOnWsl = shouldPreferWindowsOpenerOnWsl(input, runtime); for (const app of appCandidates) { if (app) { if (runtime.platform === "darwin") { plans.push(makeDarwinApplicationPlan(input, app)); } else { plans.push(makeDirectApplicationPlan(input, app)); } continue; } if (runtime.platform === "darwin") { plans.push(makeDarwinDefaultPlan(input)); continue; } if (runtime.platform === "win32" || preferWindowsOpenerOnWsl) { for (const powerShellCommand of runtime.powerShellCandidates) { plans.push(makePowerShellPlan(input, powerShellCommand)); } } if (runtime.platform === "win32") { if (!(input.wait ?? false)) { plans.push(makeWindowsExplorerPlan(input)); } continue; } plans.push(makeLinuxDefaultPlan(input)); } return plans; } function toOpenError(message: string, cause: unknown): OpenError { return new OpenError({ message, cause }); } function isWindowsBatchShim(pathService: Path.Path, filePath: string): boolean { return WINDOWS_BATCH_EXTENSIONS.includes(pathService.extname(filePath).toUpperCase() as never); } function quoteForWindowsCmd(value: string): string { return `"${value.replaceAll("%", "%%").replaceAll('"', '""')}"`; } function makeWindowsCmdCommandLine(commandPath: string, args: ReadonlyArray<string>): string { return `"${[commandPath, ...args].map(quoteForWindowsCmd).join(" ")}"`; } export interface OpenShape { readonly getAvailableEditors: Effect.Effect<ReadonlyArray<EditorId>, OpenError>; readonly openExternal: (input: OpenExternalInput) => Effect.Effect<void, OpenError>; readonly openBrowser: ( target: string, options?: Omit<OpenExternalInput, "target">, ) => Effect.Effect<void, OpenError>; readonly openInEditor: (input: OpenInEditorInput) => Effect.Effect<void, OpenError>; } export class Open extends ServiceMap.Service<Open, OpenShape>()("t3/open") {} const makeOpen = (options: OpenRuntimeOptions = {}) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const isInsideContainer = options.isInsideContainer ?? (typeof options.env?.CONTAINER === "string" || typeof options.env?.container === "string" || typeof options.env?.KUBERNETES_SERVICE_HOST === "string" ? true : yield* fileSystem.exists("/.dockerenv").pipe(Effect.catch(() => Effect.succeed(false)))); const runtime: OpenRuntime = { platform: options.platform ?? process.platform, env: options.env ?? process.env, isWsl: options.isWsl ?? detectWsl(options.platform ?? process.platform, options.env ?? process.env), isInsideContainer, powerShellCandidates: options.powerShellCommand !== undefined ? [options.powerShellCommand] : (options.isWsl ?? detectWsl(options.platform ?? process.platform, options.env ?? process.env)) ? WSL_POWERSHELL_CANDIDATES : WINDOWS_POWERSHELL_CANDIDATES, windowsPathExtensions: resolveWindowsPathExtensions(options.env ?? process.env), }; const resolveCommand = Effect.fn(function* ( command: string, ): Effect.fn.Return<Option.Option<ResolvedCommand>, never> { const candidates = resolveCommandCandidates(command, runtime, pathService); const resolveExecutableFile = Effect.fn(function* ( filePath: string, ): Effect.fn.Return<Option.Option<ResolvedCommand>, never> { const info = yield* fileSystem.stat(filePath).pipe(Effect.option); if (Option.isNone(info) || info.value.type !== "File") return Option.none(); if (runtime.platform === "win32") { const extension = pathService.extname(filePath); if ( extension.length === 0 || !runtime.windowsPathExtensions.includes(extension.toUpperCase()) ) { return Option.none(); } return Option.some({ path: filePath, usesCmdWrapper: isWindowsBatchShim(pathService, filePath), } satisfies ResolvedCommand); } return (info.value.mode & 0o111) !== 0 ? Option.some({ path: filePath, usesCmdWrapper: false, } satisfies ResolvedCommand) : Option.none(); }); if (command.includes("/") || command.includes("\\")) { for (const candidate of candidates) { const resolved = yield* resolveExecutableFile(candidate); if (Option.isSome(resolved)) return resolved; } return Option.none(); } const pathValue = resolvePathEnvironmentVariable(runtime.env); if (pathValue.length === 0) return Option.none(); const pathEntries = pathValue .split(resolvePathDelimiter(runtime.platform)) .map((entry) => stripWrappingQuotes(entry.trim())) .filter((entry) => entry.length > 0); for (const pathEntry of pathEntries) { for (const candidate of candidates) { const resolved = yield* resolveExecutableFile(pathService.join(pathEntry, candidate)); if (Option.isSome(resolved)) { return resolved; } } } return Option.none(); }); const commandAvailable = (command: string) => resolveCommand(command).pipe(Effect.map(Option.isSome)); const fileManagerAvailable = Effect.gen(function* () { const candidates = runtime.platform === "darwin" ? ["open"] : runtime.platform === "win32" ? [...runtime.powerShellCandidates, "explorer"] : ["xdg-open"]; for (const candidate of candidates) { if (yield* commandAvailable(candidate)) { return true; } } return false; }); const getAvailableEditors = Effect.gen(function* () { const available: EditorId[] = []; for (const editor of EDITORS) { if (editor.id === "file-manager") { if (yield* fileManagerAvailable) { available.push(editor.id); } continue; } if (editor.command && (yield* commandAvailable(editor.command))) { available.push(editor.id); } }