|
1 | 1 | /** |
2 | 2 | * Agent skill installation for AI coding assistants. |
3 | 3 | * |
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. |
6 | 7 | * |
7 | 8 | * Skill file contents are embedded at build time (via a generated module |
8 | 9 | * produced by script/generate-skill.ts), so no network fetch is needed. |
@@ -34,49 +35,30 @@ export function detectClaudeCode(homeDir: string): boolean { |
34 | 35 | } |
35 | 36 |
|
36 | 37 | /** |
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. |
38 | 40 | * |
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. |
41 | 43 | */ |
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"); |
44 | 49 | } |
45 | 50 |
|
46 | 51 | /** |
47 | | - * Install the Sentry CLI agent skill for Claude Code. |
| 52 | + * Write embedded skill files beneath an already-detected agent root. |
48 | 53 | * |
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. |
58 | 56 | */ |
59 | | -export async function installAgentSkills( |
60 | | - homeDir: string |
| 57 | +async function writeSkillFiles( |
| 58 | + skillPath: string |
61 | 59 | ): 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 | | - |
76 | 60 | try { |
77 | | - const skillPath = getSkillInstallPath(homeDir); |
78 | 61 | const skillDir = dirname(skillPath); |
79 | | - |
80 | 62 | const alreadyExists = existsSync(skillPath); |
81 | 63 | let referenceCount = 0; |
82 | 64 |
|
@@ -105,3 +87,62 @@ export async function installAgentSkills( |
105 | 87 | return null; |
106 | 88 | } |
107 | 89 | } |
| 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 | +} |
0 commit comments