| author | |
| committer | |
| log | 408ff8ae9bd7eb2e7e90cbfd8b3fcfe63641bf23 |
| tree | 3ad0e12c3c287f83cb02df7050f3a46333777e04 |
| parent | 7a326c244f72d6421c2f1cbf004d55806c631526 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>6 files changed, 341 insertions(+), 37 deletions(-)
apps/desktop/src/ipc/methods/window.test.ts+36| ... | ... | @@ -1,4 +1,8 @@ |
| 1 | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | |
| 3 | import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 4 | import * as FileSystem from "effect/FileSystem"; | |
| 5 | import * as Path from "effect/Path"; | |
| 2 | 6 | import * as Effect from "effect/Effect"; |
| 3 | 7 | import * as Layer from "effect/Layer"; |
| 4 | 8 | import * as Option from "effect/Option"; |
| ... | ... | @@ -26,6 +30,7 @@ import { |
| 26 | 30 | getWindowFullscreenState, |
| 27 | 31 | pasteAsText, |
| 28 | 32 | pickProjectFavicon, |
| 33 | probeRemoteEditors, | |
| 29 | 34 | } from "./window.ts"; |
| 30 | 35 | |
| 31 | 36 | const readyWslConfig: DesktopBackendManager.DesktopBackendStartConfig = { |
| ... | ... | @@ -262,3 +267,34 @@ describe("pickProjectFavicon", () => { |
| 262 | 267 | }), |
| 263 | 268 | ); |
| 264 | 269 | }); |
| 270 | ||
| 271 | it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( | |
| 272 | "finds remote editors installed without PATH launchers", | |
| 273 | () => | |
| 274 | Effect.gen(function* () { | |
| 275 | const fs = yield* FileSystem.FileSystem; | |
| 276 | const path = yield* Path.Path; | |
| 277 | const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-remote-editors-" }); | |
| 278 | for (const app of ["Cursor", "Visual Studio Code", "WebStorm"]) { | |
| 279 | const executable = path.join( | |
| 280 | home, | |
| 281 | "Applications", | |
| 282 | `${app}.app`, | |
| 283 | app === "WebStorm" ? "Contents/MacOS/webstorm" : "Contents/Resources/app/bin/code", | |
| 284 | ); | |
| 285 | yield* fs.makeDirectory(path.dirname(executable), { recursive: true }); | |
| 286 | yield* fs.writeFileString(executable, "#!/bin/sh\n"); | |
| 287 | yield* fs.chmod(executable, 0o755); | |
| 288 | } | |
| 289 | const editors = yield* probeRemoteEditors.handler(undefined).pipe( | |
| 290 | Effect.provideService(HostProcessEnvironment, { | |
| 291 | HOME: home, | |
| 292 | PATH: path.join(home, "empty"), | |
| 293 | }), | |
| 294 | Effect.provideService(HostProcessPlatform, "darwin"), | |
| 295 | ); | |
| 296 | assert.include(editors, "cursor"); | |
| 297 | assert.include(editors, "vscode"); | |
| 298 | assert.notInclude(editors, "webstorm"); | |
| 299 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | |
| 300 | ); |
apps/desktop/src/ipc/methods/window.ts+6-12| ... | ... | @@ -14,7 +14,8 @@ import { |
| 14 | 14 | type PickedThemeFile, |
| 15 | 15 | } from "@t3tools/contracts"; |
| 16 | 16 | import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview"; |
| 17 | import { isCommandAvailable } from "@t3tools/shared/shell"; | |
| 17 | import { resolveEditorCommand } from "@t3tools/shared/editor"; | |
| 18 | import * as HostProcess from "@t3tools/shared/hostProcess"; | |
| 18 | 19 | import * as NodeOS from "node:os"; |
| 19 | 20 | import * as FileSystem from "effect/FileSystem"; |
| 20 | 21 | import * as Path from "effect/Path"; |
| ... | ... | @@ -333,20 +334,13 @@ export const probeRemoteEditors = DesktopIpc.makeIpcMethod({ |
| 333 | 334 | channel: IpcChannels.PROBE_REMOTE_EDITORS_CHANNEL, |
| 334 | 335 | payload: Schema.Undefined, |
| 335 | 336 | result: Schema.Array(EditorId), |
| 336 | // Probes THIS machine (where the renderer runs) for remote-capable editor | |
| 337 | // CLIs, unlike the server's probe which walks the environment host's PATH. | |
| 338 | // A Finder-launched app can miss PATH entries; an empty result makes the | |
| 339 | // renderer fall back to VS Code only, so that fails soft. | |
| 340 | 337 | handler: Effect.fn("desktop.ipc.window.probeRemoteEditors")(function* () { |
| 341 | 338 | const available: Array<EditorId> = []; |
| 339 | const env = yield* HostProcess.HostProcessEnvironment; | |
| 342 | 340 | for (const editorId of REMOTE_CAPABLE_EDITOR_IDS) { |
| 343 | const commands = EDITORS.find((editor) => editor.id === editorId)?.commands; | |
| 344 | if (!commands) continue; | |
| 345 | for (const command of commands) { | |
| 346 | if (yield* isCommandAvailable(command, { env: process.env })) { | |
| 347 | available.push(editorId); | |
| 348 | break; | |
| 349 | } | |
| 341 | const editor = EDITORS.find((editor) => editor.id === editorId); | |
| 342 | if (editor && Option.isSome(yield* resolveEditorCommand(editor, env))) { | |
| 343 | available.push(editorId); | |
| 350 | 344 | } |
| 351 | 345 | } |
| 352 | 346 | return available; |
apps/server/src/process/externalLauncher.test.ts+153| ... | ... | @@ -922,6 +922,159 @@ it.effect("discovers editors through the service API", () => |
| 922 | 922 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 923 | 923 | ); |
| 924 | 924 | |
| 925 | for (const { platform, installPath, editor, args } of [ | |
| 926 | { | |
| 927 | platform: "darwin", | |
| 928 | installPath: "Applications/Cursor.app/Contents/Resources/app/bin/code", | |
| 929 | editor: "cursor", | |
| 930 | args: ["--classic", "--goto", "/workspace with spaces/file.ts:12:4"], | |
| 931 | }, | |
| 932 | { | |
| 933 | platform: "darwin", | |
| 934 | installPath: "Applications/Kiro.app/Contents/Resources/app/bin/code", | |
| 935 | editor: "kiro", | |
| 936 | args: ["--goto", "/workspace with spaces/file.ts:12:4"], | |
| 937 | }, | |
| 938 | { | |
| 939 | platform: "darwin", | |
| 940 | installPath: "Applications/WebStorm.app/Contents/MacOS/webstorm", | |
| 941 | editor: "webstorm", | |
| 942 | args: ["--line", "12", "--column", "4", "/workspace with spaces/file.ts"], | |
| 943 | }, | |
| 944 | { | |
| 945 | platform: "darwin", | |
| 946 | installPath: "Applications/Zed.app/Contents/MacOS/cli", | |
| 947 | editor: "zed", | |
| 948 | args: ["/workspace with spaces/file.ts:12:4"], | |
| 949 | }, | |
| 950 | { | |
| 951 | platform: "win32", | |
| 952 | installPath: "Programs/Cursor/resources/app/bin/cursor.cmd", | |
| 953 | editor: "cursor", | |
| 954 | args: ['^"--classic^"', '^"--goto^"', '^"/workspace^ with^ spaces/file.ts:12:4^"'], | |
| 955 | }, | |
| 956 | { | |
| 957 | platform: "win32", | |
| 958 | installPath: "Programs/Microsoft VS Code/bin/code.cmd", | |
| 959 | editor: "vscode", | |
| 960 | args: ['^"--goto^"', '^"/workspace^ with^ spaces/file.ts:12:4^"'], | |
| 961 | }, | |
| 962 | { | |
| 963 | platform: "win32", | |
| 964 | installPath: "Programs/JetBrains/WebStorm 2026.2/bin/webstorm64.exe", | |
| 965 | editor: "webstorm", | |
| 966 | args: ["--line", "12", "--column", "4", "/workspace with spaces/file.ts"], | |
| 967 | }, | |
| 968 | { | |
| 969 | platform: "win32", | |
| 970 | installPath: "Programs/WebStorm/bin/webstorm64.exe", | |
| 971 | editor: "webstorm", | |
| 972 | args: ["--line", "12", "--column", "4", "/workspace with spaces/file.ts"], | |
| 973 | }, | |
| 974 | { | |
| 975 | platform: "win32", | |
| 976 | installPath: "Programs/Zed/bin/zed.exe", | |
| 977 | editor: "zed", | |
| 978 | args: ["/workspace with spaces/file.ts:12:4"], | |
| 979 | }, | |
| 980 | { | |
| 981 | platform: "linux", | |
| 982 | installPath: ".local/share/JetBrains/Toolbox/scripts/idea", | |
| 983 | editor: "idea", | |
| 984 | args: ["--line", "12", "--column", "4", "/workspace with spaces/file.ts"], | |
| 985 | }, | |
| 986 | { | |
| 987 | platform: "linux", | |
| 988 | installPath: ".local/bin/zed", | |
| 989 | editor: "zed", | |
| 990 | args: ["/workspace with spaces/file.ts:12:4"], | |
| 991 | }, | |
| 992 | ] as const) { | |
| 993 | it.effect.skipIf(windowsHost && platform !== "win32")( | |
| 994 | `discovers and launches ${editor} outside PATH on ${platform}`, | |
| 995 | () => | |
| 996 | Effect.gen(function* () { | |
| 997 | const fs = yield* FileSystem.FileSystem; | |
| 998 | const path = yield* Path.Path; | |
| 999 | const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editor installs-" }); | |
| 1000 | const executable = path.join(home, installPath); | |
| 1001 | yield* fs.makeDirectory(path.dirname(executable), { recursive: true }); | |
| 1002 | yield* fs.writeFileString(executable, "#!/bin/sh\n"); | |
| 1003 | yield* fs.chmod(executable, 0o755); | |
| 1004 | let spawned: ChildProcess.StandardCommand | undefined; | |
| 1005 | yield* Effect.gen(function* () { | |
| 1006 | const launcher = yield* ExternalLauncher.ExternalLauncher; | |
| 1007 | assert.include(yield* launcher.resolveAvailableEditors(), editor); | |
| 1008 | yield* launcher.launchEditor({ editor, cwd: "/workspace with spaces/file.ts:12:4" }); | |
| 1009 | }).pipe( | |
| 1010 | Effect.provide( | |
| 1011 | testLayer({ | |
| 1012 | platform, | |
| 1013 | env: { HOME: home, LOCALAPPDATA: home, PATH: path.join(home, "empty") }, | |
| 1014 | onSpawn: (command) => { | |
| 1015 | spawned = command; | |
| 1016 | }, | |
| 1017 | }), | |
| 1018 | ), | |
| 1019 | ); | |
| 1020 | assert.ok(spawned); | |
| 1021 | assert.equal( | |
| 1022 | spawned.command, | |
| 1023 | executable.endsWith(".cmd") ? `^"${executable.replaceAll(" ", "^ ")}^"` : executable, | |
| 1024 | ); | |
| 1025 | assert.deepEqual(spawned.args, args); | |
| 1026 | assert.equal(spawned.options.shell, executable.endsWith(".cmd")); | |
| 1027 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | |
| 1028 | ); | |
| 1029 | } | |
| 1030 | ||
| 1031 | it.effect.skipIf(windowsHost)("ignores unusable app bundles and keeps PATH launchers first", () => | |
| 1032 | Effect.gen(function* () { | |
| 1033 | const fs = yield* FileSystem.FileSystem; | |
| 1034 | const path = yield* Path.Path; | |
| 1035 | const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editor-priority-" }); | |
| 1036 | const executable = path.join(home, "Applications/Cursor.app/Contents/Resources/app/bin/code"); | |
| 1037 | const env = { HOME: home, PATH: path.join(home, "bin") }; | |
| 1038 | const discover = Effect.gen(function* () { | |
| 1039 | const launcher = yield* ExternalLauncher.ExternalLauncher; | |
| 1040 | return yield* launcher.resolveAvailableEditors(); | |
| 1041 | }); | |
| 1042 | const before = yield* discover.pipe(Effect.provide(testLayer({ platform: "darwin", env }))); | |
| 1043 | yield* fs.makeDirectory(executable, { recursive: true }); | |
| 1044 | assert.deepEqual( | |
| 1045 | yield* discover.pipe(Effect.provide(testLayer({ platform: "darwin", env }))), | |
| 1046 | before, | |
| 1047 | ); | |
| 1048 | yield* fs.remove(executable, { recursive: true }); | |
| 1049 | yield* fs.writeFileString(executable, "#!/bin/sh\n"); | |
| 1050 | yield* fs.chmod(executable, 0o644); | |
| 1051 | assert.deepEqual( | |
| 1052 | yield* discover.pipe(Effect.provide(testLayer({ platform: "darwin", env }))), | |
| 1053 | before, | |
| 1054 | ); | |
| 1055 | yield* fs.chmod(executable, 0o755); | |
| 1056 | yield* fs.makeDirectory(env.PATH); | |
| 1057 | yield* fs.writeFileString(path.join(env.PATH, "cursor"), "#!/bin/sh\n"); | |
| 1058 | yield* fs.chmod(path.join(env.PATH, "cursor"), 0o755); | |
| 1059 | let spawned: ChildProcess.StandardCommand | undefined; | |
| 1060 | yield* Effect.gen(function* () { | |
| 1061 | const launcher = yield* ExternalLauncher.ExternalLauncher; | |
| 1062 | yield* launcher.launchEditor({ editor: "cursor", cwd: "/workspace" }); | |
| 1063 | }).pipe( | |
| 1064 | Effect.provide( | |
| 1065 | testLayer({ | |
| 1066 | platform: "darwin", | |
| 1067 | env: { ...env, PATH: `${env.PATH}:/usr/bin` }, | |
| 1068 | onSpawn: (command) => { | |
| 1069 | spawned = command; | |
| 1070 | }, | |
| 1071 | }), | |
| 1072 | ), | |
| 1073 | ); | |
| 1074 | assert.equal(spawned?.command, "cursor"); | |
| 1075 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | |
| 1076 | ); | |
| 1077 | ||
| 925 | 1078 | it.effect("memoizes editor discovery and refreshes after the cache window", () => { |
| 926 | 1079 | let statCalls = 0; |
| 927 | 1080 | const fileInfo = { type: "File" } as FileSystem.File.Info; |
apps/server/src/process/externalLauncher.ts+15-25| ... | ... | @@ -18,6 +18,7 @@ import { |
| 18 | 18 | type FileManagerRevealKind, |
| 19 | 19 | type LaunchEditorInput, |
| 20 | 20 | } from "@t3tools/contracts"; |
| 21 | import { resolveEditorCommand } from "@t3tools/shared/editor"; | |
| 21 | 22 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 22 | 23 | import { isCommandAvailable, resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 23 | 24 | import * as Clock from "effect/Clock"; |
| ... | ... | @@ -109,6 +110,12 @@ const CommandLookupEnvConfig = Config.all({ |
| 109 | 110 | Path: Config.String("Path").pipe(Config.option), |
| 110 | 111 | path: Config.String("path").pipe(Config.option), |
| 111 | 112 | PATHEXT: Config.String("PATHEXT").pipe(Config.option), |
| 113 | HOME: Config.String("HOME").pipe(Config.option), | |
| 114 | LOCALAPPDATA: Config.String("LOCALAPPDATA").pipe(Config.option), | |
| 115 | ProgramFiles: Config.String("ProgramFiles").pipe(Config.option), | |
| 116 | ProgramW6432: Config.String("ProgramW6432").pipe(Config.option), | |
| 117 | XDG_DATA_HOME: Config.String("XDG_DATA_HOME").pipe(Config.option), | |
| 118 | "ProgramFiles(x86)": Config.String("ProgramFiles(x86)").pipe(Config.option), | |
| 112 | 119 | }).pipe(Config.map(compactEnv)); |
| 113 | 120 | |
| 114 | 121 | const readBrowserLaunchEnv = BrowserLaunchEnvConfig.pipe(Effect.orElseSucceed(() => ({}))); |
| ... | ... | @@ -154,26 +161,6 @@ function resolveCommandEditorArgs( |
| 154 | 161 | } |
| 155 | 162 | } |
| 156 | 163 | |
| 157 | function resolveEditorArgs( | |
| 158 | editor: (typeof EDITORS)[number], | |
| 159 | target: string, | |
| 160 | ): ReadonlyArray<string> { | |
| 161 | const baseArgs = "baseArgs" in editor ? editor.baseArgs : []; | |
| 162 | return [...baseArgs, ...resolveCommandEditorArgs(editor, target)]; | |
| 163 | } | |
| 164 | ||
| 165 | const resolveAvailableCommand = Effect.fn("externalLauncher.resolveAvailableCommand")(function* ( | |
| 166 | commands: ReadonlyArray<string>, | |
| 167 | env: NodeJS.ProcessEnv, | |
| 168 | ): Effect.fn.Return<Option.Option<string>, never, FileSystem.FileSystem | Path.Path> { | |
| 169 | for (const command of commands) { | |
| 170 | if (yield* isCommandAvailable(command, { env })) { | |
| 171 | return Option.some(command); | |
| 172 | } | |
| 173 | } | |
| 174 | return Option.none(); | |
| 175 | }); | |
| 176 | ||
| 177 | 164 | function encodeUtf16LeBase64(input: string): string { |
| 178 | 165 | const bytes = new Uint8Array(input.length * 2); |
| 179 | 166 | for (let index = 0; index < input.length; index += 1) { |
| ... | ... | @@ -435,7 +422,7 @@ const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors" |
| 435 | 422 | continue; |
| 436 | 423 | } |
| 437 | 424 | |
| 438 | const command = yield* resolveAvailableCommand(editor.commands, env); | |
| 425 | const command = yield* resolveEditorCommand(editor, env); | |
| 439 | 426 | if (Option.isSome(command)) { |
| 440 | 427 | available.push(editor.id); |
| 441 | 428 | } |
| ... | ... | @@ -538,15 +525,18 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( |
| 538 | 525 | } |
| 539 | 526 | |
| 540 | 527 | if (editorDef.commands) { |
| 541 | const command = Option.getOrElse( | |
| 542 | yield* resolveAvailableCommand(editorDef.commands, env), | |
| 543 | () => editorDef.commands[0], | |
| 528 | const { command, baseArgs } = Option.getOrElse( | |
| 529 | yield* resolveEditorCommand(editorDef, env), | |
| 530 | () => ({ | |
| 531 | command: editorDef.commands[0], | |
| 532 | baseArgs: "baseArgs" in editorDef ? editorDef.baseArgs : [], | |
| 533 | }), | |
| 544 | 534 | ); |
| 545 | 535 | return { |
| 546 | 536 | editor: editorDef.id, |
| 547 | 537 | target: input.cwd, |
| 548 | 538 | command, |
| 549 | args: resolveEditorArgs(editorDef, input.cwd), | |
| 539 | args: [...baseArgs, ...resolveCommandEditorArgs(editorDef, input.cwd)], | |
| 550 | 540 | }; |
| 551 | 541 | } |
| 552 | 542 |
packages/shared/package.json+4| ... | ... | @@ -63,6 +63,10 @@ |
| 63 | 63 | "types": "./src/httpObservability.ts", |
| 64 | 64 | "import": "./src/httpObservability.ts" |
| 65 | 65 | }, |
| 66 | "./editor": { | |
| 67 | "types": "./src/editor.ts", | |
| 68 | "import": "./src/editor.ts" | |
| 69 | }, | |
| 66 | 70 | "./shell": { |
| 67 | 71 | "types": "./src/shell.ts", |
| 68 | 72 | "import": "./src/shell.ts" |
packages/shared/src/editor.ts created+127| ... | ... | @@ -0,0 +1,127 @@ |
| 1 | import type { EDITORS } from "@t3tools/contracts"; | |
| 2 | import * as Effect from "effect/Effect"; | |
| 3 | import * as FileSystem from "effect/FileSystem"; | |
| 4 | import * as Option from "effect/Option"; | |
| 5 | import * as Path from "effect/Path"; | |
| 6 | ||
| 7 | import * as HostProcess from "./hostProcess.ts"; | |
| 8 | import { isCommandAvailable } from "./shell.ts"; | |
| 9 | ||
| 10 | type Editor = (typeof EDITORS)[number]; | |
| 11 | ||
| 12 | const installNames: Partial<Record<Editor["id"], ReadonlyArray<string>>> = { | |
| 13 | vscode: ["Visual Studio Code"], | |
| 14 | "vscode-insiders": ["Visual Studio Code - Insiders"], | |
| 15 | idea: ["IntelliJ IDEA", "IntelliJ IDEA CE", "IntelliJ IDEA Ultimate"], | |
| 16 | pycharm: ["PyCharm", "PyCharm CE"], | |
| 17 | rider: ["Rider", "JetBrains Rider"], | |
| 18 | }; | |
| 19 | ||
| 20 | export const resolveEditorCommand = Effect.fn("editor.resolveEditorCommand")(function* ( | |
| 21 | editor: Editor, | |
| 22 | env: NodeJS.ProcessEnv, | |
| 23 | ) { | |
| 24 | if (editor.commands === null) return Option.none(); | |
| 25 | const baseArgs = "baseArgs" in editor ? editor.baseArgs : []; | |
| 26 | for (const command of editor.commands) { | |
| 27 | if (yield* isCommandAvailable(command, { env })) return Option.some({ command, baseArgs }); | |
| 28 | } | |
| 29 | ||
| 30 | const platform = yield* HostProcess.HostProcessPlatform; | |
| 31 | const path = yield* Path.Path; | |
| 32 | const fs = yield* FileSystem.FileSystem; | |
| 33 | const home = env.HOME; | |
| 34 | const names = installNames[editor.id] ?? [editor.label]; | |
| 35 | const command = editor.commands[0]; | |
| 36 | const jetbrains = editor.launchStyle === "line-column"; | |
| 37 | const candidates: string[] = []; | |
| 38 | ||
| 39 | if (platform === "darwin") { | |
| 40 | const roots = [...(home ? [path.join(home, "Applications")] : []), "/Applications"]; | |
| 41 | for (const root of roots) { | |
| 42 | for (const name of names) { | |
| 43 | const contents = path.join(root, `${name}.app`, "Contents"); | |
| 44 | candidates.push( | |
| 45 | ...(jetbrains || editor.id === "zed" | |
| 46 | ? [path.join(contents, "MacOS", editor.id === "zed" ? "cli" : command)] | |
| 47 | : [ | |
| 48 | path.join(contents, "Resources/app/bin", command), | |
| 49 | path.join(contents, "Resources/app/bin/code"), | |
| 50 | ]), | |
| 51 | ); | |
| 52 | } | |
| 53 | } | |
| 54 | if (home && jetbrains) { | |
| 55 | candidates.push( | |
| 56 | path.join(home, "Library/Application Support/JetBrains/Toolbox/scripts", command), | |
| 57 | ); | |
| 58 | } | |
| 59 | } else if (platform === "win32") { | |
| 60 | const roots = [ | |
| 61 | ...(env.LOCALAPPDATA ? [path.join(env.LOCALAPPDATA, "Programs")] : []), | |
| 62 | ...[env.ProgramFiles, env["ProgramFiles(x86)"], env.ProgramW6432].filter( | |
| 63 | (root): root is string => !!root, | |
| 64 | ), | |
| 65 | ]; | |
| 66 | if (jetbrains) { | |
| 67 | if (env.LOCALAPPDATA) { | |
| 68 | candidates.push(path.join(env.LOCALAPPDATA, "JetBrains/Toolbox/scripts", `${command}.cmd`)); | |
| 69 | } | |
| 70 | for (const directory of roots.flatMap((root) => [root, path.join(root, "JetBrains")])) { | |
| 71 | const entries = yield* fs.readDirectory(directory).pipe(Effect.orElseSucceed(() => [])); | |
| 72 | for (const entry of entries) { | |
| 73 | if (names.some((name) => entry === name || entry.startsWith(`${name} `))) { | |
| 74 | candidates.push(path.join(directory, entry, "bin", `${command}64.exe`)); | |
| 75 | candidates.push(path.join(directory, entry, "bin", `${command}.exe`)); | |
| 76 | } | |
| 77 | } | |
| 78 | } | |
| 79 | } else { | |
| 80 | const name = | |
| 81 | editor.id === "vscode" | |
| 82 | ? "Microsoft VS Code" | |
| 83 | : editor.id === "vscode-insiders" | |
| 84 | ? "Microsoft VS Code Insiders" | |
| 85 | : editor.label; | |
| 86 | for (const root of roots) { | |
| 87 | candidates.push( | |
| 88 | path.join(root, name, "resources/app/bin", `${command}.cmd`), | |
| 89 | path.join(root, name, "resources/app/bin/code.cmd"), | |
| 90 | path.join(root, name, "bin", `${command}.cmd`), | |
| 91 | path.join(root, name, "bin/code.cmd"), | |
| 92 | ); | |
| 93 | if (editor.id === "zed") { | |
| 94 | candidates.push( | |
| 95 | path.join(root, name, "bin", "zed.exe"), | |
| 96 | path.join(root, name, "zed.exe"), | |
| 97 | ); | |
| 98 | } | |
| 99 | } | |
| 100 | } | |
| 101 | } else if (platform === "linux") { | |
| 102 | const dirs = [ | |
| 103 | ...(home ? [path.join(home, ".local/bin")] : []), | |
| 104 | "/usr/local/bin", | |
| 105 | "/usr/bin", | |
| 106 | "/snap/bin", | |
| 107 | ]; | |
| 108 | if (jetbrains) { | |
| 109 | const dataHome = env.XDG_DATA_HOME || (home ? path.join(home, ".local/share") : undefined); | |
| 110 | if (dataHome) dirs.push(path.join(dataHome, "JetBrains/Toolbox/scripts")); | |
| 111 | } | |
| 112 | for (const dir of dirs) { | |
| 113 | for (const name of editor.commands) candidates.push(path.join(dir, name)); | |
| 114 | } | |
| 115 | } | |
| 116 | ||
| 117 | for (const candidate of candidates) { | |
| 118 | if (yield* isCommandAvailable(candidate, { env })) { | |
| 119 | return Option.some({ | |
| 120 | command: candidate, | |
| 121 | baseArgs: | |
| 122 | editor.id === "kiro" && (platform === "darwin" || platform === "win32") ? [] : baseArgs, | |
| 123 | }); | |
| 124 | } | |
| 125 | } | |
| 126 | return Option.none(); | |
| 127 | }); |