import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { assert, describe, it } from "@effect/vitest"; import { isCommandAvailable, launchDetached, resolveAvailableEditors, resolveEditorLaunch, } from "./open"; import { Effect } from "effect"; import { assertSuccess } from "@effect/vitest/utils"; describe("resolveEditorLaunch", () => { it.effect("returns commands for command-based editors", () => Effect.gen(function* () { const cursorLaunch = yield* resolveEditorLaunch( { cwd: "/tmp/workspace", editor: "cursor" }, "darwin", ); assert.deepEqual(cursorLaunch, { command: "cursor", args: ["/tmp/workspace"], }); const vscodeLaunch = yield* resolveEditorLaunch( { cwd: "/tmp/workspace", editor: "vscode" }, "darwin", ); assert.deepEqual(vscodeLaunch, { command: "code", args: ["/tmp/workspace"], }); const zedLaunch = yield* resolveEditorLaunch( { cwd: "/tmp/workspace", editor: "zed" }, "darwin", ); assert.deepEqual(zedLaunch, { command: "zed", args: ["/tmp/workspace"], }); }), ); it.effect("uses --goto when editor supports line/column suffixes", () => Effect.gen(function* () { const lineOnly = yield* resolveEditorLaunch( { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" }, "darwin", ); assert.deepEqual(lineOnly, { command: "cursor", args: ["--goto", "/tmp/workspace/AGENTS.md:48"], }); const lineAndColumn = yield* resolveEditorLaunch( { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "cursor" }, "darwin", ); assert.deepEqual(lineAndColumn, { command: "cursor", args: ["--goto", "/tmp/workspace/src/open.ts:71:5"], }); const vscodeLineAndColumn = yield* resolveEditorLaunch( { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscode" }, "darwin", ); assert.deepEqual(vscodeLineAndColumn, { command: "code", args: ["--goto", "/tmp/workspace/src/open.ts:71:5"], }); const zedLineAndColumn = yield* resolveEditorLaunch( { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" }, "darwin", ); assert.deepEqual(zedLineAndColumn, { command: "zed", args: ["/tmp/workspace/src/open.ts:71:5"], }); }), ); it.effect("maps file-manager editor to OS open commands", () => Effect.gen(function* () { const launch1 = yield* resolveEditorLaunch( { cwd: "/tmp/workspace", editor: "file-manager" }, "darwin", ); assert.deepEqual(launch1, { command: "open", args: ["/tmp/workspace"], }); const launch2 = yield* resolveEditorLaunch( { cwd: "C:\\workspace", editor: "file-manager" }, "win32", ); assert.deepEqual(launch2, { command: "explorer", args: ["C:\\workspace"], }); const launch3 = yield* resolveEditorLaunch( { cwd: "/tmp/workspace", editor: "file-manager" }, "linux", ); assert.deepEqual(launch3, { command: "xdg-open", args: ["/tmp/workspace"], }); }), ); }); describe("launchDetached", () => { it.effect("resolves when command can be spawned", () => Effect.gen(function* () { const result = yield* launchDetached({ command: process.execPath, args: ["-e", "process.exit(0)"], }).pipe(Effect.result); assertSuccess(result, undefined); }), ); it.effect("rejects when command does not exist", () => Effect.gen(function* () { const result = yield* launchDetached({ command: `t3code-no-such-command-${Date.now()}`, args: [], }).pipe(Effect.result); assert.equal(result._tag, "Failure"); }), ); }); describe("isCommandAvailable", () => { function withTempDir(run: (dir: string) => void): void { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-open-")); try { run(dir); } finally { fs.rmSync(dir, { recursive: true, force: true }); } } it("resolves win32 commands with PATHEXT", () => { withTempDir((dir) => { fs.writeFileSync(path.join(dir, "code.CMD"), "@echo off\r\n", "utf8"); const env = { PATH: dir, PATHEXT: ".COM;.EXE;.BAT;.CMD", } satisfies NodeJS.ProcessEnv; assert.equal(isCommandAvailable("code", { platform: "win32", env }), true); }); }); it("returns false when a command is not on PATH", () => { const env = { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD", } satisfies NodeJS.ProcessEnv; assert.equal(isCommandAvailable("definitely-not-installed", { platform: "win32", env }), false); }); it("does not treat bare files without executable extension as available on win32", () => { withTempDir((dir) => { fs.writeFileSync(path.join(dir, "npm"), "echo nope\r\n", "utf8"); const env = { PATH: dir, PATHEXT: ".COM;.EXE;.BAT;.CMD", } satisfies NodeJS.ProcessEnv; assert.equal(isCommandAvailable("npm", { platform: "win32", env }), false); }); }); it("appends PATHEXT for commands with non-executable extensions on win32", () => { withTempDir((dir) => { fs.writeFileSync(path.join(dir, "my.tool.CMD"), "@echo off\r\n", "utf8"); const env = { PATH: dir, PATHEXT: ".COM;.EXE;.BAT;.CMD", } satisfies NodeJS.ProcessEnv; assert.equal(isCommandAvailable("my.tool", { platform: "win32", env }), true); }); }); it("uses platform-specific PATH delimiter for platform overrides", () => { withTempDir((firstDir) => { withTempDir((secondDir) => { fs.writeFileSync(path.join(secondDir, "code.CMD"), "@echo off\r\n", "utf8"); const env = { PATH: `${firstDir};${secondDir}`, PATHEXT: ".COM;.EXE;.BAT;.CMD", } satisfies NodeJS.ProcessEnv; assert.equal(isCommandAvailable("code", { platform: "win32", env }), true); }); }); }); }); describe("resolveAvailableEditors", () => { it("returns only editors whose launch commands are available", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-editors-")); try { fs.writeFileSync(path.join(dir, "cursor.CMD"), "@echo off\r\n", "utf8"); fs.writeFileSync(path.join(dir, "explorer.EXE"), "MZ", "utf8"); const editors = resolveAvailableEditors("win32", { PATH: dir, PATHEXT: ".COM;.EXE;.BAT;.CMD", }); assert.deepEqual(editors, ["cursor", "file-manager"]); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); });