Repository navigation
fix(desktop): skip uninstalled agents when provisioning hooks #9233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fix/desktop-first-launch-quiet
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,7 @@ import { | |
| } from "./agent-wrappers"; | ||
| import { createArtifactGuardScript } from "./artifact-guard-hook"; | ||
| import { resolveDisabledSkillIds } from "./disabled-skills"; | ||
| import { isAgentInstalled } from "./installed-agents"; | ||
| import { createManagedSkills } from "./managed-skills"; | ||
| import { createNotifyScript } from "./notify-hook"; | ||
|
|
||
|
|
@@ -213,11 +214,19 @@ interface SetupAgentCapabilitiesOptions { | |
| disabledAgentIds?: readonly string[]; | ||
| /** Skills the user disabled; withheld from provisioning and reaped. */ | ||
| disabledSkillIds?: readonly string[]; | ||
| /** | ||
| * Leave agents that are not installed untouched, so no config dir is | ||
| * created for a tool the user never installed. | ||
| */ | ||
| skipUninstalledAgents?: boolean; | ||
| isInstalled?: (agentId: AgentSetupTargetId) => boolean; | ||
| } | ||
|
|
||
| export function setupAgentCapabilities({ | ||
| disabledAgentIds = [], | ||
| disabledSkillIds = [], | ||
| skipUninstalledAgents = false, | ||
| isInstalled = isAgentInstalled, | ||
| }: SetupAgentCapabilitiesOptions = {}): void { | ||
| const disabled = new Set(disabledAgentIds); | ||
| const failed: string[] = []; | ||
|
|
@@ -239,9 +248,13 @@ export function setupAgentCapabilities({ | |
|
|
||
| for (const target of AGENT_SETUP_TARGETS) { | ||
| const definition = AGENT_SETUP_DEFINITIONS[target.id]; | ||
| const isDisabled = disabled.has(target.id); | ||
| if (skipUninstalledAgents && !isDisabled && !isInstalled(target.id)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Apply the installation filter to Claude plugin provisioning too; Prompt for AI agents |
||
| continue; | ||
|
Comment on lines
+252
to
+253
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Pass the installation filter into Claude plugin provisioning while keeping the shared Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/agent-setup/src/agent-setup.ts
Line: 252-253
Comment:
**Uninstalled Claude still gets configured**
`skipUninstalledAgents` still creates `~/.claude` on a machine without Claude. Before this check, `createManagedSkills()` copies the bundled plugin into `~/.claude/skills/superset`. The check then treats that newly created directory as proof that Claude is installed and writes its settings and wrapper too.
Pass the installation filter into Claude plugin provisioning while keeping the shared `~/.agents` skills unchanged.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| } | ||
| runAgentActions( | ||
| target.id, | ||
| disabled.has(target.id) ? definition.teardown : definition.setup, | ||
| isDisabled ? definition.teardown : definition.setup, | ||
| failed, | ||
| ); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { afterEach, beforeEach, describe, expect, it } from "bun:test"; | ||
| import fs from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { setupAgentCapabilities } from "./agent-setup"; | ||
| import { defaultBinarySearchDirs, isAgentInstalled } from "./installed-agents"; | ||
|
|
||
| const ORIGINAL = { | ||
| HOME: process.env.HOME, | ||
| PATH: process.env.PATH, | ||
| SUPERSET_HOME_DIR: process.env.SUPERSET_HOME_DIR, | ||
| }; | ||
| let home: string; | ||
|
|
||
| beforeEach(() => { | ||
| home = fs.mkdtempSync(path.join(os.tmpdir(), "superset-installed-")); | ||
| process.env.HOME = home; | ||
| process.env.PATH = path.join(home, "empty-bin"); | ||
| process.env.SUPERSET_HOME_DIR = path.join(home, ".superset"); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| for (const [key, value] of Object.entries(ORIGINAL)) { | ||
| if (value === undefined) delete process.env[key]; | ||
| else process.env[key] = value; | ||
| } | ||
| fs.rmSync(home, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| describe("isAgentInstalled", () => { | ||
| it("is false on a bare home", () => { | ||
| expect(isAgentInstalled("claude", [])).toBe(false); | ||
| }); | ||
|
|
||
| it("detects a config dir", () => { | ||
| fs.mkdirSync(path.join(home, ".codex")); | ||
| expect(isAgentInstalled("codex", [])).toBe(true); | ||
| }); | ||
|
|
||
| it("detects a binary in a search dir", () => { | ||
| const bin = path.join(home, "bin"); | ||
| fs.mkdirSync(bin); | ||
| fs.writeFileSync(path.join(bin, "gemini"), "#!/bin/sh\n", { mode: 0o755 }); | ||
| expect(isAgentInstalled("gemini", [bin])).toBe(true); | ||
| }); | ||
|
|
||
| it("leaves Superset's own wrapper dir out of the search", () => { | ||
| const wrappers = path.join(home, ".superset", "bin"); | ||
| process.env.PATH = wrappers; | ||
| expect(defaultBinarySearchDirs()).not.toContain(wrappers); | ||
| }); | ||
| }); | ||
|
|
||
| describe("setupAgentCapabilities with skipUninstalledAgents", () => { | ||
| it("writes only the installed agent's config", () => { | ||
| fs.mkdirSync(path.join(home, ".superset", "hooks"), { recursive: true }); | ||
| fs.mkdirSync(path.join(home, ".superset", "bin"), { recursive: true }); | ||
| setupAgentCapabilities({ | ||
| skipUninstalledAgents: true, | ||
| isInstalled: (id) => id === "claude", | ||
| }); | ||
|
|
||
| expect(fs.existsSync(path.join(home, ".claude", "settings.json"))).toBe( | ||
| true, | ||
| ); | ||
| for (const dir of [".codex", ".cursor", ".gemini", ".grok", ".factory"]) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This test does not verify the “only installed agent” claim for most configured agents. Expand the negative assertions to cover every agent-specific config footprint, otherwise provisioning regressions for Amp, OpenCode, OMP, Pi, MastraCode, Kimi, Vibe, Devin, or Muse can pass unnoticed. Prompt for AI agents |
||
| expect(fs.existsSync(path.join(home, dir))).toBe(false); | ||
| } | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||||||||||||||||
| import fs from "node:fs"; | ||||||||||||||||||||||||
| import path from "node:path"; | ||||||||||||||||||||||||
| import type { AgentSetupTargetId } from "./agent-setup-targets"; | ||||||||||||||||||||||||
| import { getBinDir, resolveUserHomeDir } from "./paths"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| interface AgentFootprint { | ||||||||||||||||||||||||
| binaries: readonly string[]; | ||||||||||||||||||||||||
| homeDirs: readonly string[]; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const AGENT_FOOTPRINTS: Record<AgentSetupTargetId, AgentFootprint> = { | ||||||||||||||||||||||||
| amp: { binaries: ["amp"], homeDirs: [".config/amp"] }, | ||||||||||||||||||||||||
| claude: { binaries: ["claude"], homeDirs: [".claude"] }, | ||||||||||||||||||||||||
| codex: { binaries: ["codex"], homeDirs: [".codex"] }, | ||||||||||||||||||||||||
| droid: { binaries: ["droid"], homeDirs: [".factory"] }, | ||||||||||||||||||||||||
| opencode: { binaries: ["opencode"], homeDirs: [".config/opencode"] }, | ||||||||||||||||||||||||
| omp: { binaries: ["omp"], homeDirs: [".omp"] }, | ||||||||||||||||||||||||
| pi: { binaries: ["pi"], homeDirs: [".pi"] }, | ||||||||||||||||||||||||
| "cursor-agent": { | ||||||||||||||||||||||||
| binaries: ["cursor-agent"], | ||||||||||||||||||||||||
| homeDirs: [".cursor"], | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| gemini: { binaries: ["gemini"], homeDirs: [".gemini"] }, | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The shared Prompt for AI agents |
||||||||||||||||||||||||
| mastracode: { binaries: ["mastracode"], homeDirs: [".mastracode"] }, | ||||||||||||||||||||||||
| kimi: { binaries: ["kimi"], homeDirs: [".kimi-code"] }, | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Resolve Kimi’s effective config home, including Prompt for AI agents |
||||||||||||||||||||||||
| grok: { binaries: ["grok"], homeDirs: [".grok"] }, | ||||||||||||||||||||||||
| copilot: { binaries: ["copilot"], homeDirs: [".copilot"] }, | ||||||||||||||||||||||||
| vibe: { binaries: ["vibe"], homeDirs: [".vibe"] }, | ||||||||||||||||||||||||
| devin: { binaries: ["devin"], homeDirs: [".config/devin"] }, | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The installation fallback ignores Prompt for AI agents |
||||||||||||||||||||||||
| muse: { binaries: ["muse"], homeDirs: [".config/muse"] }, | ||||||||||||||||||||||||
| ufo: { binaries: ["ufo"], homeDirs: [] }, | ||||||||||||||||||||||||
| agy: { binaries: ["agy"], homeDirs: [] }, | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const EXTRA_BINARY_DIRS = [ | ||||||||||||||||||||||||
| ".local/bin", | ||||||||||||||||||||||||
| ".bun/bin", | ||||||||||||||||||||||||
| ".cargo/bin", | ||||||||||||||||||||||||
| ".npm-global/bin", | ||||||||||||||||||||||||
| ".claude/local", | ||||||||||||||||||||||||
| ".volta/bin", | ||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const SYSTEM_BINARY_DIRS = ["/opt/homebrew/bin", "/usr/local/bin"]; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export function defaultBinarySearchDirs(): string[] { | ||||||||||||||||||||||||
| const home = resolveUserHomeDir(); | ||||||||||||||||||||||||
| const wrapperDir = path.resolve(getBinDir()); | ||||||||||||||||||||||||
| const fromPath = (process.env.PATH ?? "").split(path.delimiter); | ||||||||||||||||||||||||
| return [ | ||||||||||||||||||||||||
| ...fromPath, | ||||||||||||||||||||||||
| ...EXTRA_BINARY_DIRS.map((dir) => path.join(home, dir)), | ||||||||||||||||||||||||
| ...SYSTEM_BINARY_DIRS, | ||||||||||||||||||||||||
| ].filter((dir) => dir && path.resolve(dir) !== wrapperDir); | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This filter still treats other Superset workspace wrappers such as Prompt for AI agents
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The wrapper-directory filter compares Windows paths as case-sensitive strings, so a differently cased PATH entry can expose Superset's own wrappers and mark every wrapped agent installed. Compare normalized Windows paths case-insensitively before probing binaries. Prompt for AI agents |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function hasBinary(names: readonly string[], dirs: readonly string[]): boolean { | ||||||||||||||||||||||||
| const extensions = process.platform === "win32" ? ["", ".exe", ".cmd"] : [""]; | ||||||||||||||||||||||||
| return names.some((name) => | ||||||||||||||||||||||||
| dirs.some((dir) => | ||||||||||||||||||||||||
| extensions.some((extension) => { | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| fs.accessSync(path.join(dir, name + extension), fs.constants.X_OK); | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * An agent counts as installed when its binary is on PATH (or a usual install | ||||||||||||||||||||||||
| * dir — Finder-launched apps get a minimal PATH) or its home config dir | ||||||||||||||||||||||||
| * exists. Superset's own wrapper dir is ignored. | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| export function isAgentInstalled( | ||||||||||||||||||||||||
| agentId: AgentSetupTargetId, | ||||||||||||||||||||||||
| binaryDirs: readonly string[] = defaultBinarySearchDirs(), | ||||||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||||||
| const footprint = AGENT_FOOTPRINTS[agentId]; | ||||||||||||||||||||||||
| const home = resolveUserHomeDir(); | ||||||||||||||||||||||||
| if (footprint.homeDirs.some((dir) => fs.existsSync(path.join(home, dir)))) { | ||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+84
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The config-directory fallback ignores supported custom locations. If Resolve the same effective config paths here. The same mismatch affects Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/agent-setup/src/installed-agents.ts
Line: 84-86
Comment:
**Custom agent homes get missed**
The config-directory fallback ignores supported custom locations. If `KIMI_CODE_HOME` points to an existing profile and the Kimi binary is outside the desktop's search directories, this checks only `~/.kimi-code` and skips Kimi's hook setup. The existing writer correctly uses `KIMI_CODE_HOME`.
Resolve the same effective config paths here. The same mismatch affects `OMP_CODING_AGENT_DIR` and the `XDG_CONFIG_HOME` paths for Devin and Muse.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||||||||||||||||||||||||
| return hasBinary(footprint.binaries, binaryDirs); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
lastRunAtstarts at app initialization, so it consumes the one-minute cooldown before onboarding has run. If onboarding completes during that minute, the first focus afterward is skipped and a newly installed agent is not picked up until a later focus; initialize the cooldown after an actual provisioning run instead.Prompt for AI agents