From a52f3b8eb0550b5dbe93e1d0576f67873608da8d Mon Sep 17 00:00:00 2001 From: tomaspiaggio Date: Tue, 7 Apr 2026 15:32:20 -0700 Subject: [PATCH 1/2] fix: intellij not being visible even though it's installed --- apps/server/src/open.test.ts | 40 ++++++++++++++++++++++++-- apps/server/src/open.ts | 48 +++++++++++++++++++++++++++++--- packages/contracts/src/editor.ts | 11 ++++++-- 3 files changed, 90 insertions(+), 9 deletions(-) diff --git a/apps/server/src/open.test.ts b/apps/server/src/open.test.ts index 59b0239c96..cd50528a3e 100644 --- a/apps/server/src/open.test.ts +++ b/apps/server/src/open.test.ts @@ -5,10 +5,12 @@ import { FileSystem, Path, Effect } from "effect"; import { isCommandAvailable, + isAppInstalled, launchDetached, resolveAvailableEditors, resolveEditorLaunch, } from "./open"; +import {EDITORS} from "@t3tools/contracts"; it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { it.effect("returns commands for command-based editors", () => @@ -82,7 +84,8 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { const ideaLaunch = yield* resolveEditorLaunch( { cwd: "/tmp/workspace", editor: "idea" }, - "darwin", + "linux", + { PATH: "" }, ); assert.deepEqual(ideaLaunch, { command: "idea", @@ -172,7 +175,8 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { const ideaLineOnly = yield* resolveEditorLaunch( { cwd: "/tmp/workspace/AGENTS.md:48", editor: "idea" }, - "darwin", + "linux", + { PATH: "" }, ); assert.deepEqual(ideaLineOnly, { command: "idea", @@ -181,7 +185,8 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { const ideaLineAndColumn = yield* resolveEditorLaunch( { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "idea" }, - "darwin", + "linux", + { PATH: "" }, ); assert.deepEqual(ideaLineAndColumn, { command: "idea", @@ -221,6 +226,25 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { }), ); + it.effect("falls back to open -a on macOS when CLI is missing but .app is installed", () => + Effect.gen(function* () { + const idea = EDITORS.find(e => e.id === "idea") + assert.isDefined(idea); + + if (!isAppInstalled(idea, "darwin")) return; + + const launch = yield* resolveEditorLaunch( + { cwd: "/tmp/workspace", editor: "idea" }, + "darwin", + { PATH: "" }, + ); + assert.deepEqual(launch, { + command: "open", + args: ["-a", "IntelliJ IDEA", "/tmp/workspace"], + }); + }), + ); + it.effect("maps file-manager editor to OS open commands", () => Effect.gen(function* () { const launch1 = yield* resolveEditorLaunch( @@ -383,6 +407,16 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => { }), ); + it("includes editors detected via macOS .app bundle", () => { + const idea = EDITORS.find(e => e.id === "idea") + assert.isDefined(idea); + + if (!isAppInstalled(idea, "darwin")) return; + + const editors = resolveAvailableEditors("darwin", { PATH: "" }); + assert.isTrue(editors.includes("idea")); + }); + it("omits file-manager when the platform opener is unavailable", () => { const editors = resolveAvailableEditors("linux", { PATH: "", diff --git a/apps/server/src/open.ts b/apps/server/src/open.ts index ef50d3a5b8..6ee37b1966 100644 --- a/apps/server/src/open.ts +++ b/apps/server/src/open.ts @@ -10,7 +10,7 @@ import { spawn } from "node:child_process"; import { accessSync, constants, statSync } from "node:fs"; import { extname, join } from "node:path"; -import { EDITORS, OpenError, type EditorId } from "@t3tools/contracts"; +import {EDITORS, OpenError, type EditorId, EditorDefinition} from "@t3tools/contracts"; import { ServiceMap, Effect, Layer } from "effect"; // ============================== @@ -203,6 +203,31 @@ export function isCommandAvailable( return false; } +function resolveAppPaths(appName: string, platform: NodeJS.Platform): ReadonlyArray { + switch (platform) { + case "darwin": + return [`/Applications/${appName}.app`]; + default: + return []; + } +} + +export function isAppInstalled( + editor: EditorDefinition, + platform: NodeJS.Platform +): editor is EditorDefinition & {appName: string} { + if (!("appName" in editor)) return false + for (const appPath of resolveAppPaths(editor.appName, platform)) { + try { + statSync(appPath); + return true; + } catch { + // not found at this path + } + } + return false; +} + export function resolveAvailableEditors( platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env, @@ -221,6 +246,8 @@ export function resolveAvailableEditors( const command = resolveAvailableCommand(editor.commands, { platform, env }); if (command !== null) { available.push(editor.id); + } else if (isAppInstalled(editor, platform)) { + available.push(editor.id); } } @@ -269,10 +296,23 @@ export const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( } if (editorDef.commands) { - const command = - resolveAvailableCommand(editorDef.commands, { platform, env }) ?? editorDef.commands[0]; + const command = resolveAvailableCommand(editorDef.commands, { platform, env }); + if (command) { + return { + command, + args: resolveCommandEditorArgs(editorDef, input.cwd), + }; + } + + if (isAppInstalled(editorDef, platform)) { + return { + command: "open", + args: ["-a", editorDef.appName, input.cwd], + }; + } + return { - command, + command: editorDef.commands[0], args: resolveCommandEditorArgs(editorDef, input.cwd), }; } diff --git a/packages/contracts/src/editor.ts b/packages/contracts/src/editor.ts index 2ffdf4c6db..4ba2cb4bc3 100644 --- a/packages/contracts/src/editor.ts +++ b/packages/contracts/src/editor.ts @@ -4,11 +4,12 @@ import { TrimmedNonEmptyString } from "./baseSchemas"; export const EditorLaunchStyle = Schema.Literals(["direct-path", "goto", "line-column"]); export type EditorLaunchStyle = typeof EditorLaunchStyle.Type; -type EditorDefinition = { +export type EditorDefinition = { readonly id: string; readonly label: string; readonly commands: readonly [string, ...string[]] | null; readonly launchStyle: EditorLaunchStyle; + readonly appName?: string; }; export const EDITORS = [ @@ -24,7 +25,13 @@ export const EDITORS = [ { id: "vscodium", label: "VSCodium", commands: ["codium"], launchStyle: "goto" }, { id: "zed", label: "Zed", commands: ["zed", "zeditor"], launchStyle: "direct-path" }, { id: "antigravity", label: "Antigravity", commands: ["agy"], launchStyle: "goto" }, - { id: "idea", label: "IntelliJ IDEA", commands: ["idea"], launchStyle: "line-column" }, + { + id: "idea", + label: "IntelliJ IDEA", + commands: ["idea"], + launchStyle: "line-column", + appName: "IntelliJ IDEA", + }, { id: "file-manager", label: "File Manager", commands: null, launchStyle: "direct-path" }, ] as const satisfies ReadonlyArray; From bb1226e5b69934b70acfffb0e05a8f7c91d4a282 Mon Sep 17 00:00:00 2001 From: tomaspiaggio Date: Tue, 7 Apr 2026 16:16:14 -0700 Subject: [PATCH 2/2] fix: added proper argument parsing --- apps/server/src/open.test.ts | 6 +++--- apps/server/src/open.ts | 37 ++++++++++++++++-------------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/apps/server/src/open.test.ts b/apps/server/src/open.test.ts index cd50528a3e..d978cf89f0 100644 --- a/apps/server/src/open.test.ts +++ b/apps/server/src/open.test.ts @@ -228,7 +228,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { it.effect("falls back to open -a on macOS when CLI is missing but .app is installed", () => Effect.gen(function* () { - const idea = EDITORS.find(e => e.id === "idea") + const idea = EDITORS.find((e) => e.id === "idea"); assert.isDefined(idea); if (!isAppInstalled(idea, "darwin")) return; @@ -240,7 +240,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { ); assert.deepEqual(launch, { command: "open", - args: ["-a", "IntelliJ IDEA", "/tmp/workspace"], + args: ["-a", "IntelliJ IDEA", "--args", "/tmp/workspace"], }); }), ); @@ -408,7 +408,7 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => { ); it("includes editors detected via macOS .app bundle", () => { - const idea = EDITORS.find(e => e.id === "idea") + const idea = EDITORS.find((e) => e.id === "idea"); assert.isDefined(idea); if (!isAppInstalled(idea, "darwin")) return; diff --git a/apps/server/src/open.ts b/apps/server/src/open.ts index 6ee37b1966..eacc276ee3 100644 --- a/apps/server/src/open.ts +++ b/apps/server/src/open.ts @@ -10,7 +10,7 @@ import { spawn } from "node:child_process"; import { accessSync, constants, statSync } from "node:fs"; import { extname, join } from "node:path"; -import {EDITORS, OpenError, type EditorId, EditorDefinition} from "@t3tools/contracts"; +import { EDITORS, OpenError, type EditorId, EditorDefinition } from "@t3tools/contracts"; import { ServiceMap, Effect, Layer } from "effect"; // ============================== @@ -53,10 +53,7 @@ function parseTargetPathAndPosition(target: string): { }; } -function resolveCommandEditorArgs( - editor: (typeof EDITORS)[number], - target: string, -): ReadonlyArray { +function resolveCommandEditorArgs(editor: EditorDefinition, target: string): ReadonlyArray { const parsedTarget = parseTargetPathAndPosition(target); switch (editor.launchStyle) { @@ -213,10 +210,10 @@ function resolveAppPaths(appName: string, platform: NodeJS.Platform): ReadonlyAr } export function isAppInstalled( - editor: EditorDefinition, - platform: NodeJS.Platform -): editor is EditorDefinition & {appName: string} { - if (!("appName" in editor)) return false + editor: EditorDefinition, + platform: NodeJS.Platform, +): editor is EditorDefinition & { appName: string } { + if (!("appName" in editor)) return false; for (const appPath of resolveAppPaths(editor.appName, platform)) { try { statSync(appPath); @@ -297,24 +294,22 @@ export const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( if (editorDef.commands) { const command = resolveAvailableCommand(editorDef.commands, { platform, env }); + const args = resolveCommandEditorArgs(editorDef, input.cwd); if (command) { - return { - command, - args: resolveCommandEditorArgs(editorDef, input.cwd), - }; + return { command, args }; } if (isAppInstalled(editorDef, platform)) { - return { - command: "open", - args: ["-a", editorDef.appName, input.cwd], - }; + switch (platform) { + case "darwin": + return { + command: "open", + args: ["-a", editorDef.appName, "--args", ...args], + }; + } } - return { - command: editorDef.commands[0], - args: resolveCommandEditorArgs(editorDef, input.cwd), - }; + return { command: editorDef.commands[0], args }; } if (editorDef.id !== "file-manager") {