diff --git a/src/services/skills-installer.ts b/src/services/skills-installer.ts index d45348bdd..0deb8364b 100644 --- a/src/services/skills-installer.ts +++ b/src/services/skills-installer.ts @@ -48,6 +48,12 @@ export const TARGET_CONFIGS: Record = { // Codeium's per-user config tree (memories, global_workflows). relativeDir: path.join(".codeium", "windsurf", "skills"), }, + zed: { + name: "Zed", + // Per zed.dev/docs/ai/skills, Zed's agent loads user-level Agent Skills + // from ~/.agents/skills/ (project-level lives at /.agents/skills/). + relativeDir: path.join(".agents", "skills"), + }, }; /** diff --git a/src/services/tool-detector.ts b/src/services/tool-detector.ts index b67238dda..aeb7b626a 100644 --- a/src/services/tool-detector.ts +++ b/src/services/tool-detector.ts @@ -89,6 +89,22 @@ const TOOL_CHECKS: ToolCheck[] = [ winPaths: [path.join(localAppData, "Programs", "Windsurf", "Windsurf.exe")], configDirs: [path.join(home, ".windsurf")], }, + { + id: "zed", + name: "Zed", + installMethod: InstallMethod.FileCopy, + cliNames: ["zed"], + macApps: ["/Applications/Zed.app"], + // Zed's Linux install script unpacks to ~/.local/zed.app and links a `zed` + // launcher into ~/.local/bin; package builds land in /usr/{bin,lib}. + linuxPaths: [ + path.join(home, ".local", "zed.app"), + "/usr/bin/zed", + "/usr/lib/zed", + ], + winPaths: [path.join(localAppData, "Programs", "Zed", "Zed.exe")], + configDirs: [path.join(home, ".config", "zed")], + }, ]; function checkCli(name: string): Promise { diff --git a/test/unit/commands/init.test.ts b/test/unit/commands/init.test.ts index d1fd53b7e..b398e8464 100644 --- a/test/unit/commands/init.test.ts +++ b/test/unit/commands/init.test.ts @@ -53,6 +53,13 @@ const ALL_UNDETECTED: DetectedTool[] = [ evidence: "", installMethod: InstallMethod.FileCopy, }, + { + id: "zed", + name: "Zed", + detected: false, + evidence: "", + installMethod: InstallMethod.FileCopy, + }, ]; async function buildSkillsTarball(...names: string[]): Promise { diff --git a/test/unit/commands/skills/install.test.ts b/test/unit/commands/skills/install.test.ts index a7eae75ee..8b28f9468 100644 --- a/test/unit/commands/skills/install.test.ts +++ b/test/unit/commands/skills/install.test.ts @@ -65,6 +65,13 @@ const ALL_UNDETECTED: DetectedTool[] = [ evidence: "", installMethod: InstallMethod.FileCopy, }, + { + id: "zed", + name: "Zed", + detected: false, + evidence: "", + installMethod: InstallMethod.FileCopy, + }, ]; async function buildSkillsTarball(...names: string[]): Promise { @@ -237,6 +244,7 @@ describe("skills:install command", () => { expect(stdout).toContain("auto"); expect(stdout).toContain("vscode"); expect(stdout).toContain("windsurf"); + expect(stdout).toContain("zed"); }); }); diff --git a/test/unit/services/skills-installer.test.ts b/test/unit/services/skills-installer.test.ts index 7467f0b95..440ae799f 100644 --- a/test/unit/services/skills-installer.test.ts +++ b/test/unit/services/skills-installer.test.ts @@ -120,12 +120,12 @@ describe("SkillsInstaller", () => { const { results } = installer.install({ skills, - targets: ["claude-code", "cursor", "vscode"], + targets: ["claude-code", "cursor", "vscode", "zed"], }); - expect(results).toHaveLength(3); + expect(results).toHaveLength(4); - for (const target of [".claude", ".cursor", ".copilot"]) { + for (const target of [".claude", ".cursor", ".copilot", ".agents"]) { expect( fs.existsSync( path.join( diff --git a/test/unit/services/tool-detector.test.ts b/test/unit/services/tool-detector.test.ts index 87850722d..836db3c9d 100644 --- a/test/unit/services/tool-detector.test.ts +++ b/test/unit/services/tool-detector.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { execFile } from "node:child_process"; import fs from "node:fs"; +import path from "node:path"; vi.mock("node:child_process", () => ({ execFile: vi.fn(), @@ -106,6 +107,30 @@ describe("tool-detector", () => { expect(cursor!.installMethod).toBe("file-copy"); }); + it("should detect Zed via its config directory", async () => { + mockedExecFile.mockImplementation( + (_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => { + (cb as (err: Error | null, stdout: string) => void)( + new Error("not found"), + "", + ); + return undefined as never; + }, + ); + + mockedExistsSync.mockImplementation((p: fs.PathLike) => + String(p).includes(path.join(".config", "zed")), + ); + + const results = await detectTools(); + const zed = results.find((t) => t.id === "zed"); + + expect(zed).toBeDefined(); + expect(zed!.detected).toBe(true); + expect(zed!.evidence).toMatch(/^config:/); + expect(zed!.installMethod).toBe("file-copy"); + }); + it("should detect multiple tools simultaneously", async () => { mockedExecFile.mockImplementation( (_cmd: unknown, args: unknown, _opts: unknown, cb: unknown) => {