959 lines · 28.8 KB · TypeScript
RawDownload
/** * 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 { spawn as spawnNodeChildProcess } from "node:child_process";import { EDITORS, type EditorId } from "@t3tools/contracts";import { Array, Effect, FileSystem, Layer, Option, Path, Scope } from "effect";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";import { LINE_COLUMN_SUFFIX_PATTERN, makeWindowsEditorProtocolTarget } from "../../editorProtocol";import {  isWindowsBatchShim,  makeWindowsCmdSpawnArguments,  resolveWindowsCommandShell,} from "../windowsCommand";import {  DesktopLauncherCommandNotFoundError,  DesktopLauncherDiscoveryError,  DesktopLauncherLaunchAttemptsExhaustedError,  DesktopLauncherNonZeroExitError,  DesktopLauncherSpawnError,  DesktopLauncherUnknownEditorError,  DesktopLauncherValidationError,  DesktopLauncher,  type DesktopLauncherShape,  type OpenApplicationInput,  type OpenExternalInput,} from "../Services/DesktopLauncher"; export interface DetachedSpawnInput {  readonly command: string;  readonly args: ReadonlyArray<string>;  readonly detached?: boolean;  readonly shell?: boolean;  readonly windowsVerbatimArguments?: boolean;  readonly windowsHide?: boolean;  readonly stdin?: "ignore";  readonly stdout?: "ignore";  readonly stderr?: "ignore";} type DesktopLauncherOperation =  | "getAvailableEditors"  | "openBrowser"  | "openExternal"  | "openInEditor"; interface LaunchContext {  readonly operation: DesktopLauncherOperation;  readonly target?: string;  readonly editor?: EditorId;} export interface LaunchRuntimeOptions {  readonly platform?: NodeJS.Platform;  readonly env?: NodeJS.ProcessEnv;  readonly isWsl?: boolean;  readonly isInsideContainer?: boolean;  readonly powerShellCommand?: string;  readonly spawnDetached?: (    input: DetachedSpawnInput,    context: LaunchContext,  ) => Effect.Effect<void, DesktopLauncherSpawnError>;} interface LaunchRuntime {  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;} interface OpenApplicationCandidate {  readonly name: string;  readonly arguments: ReadonlyArray<string>;} interface LaunchAttemptFailure {  readonly command: string;  readonly args: ReadonlyArray<string>;  readonly reason: "commandNotFound" | "spawnFailed" | "nonZeroExit";  readonly detail: string;  readonly exitCode?: number;} type LaunchAttemptError =  | DesktopLauncherCommandNotFoundError  | DesktopLauncherNonZeroExitError  | DesktopLauncherSpawnError; type LaunchPlanError = LaunchAttemptError | DesktopLauncherLaunchAttemptsExhaustedError; 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;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.dedupe(parsed) : fallback;} function resolveCommandCandidates(  command: string,  runtime: LaunchRuntime,  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.dedupe([      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.dedupe(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 quotePowerShellArgument(value: string): string {  return `"\`"${value.replaceAll("`", "``").replaceAll('"', '`"')}\`""`;} function encodePowerShellCommand(command: string): string {  return Buffer.from(command, "utf16le").toString("base64");} function normalizeAppCandidates(  app: OpenApplicationInput | ReadonlyArray<OpenApplicationInput> | undefined,): ReadonlyArray<OpenApplicationCandidate | undefined> {  if (!app) return [undefined];   const apps = Array.ensure(app);  const candidates: Array<OpenApplicationCandidate> = [];   for (const appDef of apps) {    const names = Array.ensure(appDef.name);    for (const name of names) {      candidates.push({ name, arguments: appDef.arguments ?? [] });    }  }   return candidates.length > 0 ? candidates : [undefined];} function isUriLikeTarget(target: string): boolean {  return /^[A-Za-z][A-Za-z\d+.-]*:/.test(target);} function shouldPreferWindowsOpenerOnWsl(input: OpenExternalInput, runtime: LaunchRuntime): 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: OpenApplicationCandidate,): 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 makePowerShellStartProcessArgs(  commandPath: string,  args: ReadonlyArray<string>,): ReadonlyArray<string> {  const argumentList =    args.length > 0 ? ` -ArgumentList ${args.map(quotePowerShellArgument).join(",")}` : "";  const command = `Start ${quotePowerShellArgument(commandPath)}${argumentList} -WindowStyle Hidden`;  return [    "-NoProfile",    "-NonInteractive",    "-ExecutionPolicy",    "Bypass",    "-EncodedCommand",    encodePowerShellCommand(command),  ];} 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,    detached: true,    stdio: "ignore",    shell: false,  });} function makeDirectApplicationPlan(  input: OpenExternalInput,  app: OpenApplicationCandidate,): LaunchPlan {  const wait = input.wait ?? false;  return makeLaunchPlan(app.name, [...app.arguments, input.target], {    wait,    allowNonzeroExitCode: input.allowNonzeroExitCode ?? false,    detached: !wait,    ...(wait ? {} : { stdio: "ignore" as const }),    shell: false,  });} function resolveExternalPlans(  input: OpenExternalInput,  runtime: LaunchRuntime,): 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 resolveSpawnInput(  runtime: LaunchRuntime,  plan: LaunchPlan,  resolvedCommand: ResolvedCommand,  startProcessLauncher: string | undefined,): DetachedSpawnInput {  if (plan.detached && resolvedCommand.usesCmdWrapper && startProcessLauncher !== undefined) {    return {      command: startProcessLauncher,      args: makePowerShellStartProcessArgs(resolvedCommand.path, plan.args),      ...(plan.detached !== undefined ? { detached: plan.detached } : {}),      ...(plan.shell !== undefined ? { shell: plan.shell } : {}),      ...(runtime.platform === "win32"        ? { windowsVerbatimArguments: true, windowsHide: true }        : {}),      ...(plan.stdio === "ignore"        ? {            stdin: "ignore" as const,            stdout: "ignore" as const,            stderr: "ignore" as const,          }        : {}),    };  }  const windowsHide = runtime.platform === "win32" && plan.detached ? true : undefined;  return {    command: resolvedCommand.usesCmdWrapper      ? resolveWindowsCommandShell(runtime.env)      : resolvedCommand.path,    args: resolvedCommand.usesCmdWrapper      ? makeWindowsCmdSpawnArguments(resolvedCommand.path, plan.args)      : [...plan.args],    ...(plan.detached !== undefined ? { detached: plan.detached } : {}),    ...(plan.shell !== undefined ? { shell: plan.shell } : {}),    ...(windowsHide ? { windowsHide } : {}),    ...(resolvedCommand.usesCmdWrapper ? { windowsVerbatimArguments: true } : {}),    ...(plan.stdio === "ignore"      ? {          stdin: "ignore" as const,          stdout: "ignore" as const,          stderr: "ignore" as const,        }      : {}),  };} function makeCommandNotFoundError(  context: LaunchContext,  command: string,): DesktopLauncherCommandNotFoundError {  return new DesktopLauncherCommandNotFoundError({    operation: context.operation,    command,    ...(context.target !== undefined ? { target: context.target } : {}),    ...(context.editor !== undefined ? { editor: context.editor } : {}),  });} function makeSpawnError(  context: LaunchContext,  command: string,  args: ReadonlyArray<string>,  cause?: unknown,): DesktopLauncherSpawnError {  return new DesktopLauncherSpawnError({    operation: context.operation,    command,    args: [...args],    ...(context.target !== undefined ? { target: context.target } : {}),    ...(context.editor !== undefined ? { editor: context.editor } : {}),    ...(cause !== undefined ? { cause } : {}),  });} function makeNonZeroExitError(  context: LaunchContext,  command: string,  args: ReadonlyArray<string>,  exitCode: number,): DesktopLauncherNonZeroExitError {  return new DesktopLauncherNonZeroExitError({    operation: context.operation,    command,    args: [...args],    exitCode,    ...(context.target !== undefined ? { target: context.target } : {}),    ...(context.editor !== undefined ? { editor: context.editor } : {}),  });} function toLaunchAttemptFailure(error: LaunchAttemptError): LaunchAttemptFailure {  switch (error._tag) {    case "DesktopLauncherCommandNotFoundError":      return {        command: error.command,        args: [],        reason: "commandNotFound",

Lines 1–500 of 959.