feat(app): install the openidea command on macOS, Windows and Linux (#2009)

Adds Install/Uninstall 'openidea' Command actions. macOS uses the bundle launcher, Windows a user PATH wrapper and named pipe, and Linux the executable/AppImage path with a PATH hint. Files, folders, positions and `diff` go to the running app or start it.

Closes #1286

Co-authored-by: jaw52 <[email protected]>
Цей коміт міститься в:
l0ng-aiтаjaw52 authored and GitHub committed 2026-11-08 19:25:50 +08:00
1 джерело 25424465c0
коміт 0bfd4fa3cc
28 files changed
+1601 -42

No files matched your search

+56
Переглянути файл
@@ -0,0 +1,56 @@
import { beforeEach, expect, it, vi } from "vitest";
import { getContext } from "@/platform/context";
import { provideNotificationService } from "@/workbench/contributions";
const { install, uninstall, notify } = vi.hoisted(() => ({ install: vi.fn(), uninstall: vi.fn(), notify: vi.fn() }));
vi.mock("@/platform/ipc", () => ({
call: (result: Promise<unknown>) => result,
commands: { appCliInstall: install, appCliUninstall: uninstall },
isCancelled: (error: unknown) => (error as { kind?: string })?.kind === "cancelled",
}));
const { cliActions } = await import("../cliCommand");
const run = (id: string) =>
cliActions()
.find((action) => action.id === id)!
.run({ ctx: getContext() });
beforeEach(() => {
vi.resetAllMocks();
provideNotificationService({ notify, expire: () => {} });
});
it("explains the Linux PATH step when the user bin is missing from PATH", async () => {
install.mockResolvedValue({ path: "/home/user/.local/bin/openidea", pathDirectory: "/home/user/.local/bin" });
await run("app.installCli");
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({
level: "info",
body: expect.stringContaining("Add /home/user/.local/bin to the beginning of PATH"),
}),
);
});
it("tells users to open a new terminal after installation", async () => {
install.mockResolvedValue({ path: "C:/Users/user/OpenIDEA/bin/openidea.cmd", pathDirectory: null });
await run("app.installCli");
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({ body: expect.stringContaining("Open a new terminal") }),
);
});
it("reports an absent command on repeated uninstall", async () => {
uninstall.mockResolvedValue([]);
await run("app.uninstallCli");
expect(notify).toHaveBeenCalledWith(expect.objectContaining({ title: "The openidea command is not installed" }));
});
it("reports an install failure and stays quiet when an administrator prompt is cancelled", async () => {
install.mockRejectedValueOnce(new Error("foreign command"));
await run("app.installCli");
expect(notify).toHaveBeenCalledWith(expect.objectContaining({ level: "error", body: "foreign command" }));
notify.mockClear();
install.mockRejectedValueOnce({ kind: "cancelled" });
await run("app.installCli");
expect(notify).not.toHaveBeenCalled();
});
+73
Переглянути файл
@@ -0,0 +1,73 @@
/** Install / Uninstall the `openidea` shell command. */
import type { Action } from "@/platform/actions";
import { tx } from "@/platform/i18n";
import { call, commands, isCancelled } from "@/platform/ipc";
import { notifications } from "@/workbench/contributions";
const KEY = "app.cli";
function errorText(e: unknown) {
return e instanceof Error ? e.message : String(e);
}
async function installCli(): Promise<void> {
try {
const result = await call(commands.appCliInstall());
const hint = result.pathDirectory
? tx`Add ${result.pathDirectory} to the beginning of PATH, then open a new terminal.`
: tx("Open a new terminal to use the openidea command.");
notifications().notify({
level: "info",
title: tx("The openidea command is installed"),
body: `${result.path}\n${hint}`,
key: KEY,
});
} catch (e) {
if (!isCancelled(e))
notifications().notify({
level: "error",
title: tx("Cannot install the openidea command"),
body: errorText(e),
key: KEY,
});
}
}
async function uninstallCli(): Promise<void> {
try {
const links = await call(commands.appCliUninstall());
notifications().notify({
level: "info",
title: tx(links.length ? "The openidea command is uninstalled" : "The openidea command is not installed"),
body: links.join("\n") || undefined,
key: KEY,
});
} catch (e) {
if (!isCancelled(e))
notifications().notify({
level: "error",
title: tx("Cannot uninstall the openidea command"),
body: errorText(e),
key: KEY,
});
}
}
export function cliActions(): Action[] {
return [
{
id: "app.installCli",
title: tx("Install 'openidea' Command"),
category: tx("Tools"),
keywords: ["cli", "shell", "terminal", "path", "command line", "launcher"],
run: installCli,
},
{
id: "app.uninstallCli",
title: tx("Uninstall 'openidea' Command"),
category: tx("Tools"),
keywords: ["cli", "shell", "terminal", "path", "command line", "launcher"],
run: uninstallCli,
},
];
}
+2
Переглянути файл
@@ -6,6 +6,7 @@ import { registerOverlay, registerStatusItem } from "@/workbench/contributions";
import { AboutOverlay } from "./AboutDialog";
import { appActions, updateActions } from "./appActions";
import { checkForUpdatesOnStartup } from "./updates";
import { cliActions } from "./cliCommand";
import { installCloseGuard } from "./closeGuard";
import { TerminationOverlay } from "./termination";
import { InvalidateCachesOverlay, registerInvalidateCachesActions } from "./InvalidateCaches";
@@ -31,6 +32,7 @@ export function activate(): () => void {
const disposers = [
installLocaleSync(),
registerLocalizedActions(() => [...appActions(), ...windowActions()]),
registerLocalizedActions(cliActions),
registerOverlay({ id: "app.about", component: AboutOverlay }),
registerOverlay({ id: "app.keymapReference", component: KeymapReferenceOverlay }),
registerOverlay({ id: "app.invalidateCaches", component: InvalidateCachesOverlay }),