Skip to content

Commit 5c4bd00

Browse files
authored
feat(setup): install agent skills for detected roots (#747)
## Summary This updates the existing review branch with the smaller change built from `main` instead of the earlier unconditional `.agents` installer. It keeps the Claude-style flow, adds shared `~/.agents` support only when that root already exists, and keeps setup output aligned with the file that was actually created. ## Changes - detect existing `~/.agents` and `~/.claude` roots instead of creating a top-level shared root - reuse the existing skill path helper for both targets and keep each install independent - update setup and unit tests for no-agent silence, detected `~/.agents` installs, and fresh-path reporting ## Test Plan - [x] `bun test test/lib/agent-skills.test.ts` - [x] `bun test test/commands/cli/setup.test.ts` - [x] `bun run typecheck` - [x] `bun run lint` (passes with an existing unrelated warning in `src/lib/formatters/markdown.ts`)
1 parent c70677a commit 5c4bd00

4 files changed

Lines changed: 226 additions & 71 deletions

File tree

src/commands/cli/setup.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,9 @@ async function handleCompletions(
280280
/**
281281
* Handle agent skill installation for AI coding assistants.
282282
*
283-
* Detects supported agents (currently Claude Code) and installs the
284-
* version-pinned skill file. Silent when no agent is detected.
283+
* Detects supported agent roots (currently `~/.agents` and `~/.claude`)
284+
* and installs the version-pinned skill files. Silent when no compatible
285+
* agent root is detected.
285286
*
286287
* Only produces output when the skill file is freshly created. Subsequent
287288
* runs (e.g. after upgrade) silently update without printing.
@@ -440,7 +441,7 @@ export const setupCommand = buildCommand({
440441
"Sets up shell integration for the Sentry CLI:\n\n" +
441442
"- Adds binary directory to PATH (if not already in PATH)\n" +
442443
"- Installs shell completions (bash, zsh, fish)\n" +
443-
"- Installs agent skills for AI coding assistants (e.g., Claude Code)\n" +
444+
"- Installs agent skills for detected AI coding assistants\n" +
444445
"- Records installation metadata for upgrades\n\n" +
445446
"With --install, also handles binary placement from a temporary\n" +
446447
"download location (used by the install script and upgrade command).\n\n" +

src/lib/agent-skills.ts

Lines changed: 76 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
22
* Agent skill installation for AI coding assistants.
33
*
4-
* Detects supported AI coding agents (currently Claude Code) and installs
5-
* the Sentry CLI skill files so the agent can use CLI commands effectively.
4+
* Detects supported AI coding agents (currently Claude Code and agents that
5+
* use the shared `~/.agents` root) and installs the Sentry CLI skill files
6+
* so the agent can use CLI commands effectively.
67
*
78
* Skill file contents are embedded at build time (via a generated module
89
* produced by script/generate-skill.ts), so no network fetch is needed.
@@ -34,49 +35,30 @@ export function detectClaudeCode(homeDir: string): boolean {
3435
}
3536

3637
/**
37-
* Get the installation path for the Sentry CLI skill in Claude Code.
38+
* Get the installation path for the Sentry CLI skill under a supported AI
39+
* coding assistant root.
3840
*
39-
* Skills are stored under ~/.claude/skills/<skill-name>/SKILL.md,
40-
* matching the convention used by the `npx skills` tool.
41+
* `~/.claude` remains the default to preserve the existing helper behavior,
42+
* while callers can also pass `".agents"` for the shared Agent Skills path.
4143
*/
42-
export function getSkillInstallPath(homeDir: string): string {
43-
return join(homeDir, ".claude", "skills", "sentry-cli", "SKILL.md");
44+
export function getSkillInstallPath(
45+
homeDir: string,
46+
rootDir: ".agents" | ".claude" = ".claude"
47+
): string {
48+
return join(homeDir, rootDir, "skills", "sentry-cli", "SKILL.md");
4449
}
4550

4651
/**
47-
* Install the Sentry CLI agent skill for Claude Code.
52+
* Write embedded skill files beneath an already-detected agent root.
4853
*
49-
* Checks if Claude Code is installed and writes the embedded skill files
50-
* to the Claude Code skills directory. Skill content is bundled into the
51-
* binary at build time, so no network access is required.
52-
*
53-
* Returns null (without throwing) if Claude Code isn't detected
54-
* or any other error occurs.
55-
*
56-
* @param homeDir - User's home directory
57-
* @returns Location info if installed, null otherwise
54+
* Callers must ensure the target root exists and is writable before invoking
55+
* this helper. Returns null on any filesystem failure.
5856
*/
59-
export async function installAgentSkills(
60-
homeDir: string
57+
async function writeSkillFiles(
58+
skillPath: string
6159
): Promise<AgentSkillLocation | null> {
62-
if (!detectClaudeCode(homeDir)) {
63-
return null;
64-
}
65-
66-
// Verify .claude is writable before attempting file creation.
67-
// In sandboxed environments (e.g., Claude Code sandbox), .claude may exist
68-
// but be read-only. Some sandboxes terminate the process on write attempts,
69-
// bypassing JavaScript error handling — so we must check before writing.
70-
try {
71-
accessSync(join(homeDir, ".claude"), constants.W_OK);
72-
} catch {
73-
return null;
74-
}
75-
7660
try {
77-
const skillPath = getSkillInstallPath(homeDir);
7861
const skillDir = dirname(skillPath);
79-
8062
const alreadyExists = existsSync(skillPath);
8163
let referenceCount = 0;
8264

@@ -105,3 +87,62 @@ export async function installAgentSkills(
10587
return null;
10688
}
10789
}
90+
91+
/**
92+
* Install the Sentry CLI agent skill for detected AI coding assistants.
93+
*
94+
* Checks supported roots and writes the embedded skill files to each detected
95+
* location. The installer never creates top-level agent roots. Their presence
96+
* is the detection signal that the user already has a compatible agent installed.
97+
*
98+
* If any target is freshly created, the returned `path` points to that new
99+
* installation so setup output matches the file that was actually added.
100+
*
101+
* @param homeDir - User's home directory
102+
* @returns Location info if installed to at least one detected target, null otherwise
103+
*/
104+
export async function installAgentSkills(
105+
homeDir: string
106+
): Promise<AgentSkillLocation | null> {
107+
const installTargets = [
108+
{
109+
detected: existsSync(join(homeDir, ".agents")),
110+
rootDir: ".agents" as const,
111+
},
112+
{
113+
detected: detectClaudeCode(homeDir),
114+
rootDir: ".claude" as const,
115+
},
116+
];
117+
const results: AgentSkillLocation[] = [];
118+
119+
for (const target of installTargets) {
120+
if (!target.detected) {
121+
continue;
122+
}
123+
124+
const parentDir = join(homeDir, target.rootDir);
125+
126+
// In sandboxed environments, the agent root may exist but be read-only.
127+
// Some sandboxes terminate the process on write attempts, bypassing
128+
// JavaScript error handling — so we must check before writing.
129+
try {
130+
accessSync(parentDir, constants.W_OK);
131+
} catch {
132+
continue;
133+
}
134+
135+
const location = await writeSkillFiles(
136+
getSkillInstallPath(homeDir, target.rootDir)
137+
);
138+
if (location) {
139+
results.push(location);
140+
}
141+
}
142+
143+
if (results.length === 0) {
144+
return null;
145+
}
146+
147+
return results.find((location) => location.created) ?? results[0] ?? null;
148+
}

test/commands/cli/setup.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ describe("sentry cli setup", () => {
730730
expect(existsSync(skillPath)).toBe(true);
731731
});
732732

733-
test("silently skips when Claude Code is not detected", async () => {
733+
test("silently skips when no supported agent root is detected", async () => {
734734
const { context, getOutput, restore } = createMockContext({
735735
homeDir: testDir,
736736
execPath: join(testDir, "bin", "sentry"),
@@ -750,6 +750,34 @@ describe("sentry cli setup", () => {
750750
expect(getOutput()).not.toContain("Agent skills:");
751751
});
752752

753+
test("installs to ~/.agents/ when the shared agent root is detected", async () => {
754+
mkdirSync(join(testDir, ".agents"), { recursive: true });
755+
756+
const { context, getOutput, restore } = createMockContext({
757+
homeDir: testDir,
758+
execPath: join(testDir, "bin", "sentry"),
759+
env: {
760+
PATH: `/usr/bin:${join(testDir, "bin")}:/bin`,
761+
SHELL: "/bin/bash",
762+
},
763+
});
764+
restoreStderr = restore;
765+
766+
await run(
767+
app,
768+
["cli", "setup", "--no-modify-path", "--no-completions"],
769+
context
770+
);
771+
772+
expect(getOutput()).toContain("Agent skills: Installed to");
773+
expect(
774+
existsSync(join(testDir, ".agents", "skills", "sentry-cli", "SKILL.md"))
775+
).toBe(true);
776+
expect(
777+
existsSync(join(testDir, ".claude", "skills", "sentry-cli", "SKILL.md"))
778+
).toBe(false);
779+
});
780+
753781
test("suppresses agent skills message on subsequent runs (upgrade scenario)", async () => {
754782
mkdirSync(join(testDir, ".claude"), { recursive: true });
755783

0 commit comments

Comments
 (0)