/** * ExternalLauncher - external application launch service interface. * * Owns process launch helpers for browser URLs and workspace paths * in configured editor integrations. * * @module ExternalLauncher */import { EDITORS, ExternalLauncherError, type EditorId, type LaunchEditorInput,} from "@t3tools/contracts";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import { isCommandAvailable, resolveSpawnCommand } from "@t3tools/shared/shell";import * as Config from "effect/Config";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; // ==============================// Definitions// ============================== export { ExternalLauncherError };export type { LaunchEditorInput };interface EditorLaunch { readonly command: string; readonly args: ReadonlyArray<string>;} interface ProcessLaunch { readonly command: string; readonly args: ReadonlyArray<string>; readonly options: ChildProcess.CommandOptions;} interface TargetPathAndPosition { readonly path: string; readonly line: string; readonly column: Option.Option<string>;} const TARGET_WITH_POSITION_PATTERN = /^(.*?):(\d+)(?::(\d+))?$/;const POWERSHELL_ARGUMENTS_PREFIX = [ "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand",] as const; const DETACHED_IGNORE_STDIO_OPTIONS = { detached: true, stdin: "ignore", stdout: "ignore", stderr: "ignore",} as const satisfies ChildProcess.CommandOptions; const compactEnv = (input: Record<string, Option.Option<string>>): NodeJS.ProcessEnv => Object.fromEntries( Object.entries(input).flatMap(([key, value]) => Option.match(value, { onNone: () => [], onSome: (resolved) => [[key, resolved]], }), ), ); const BrowserLaunchEnvConfig = Config.all({ SYSTEMROOT: Config.string("SYSTEMROOT").pipe(Config.option), windir: Config.string("windir").pipe(Config.option), WSL_DISTRO_NAME: Config.string("WSL_DISTRO_NAME").pipe(Config.option), WSL_INTEROP: Config.string("WSL_INTEROP").pipe(Config.option), SSH_CONNECTION: Config.string("SSH_CONNECTION").pipe(Config.option), SSH_TTY: Config.string("SSH_TTY").pipe(Config.option), container: Config.string("container").pipe(Config.option),}).pipe(Config.map(compactEnv)); const CommandLookupEnvConfig = Config.all({ PATH: Config.string("PATH").pipe(Config.option), Path: Config.string("Path").pipe(Config.option), path: Config.string("path").pipe(Config.option), PATHEXT: Config.string("PATHEXT").pipe(Config.option),}).pipe(Config.map(compactEnv)); const readBrowserLaunchEnv = BrowserLaunchEnvConfig.pipe(Effect.orElseSucceed(() => ({})));const readCommandLookupEnv = CommandLookupEnvConfig.pipe(Effect.orElseSucceed(() => ({}))); function parseTargetPathAndPosition(target: string): Option.Option<TargetPathAndPosition> { const match = TARGET_WITH_POSITION_PATTERN.exec(target); if (!match?.[1] || !match[2]) { return Option.none(); } return Option.some({ path: match[1], line: match[2], column: Option.fromUndefinedOr(match[3]), });} function resolveCommandEditorArgs( editor: (typeof EDITORS)[number], target: string,): ReadonlyArray<string> { const parsedTarget = parseTargetPathAndPosition(target); switch (editor.launchStyle) { case "direct-path": return [target]; case "goto": return Option.isSome(parsedTarget) ? ["--goto", target] : [target]; case "line-column": return Option.match(parsedTarget, { onNone: () => [target], onSome: ({ path, line, column }) => [ "--line", line, ...Option.match(column, { onNone: () => [], onSome: (value) => ["--column", value], }), path, ], }); }} function resolveEditorArgs( editor: (typeof EDITORS)[number], target: string,): ReadonlyArray<string> { const baseArgs = "baseArgs" in editor ? editor.baseArgs : []; return [...baseArgs, ...resolveCommandEditorArgs(editor, target)];} const resolveAvailableCommand = Effect.fn("externalLauncher.resolveAvailableCommand")(function* ( commands: ReadonlyArray<string>, env: NodeJS.ProcessEnv,): Effect.fn.Return<Option.Option<string>, never, FileSystem.FileSystem | Path.Path> { for (const command of commands) { if (yield* isCommandAvailable(command, { env })) { return Option.some(command); } } return Option.none();}); function encodeUtf16LeBase64(input: string): string { const bytes = new Uint8Array(input.length * 2); for (let index = 0; index < input.length; index += 1) { const code = input.charCodeAt(index); bytes[index * 2] = code & 0xff; bytes[index * 2 + 1] = code >>> 8; } return Encoding.encodeBase64(bytes);} function escapePowerShellStringLiteral(input: string): string { return `'${input.replaceAll("'", "''")}'`;} function resolvePowerShellPath(env: NodeJS.ProcessEnv = {}): string { return `${env.SYSTEMROOT || env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;} function resolveWslPowerShellPath(): string { return "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";} function shouldUseWindowsBrowserFromWsl( platform: NodeJS.Platform, env: NodeJS.ProcessEnv = {},): boolean { return ( platform === "linux" && (env.WSL_DISTRO_NAME !== undefined || env.WSL_INTEROP !== undefined) && env.SSH_CONNECTION === undefined && env.SSH_TTY === undefined && env.container === undefined );} function resolveWindowsBrowserLaunch(target: string, command: string): ProcessLaunch { const encodedCommand = encodeUtf16LeBase64( `$ProgressPreference = 'SilentlyContinue'; Start ${escapePowerShellStringLiteral(target)}`, ); return { command, args: [...POWERSHELL_ARGUMENTS_PREFIX, encodedCommand], options: { detached: true, shell: false, stdin: "ignore", stdout: "ignore", stderr: "ignore", }, };} function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { switch (platform) { case "darwin": return "open"; case "win32": return "explorer"; default: return "xdg-open"; }} function buildBrowserLaunch( target: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv = {},): ProcessLaunch { if (platform === "darwin") { return { command: "open", args: [target], options: DETACHED_IGNORE_STDIO_OPTIONS, }; } if (platform === "win32") { return resolveWindowsBrowserLaunch(target, resolvePowerShellPath(env)); } if (shouldUseWindowsBrowserFromWsl(platform, env)) { return resolveWindowsBrowserLaunch(target, resolveWslPowerShellPath()); } return { command: "xdg-open", args: [target], options: DETACHED_IGNORE_STDIO_OPTIONS, };} const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors")(function* ( platform: NodeJS.Platform, env: NodeJS.ProcessEnv,): Effect.fn.Return<ReadonlyArray<EditorId>, never, FileSystem.FileSystem | Path.Path> { const available: EditorId[] = []; for (const editor of EDITORS) { if (editor.commands === null) { const command = fileManagerCommandForPlatform(platform); if (yield* isCommandAvailable(command, { env })) { available.push(editor.id); } continue; } const command = yield* resolveAvailableCommand(editor.commands, env); if (Option.isSome(command)) { available.push(editor.id); } } return available;}); const resolveBrowserLaunch = Effect.fn("externalLauncher.resolveBrowserLaunch")(function* ( target: string,) { const platform = yield* HostProcessPlatform; const env = yield* readBrowserLaunchEnv; return buildBrowserLaunch(target, platform, env);}); const resolveAvailableEditors = Effect.fn("externalLauncher.resolveAvailableEditors")(function* () { const platform = yield* HostProcessPlatform; const env = yield* readCommandLookupEnv; return yield* buildAvailableEditors(platform, env);}); /** * ExternalLauncherShape - Service API for browser and editor launch actions. */export interface ExternalLauncherShape { readonly resolveAvailableEditors: () => Effect.Effect<ReadonlyArray<EditorId>>; /** * Launch a URL target in the default browser. */ readonly launchBrowser: (target: string) => Effect.Effect<void, ExternalLauncherError>; /** * Launch a workspace path in a selected editor integration. * * Launches the editor as a detached process so server startup is not blocked. */ readonly launchEditor: (input: LaunchEditorInput) => Effect.Effect<void, ExternalLauncherError>;} /** * ExternalLauncher - Service tag for browser/editor launch operations. */export class ExternalLauncher extends Context.Service<ExternalLauncher, ExternalLauncherShape>()( "t3/process/externalLauncher",) {} // ==============================// Implementations// ============================== const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( input: LaunchEditorInput,): Effect.fn.Return<EditorLaunch, ExternalLauncherError, FileSystem.FileSystem | Path.Path> { const platform = yield* HostProcessPlatform; const env = yield* readCommandLookupEnv; yield* Effect.annotateCurrentSpan({ "externalLauncher.editor": input.editor, "externalLauncher.cwd": input.cwd, "externalLauncher.platform": platform, }); const editorDef = EDITORS.find((editor) => editor.id === input.editor); if (!editorDef) { return yield* new ExternalLauncherError({ message: `Unknown editor: ${input.editor}` }); } if (editorDef.commands) { const command = Option.getOrElse( yield* resolveAvailableCommand(editorDef.commands, env), () => editorDef.commands[0], ); return { command, args: resolveEditorArgs(editorDef, input.cwd), }; } if (editorDef.id !== "file-manager") { return yield* new ExternalLauncherError({ message: `Unsupported editor: ${input.editor}` }); } return { command: fileManagerCommandForPlatform(platform), args: [input.cwd] };}); const launchAndUnref = Effect.fn("externalLauncher.launchAndUnref")(function* ( launch: ProcessLaunch, errorMessage: string,): Effect.fn.Return<void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner> { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const command = ChildProcess.make(launch.command, launch.args, launch.options); yield* spawner.spawn(command).pipe( Effect.flatMap((handle) => handle.unref), Effect.asVoid, Effect.scoped, Effect.mapError((cause) => new ExternalLauncherError({ message: errorMessage, cause })), );}); const launchBrowser = Effect.fn("externalLauncher.launchBrowser")(function* ( target: string,): Effect.fn.Return<void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner> { const launch = yield* resolveBrowserLaunch(target); return yield* launchAndUnref(launch, "Browser auto-open failed");}); const launchEditorProcess = Effect.fn("externalLauncher.launchEditorProcess")(function* ( launch: EditorLaunch,): Effect.fn.Return< void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path> { const env = yield* readCommandLookupEnv; if (!(yield* isCommandAvailable(launch.command, { env }))) { return yield* new ExternalLauncherError({ message: `Editor command not found: ${launch.command}`, }); } const spawnCommand = yield* resolveSpawnCommand(launch.command, launch.args, { env }); yield* launchAndUnref( { command: spawnCommand.command, args: spawnCommand.args, options: { detached: true, shell: spawnCommand.shell, stdin: "ignore", stdout: "ignore", stderr: "ignore", }, }, "failed to spawn detached process", );}); const make = Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const provideCommandResolutionServices = <A, E, R>( effect: Effect.Effect<A, E, R | FileSystem.FileSystem | Path.Path>, ) => effect.pipe( Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), ); return { resolveAvailableEditors: () => provideCommandResolutionServices(resolveAvailableEditors()), launchBrowser: (target) => launchBrowser(target).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), ), launchEditor: (input) => provideCommandResolutionServices( Effect.flatMap(resolveEditorLaunch(input), (launch) => launchEditorProcess(launch).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), ), ), ), } satisfies ExternalLauncherShape;}); export const layer = Layer.effect(ExternalLauncher, make);
/**
* ExternalLauncher - external application launch service interface.
*
* Owns process launch helpers for browser URLs and workspace paths
* in configured editor integrations.
*
* @module ExternalLauncher
*/
import {
EDITORS,
ExternalLauncherError,
type EditorId,
type LaunchEditorInput,
} from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { isCommandAvailable, resolveSpawnCommand } from "@t3tools/shared/shell";
import * as Config from "effect/Config";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Encoding from "effect/Encoding";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
// ==============================
// Definitions
// ==============================
export { ExternalLauncherError };
export type { LaunchEditorInput };
interface EditorLaunch {
readonly command: string;
readonly args: ReadonlyArray<string>;
}
interface ProcessLaunch {
readonly command: string;
readonly args: ReadonlyArray<string>;
readonly options: ChildProcess.CommandOptions;
}
interface TargetPathAndPosition {
readonly path: string;
readonly line: string;
readonly column: Option.Option<string>;
}
const TARGET_WITH_POSITION_PATTERN = /^(.*?):(\d+)(?::(\d+))?$/;
const POWERSHELL_ARGUMENTS_PREFIX = [
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-EncodedCommand",
] as const;
const DETACHED_IGNORE_STDIO_OPTIONS = {
detached: true,
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
} as const satisfies ChildProcess.CommandOptions;
const compactEnv = (input: Record<string, Option.Option<string>>): NodeJS.ProcessEnv =>
Object.fromEntries(
Object.entries(input).flatMap(([key, value]) =>
Option.match(value, {
onNone: () => [],
onSome: (resolved) => [[key, resolved]],
}),
),
);
const BrowserLaunchEnvConfig = Config.all({
SYSTEMROOT: Config.string("SYSTEMROOT").pipe(Config.option),
windir: Config.string("windir").pipe(Config.option),
WSL_DISTRO_NAME: Config.string("WSL_DISTRO_NAME").pipe(Config.option),
WSL_INTEROP: Config.string("WSL_INTEROP").pipe(Config.option),
SSH_CONNECTION: Config.string("SSH_CONNECTION").pipe(Config.option),
SSH_TTY: Config.string("SSH_TTY").pipe(Config.option),
container: Config.string("container").pipe(Config.option),
}).pipe(Config.map(compactEnv));
const CommandLookupEnvConfig = Config.all({
PATH: Config.string("PATH").pipe(Config.option),
Path: Config.string("Path").pipe(Config.option),
path: Config.string("path").pipe(Config.option),
PATHEXT: Config.string("PATHEXT").pipe(Config.option),
}).pipe(Config.map(compactEnv));
const readBrowserLaunchEnv = BrowserLaunchEnvConfig.pipe(Effect.orElseSucceed(() => ({})));
const readCommandLookupEnv = CommandLookupEnvConfig.pipe(Effect.orElseSucceed(() => ({})));
function parseTargetPathAndPosition(target: string): Option.Option<TargetPathAndPosition> {
const match = TARGET_WITH_POSITION_PATTERN.exec(target);
if (!match?.[1] || !match[2]) {
return Option.none();
}
return Option.some({
path: match[1],
line: match[2],
column: Option.fromUndefinedOr(match[3]),
});
}
function resolveCommandEditorArgs(
editor: (typeof EDITORS)[number],
target: string,
): ReadonlyArray<string> {
const parsedTarget = parseTargetPathAndPosition(target);
switch (editor.launchStyle) {
case "direct-path":
return [target];
case "goto":
return Option.isSome(parsedTarget) ? ["--goto", target] : [target];
case "line-column":
return Option.match(parsedTarget, {
onNone: () => [target],
onSome: ({ path, line, column }) => [
"--line",
line,
...Option.match(column, {
onNone: () => [],
onSome: (value) => ["--column", value],
}),
path,
],
});
}
}
function resolveEditorArgs(
editor: (typeof EDITORS)[number],
target: string,
): ReadonlyArray<string> {
const baseArgs = "baseArgs" in editor ? editor.baseArgs : [];
return [...baseArgs, ...resolveCommandEditorArgs(editor, target)];
}
const resolveAvailableCommand = Effect.fn("externalLauncher.resolveAvailableCommand")(function* (
commands: ReadonlyArray<string>,
env: NodeJS.ProcessEnv,
): Effect.fn.Return<Option.Option<string>, never, FileSystem.FileSystem | Path.Path> {
for (const command of commands) {
if (yield* isCommandAvailable(command, { env })) {
return Option.some(command);
}
}
return Option.none();
});
function encodeUtf16LeBase64(input: string): string {
const bytes = new Uint8Array(input.length * 2);
for (let index = 0; index < input.length; index += 1) {
const code = input.charCodeAt(index);
bytes[index * 2] = code & 0xff;
bytes[index * 2 + 1] = code >>> 8;
}
return Encoding.encodeBase64(bytes);
}
function escapePowerShellStringLiteral(input: string): string {
return `'${input.replaceAll("'", "''")}'`;
}
function resolvePowerShellPath(env: NodeJS.ProcessEnv = {}): string {
return `${env.SYSTEMROOT || env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
}
function resolveWslPowerShellPath(): string {
return "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
}
function shouldUseWindowsBrowserFromWsl(
platform: NodeJS.Platform,
env: NodeJS.ProcessEnv = {},
): boolean {
return (
platform === "linux" &&
(env.WSL_DISTRO_NAME !== undefined || env.WSL_INTEROP !== undefined) &&
env.SSH_CONNECTION === undefined &&
env.SSH_TTY === undefined &&
env.container === undefined
);
}
function resolveWindowsBrowserLaunch(target: string, command: string): ProcessLaunch {
const encodedCommand = encodeUtf16LeBase64(
`$ProgressPreference = 'SilentlyContinue'; Start ${escapePowerShellStringLiteral(target)}`,
);
return {
command,
args: [...POWERSHELL_ARGUMENTS_PREFIX, encodedCommand],
options: {
detached: true,
shell: false,
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
},
};
}
function fileManagerCommandForPlatform(platform: NodeJS.Platform): string {
switch (platform) {
case "darwin":
return "open";
case "win32":
return "explorer";
default:
return "xdg-open";
}
}
function buildBrowserLaunch(
target: string,
platform: NodeJS.Platform,
env: NodeJS.ProcessEnv = {},
): ProcessLaunch {
if (platform === "darwin") {
return {
command: "open",
args: [target],
options: DETACHED_IGNORE_STDIO_OPTIONS,
};
}
if (platform === "win32") {
return resolveWindowsBrowserLaunch(target, resolvePowerShellPath(env));
}
if (shouldUseWindowsBrowserFromWsl(platform, env)) {
return resolveWindowsBrowserLaunch(target, resolveWslPowerShellPath());
}
return {
command: "xdg-open",
args: [target],
options: DETACHED_IGNORE_STDIO_OPTIONS,
};
}
const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors")(function* (
platform: NodeJS.Platform,
env: NodeJS.ProcessEnv,
): Effect.fn.Return<ReadonlyArray<EditorId>, never, FileSystem.FileSystem | Path.Path> {
const available: EditorId[] = [];
for (const editor of EDITORS) {
if (editor.commands === null) {
const command = fileManagerCommandForPlatform(platform);
if (yield* isCommandAvailable(command, { env })) {
available.push(editor.id);
}
continue;
}
const command = yield* resolveAvailableCommand(editor.commands, env);
if (Option.isSome(command)) {
available.push(editor.id);
}
}
return available;
});
const resolveBrowserLaunch = Effect.fn("externalLauncher.resolveBrowserLaunch")(function* (
target: string,
) {
const platform = yield* HostProcessPlatform;
const env = yield* readBrowserLaunchEnv;
return buildBrowserLaunch(target, platform, env);
});
const resolveAvailableEditors = Effect.fn("externalLauncher.resolveAvailableEditors")(function* () {
const platform = yield* HostProcessPlatform;
const env = yield* readCommandLookupEnv;
return yield* buildAvailableEditors(platform, env);
});
/**
* ExternalLauncherShape - Service API for browser and editor launch actions.
*/
export interface ExternalLauncherShape {
readonly resolveAvailableEditors: () => Effect.Effect<ReadonlyArray<EditorId>>;
/**
* Launch a URL target in the default browser.
*/
readonly launchBrowser: (target: string) => Effect.Effect<void, ExternalLauncherError>;
/**
* Launch a workspace path in a selected editor integration.
*
* Launches the editor as a detached process so server startup is not blocked.
*/
readonly launchEditor: (input: LaunchEditorInput) => Effect.Effect<void, ExternalLauncherError>;
}
/**
* ExternalLauncher - Service tag for browser/editor launch operations.
*/
export class ExternalLauncher extends Context.Service<ExternalLauncher, ExternalLauncherShape>()(
"t3/process/externalLauncher",
) {}
// ==============================
// Implementations
// ==============================
const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
input: LaunchEditorInput,
): Effect.fn.Return<EditorLaunch, ExternalLauncherError, FileSystem.FileSystem | Path.Path> {
const platform = yield* HostProcessPlatform;
const env = yield* readCommandLookupEnv;
yield* Effect.annotateCurrentSpan({
"externalLauncher.editor": input.editor,
"externalLauncher.cwd": input.cwd,
"externalLauncher.platform": platform,
});
const editorDef = EDITORS.find((editor) => editor.id === input.editor);
if (!editorDef) {
return yield* new ExternalLauncherError({ message: `Unknown editor: ${input.editor}` });
}
if (editorDef.commands) {
const command = Option.getOrElse(
yield* resolveAvailableCommand(editorDef.commands, env),
() => editorDef.commands[0],
);
return {
command,
args: resolveEditorArgs(editorDef, input.cwd),
};
}
if (editorDef.id !== "file-manager") {
return yield* new ExternalLauncherError({ message: `Unsupported editor: ${input.editor}` });
}
return { command: fileManagerCommandForPlatform(platform), args: [input.cwd] };
});
const launchAndUnref = Effect.fn("externalLauncher.launchAndUnref")(function* (
launch: ProcessLaunch,
errorMessage: string,
): Effect.fn.Return<void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner> {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const command = ChildProcess.make(launch.command, launch.args, launch.options);
yield* spawner.spawn(command).pipe(
Effect.flatMap((handle) => handle.unref),
Effect.asVoid,
Effect.scoped,
Effect.mapError((cause) => new ExternalLauncherError({ message: errorMessage, cause })),
);
});
const launchBrowser = Effect.fn("externalLauncher.launchBrowser")(function* (
target: string,
): Effect.fn.Return<void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner> {
const launch = yield* resolveBrowserLaunch(target);
return yield* launchAndUnref(launch, "Browser auto-open failed");
});
const launchEditorProcess = Effect.fn("externalLauncher.launchEditorProcess")(function* (
launch: EditorLaunch,
): Effect.fn.Return<
void,
ExternalLauncherError,
ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path
> {
const env = yield* readCommandLookupEnv;
if (!(yield* isCommandAvailable(launch.command, { env }))) {
return yield* new ExternalLauncherError({
message: `Editor command not found: ${launch.command}`,
});
}
const spawnCommand = yield* resolveSpawnCommand(launch.command, launch.args, { env });
yield* launchAndUnref(
{
command: spawnCommand.command,
args: spawnCommand.args,
options: {
detached: true,
shell: spawnCommand.shell,
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
},
},
"failed to spawn detached process",
);
});
const make = Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const provideCommandResolutionServices = <A, E, R>(
effect: Effect.Effect<A, E, R | FileSystem.FileSystem | Path.Path>,
) =>
effect.pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
);
return {
resolveAvailableEditors: () => provideCommandResolutionServices(resolveAvailableEditors()),
launchBrowser: (target) =>
launchBrowser(target).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
),
launchEditor: (input) =>
provideCommandResolutionServices(
Effect.flatMap(resolveEditorLaunch(input), (launch) =>
launchEditorProcess(launch).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
),
),
),
} satisfies ExternalLauncherShape;
});
export const layer = Layer.effect(ExternalLauncher, make);