feat(agent-skills): Add --skills command to install Sentry agent skills#1187
feat(agent-skills): Add --skills command to install Sentry agent skills#1187
Conversation
This adds a new wizard feature that installs Sentry agent skills for various AI coding assistants: - Claude Code (.claude/skills/) - OpenAI Codex (.codex/skills/) - OpenCode (.opencode/skill/) - Cursor (.cursor/skills/) - GitHub Copilot (.github/skills/) - Factory Droid (.factory/skills/) Features: - Interactive mode with scope and editor selection prompts - Headless mode: --skills claude-code opencode - Project-scoped (default) or user-scoped installation: --scope user - Smart update logic: updates Sentry skills, preserves user-created skills - Downloads skills from getsentry/sentry-agent-skills GitHub releases Usage: npx @sentry/wizard --skills # Interactive npx @sentry/wizard --skills claude-code cursor # Headless npx @sentry/wizard --skills claude-code --scope user # User profile
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog. Features
🤖 This preview updates automatically when you update the PR. |
Lms24
left a comment
There was a problem hiding this comment.
Nice! I think the new flow is a good starting point. We could even potentially call it from SDK setup flows (if we want to) but this makes sense standalone as well (as discussed on Slack)
A general question: IIUC for now, this downloads the latest skill version at the time. Is there some way users can easily update skills without having to periodically rerun the wizard? This is more a general "skill ecosystem" question. My only concern is these skills becoming outdated and users not being aware that there are improvements available.
| chalk.red( | ||
| 'Failed to download Sentry agent skills from GitHub.\n' + | ||
| 'Please check your internet connection and try again.\n\n' + | ||
| 'You can also install skills manually from:\n' + | ||
| chalk.cyan('https://github.com/getsentry/sentry-agent-skills'), | ||
| ), |
There was a problem hiding this comment.
nit: We usually don't color entire error messages. clack.log.error already marks the output visually as errored. Feel free to disregard though, no strong feelings :) IF you change it, let's also use template strings over concatinating line-by-line. Makes it a bit more readable IMHO.
| const tempDir = path.join(os.tmpdir(), `sentry-skills-${Date.now()}`); | ||
| await fs.promises.mkdir(tempDir, { recursive: true }); |
There was a problem hiding this comment.
l: let's use
| const tempDir = path.join(os.tmpdir(), `sentry-skills-${Date.now()}`); | |
| await fs.promises.mkdir(tempDir, { recursive: true }); | |
| const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sentry-skills-')); |
instead for a truly random temp dir (fs.mkdtemp will automatically append a random sequence to the last - specified in the name).
| } catch { | ||
| // Network error on HEAD, try archive | ||
| tarballUrl = SKILLS_REPO_ARCHIVE_URL; | ||
| isArchive = true; | ||
| } |
There was a problem hiding this comment.
q: if we land here, something went wrong, correct? Should we emit a log/warning?
| const skillsExist = await fs.promises | ||
| .access(extractPath) | ||
| .then(() => true) | ||
| .catch(() => false); |
There was a problem hiding this comment.
l: can we use fs.existsSync(extractPath) here instead?
| const skillsExist = await fs.promises | |
| .access(extractPath) | |
| .then(() => true) | |
| .catch(() => false); | |
| const skillsExist = fs.existsSync(extractPath); |
No strong feelings but I think fs.access is nice for high throughput envs but in case of the wizard the sync operation is totally fine
| const targetExists = await fs.promises | ||
| .access(targetPath) | ||
| .then(() => true) | ||
| .catch(() => false); |
|
One more thing: Let's make sure to add an e2e test for this flow, to have it covered. Thanks! |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| await fs.promises.rm(tempDir, { recursive: true, force: true }); | ||
| } catch { | ||
| // Ignore cleanup errors | ||
| } |
There was a problem hiding this comment.
Temp directory not fully cleaned up after archive download
Medium Severity
The downloadSkills() function returns extractPath, which is often a subdirectory of the actual temp directory (e.g., <tempDir>/sentry-agent-skills-main/skills when isArchive is true). The cleanup code at line 177 removes only this subdirectory, leaving the parent temp directory orphaned. Each wizard run accumulates leftover temp directories that are never cleaned up.
Additional Locations (1)
| async () => { | ||
| if (options.scope) { | ||
| return options.scope; | ||
| } |
There was a problem hiding this comment.
Scope selection prompt unreachable in interactive mode
Medium Severity
The interactive scope selection prompt is dead code that can never be reached. The yargs definition sets default: 'project' for the scope option, so finalArgs.scope is always 'project' even when the user doesn't specify it. The wizard then checks if (options.scope) which is always truthy, causing an immediate return before the prompt. Users in interactive mode cannot select between project and user scope.


Summary
This PR adds a new wizard feature that installs Sentry agent skills for various AI coding assistants.
Supported Editors
.claude/skills/).codex/skills/).opencode/skill/).cursor/skills/).github/skills/).factory/skills/)Features
--skills claude-code opencode--scope userUsage
Changes
bin.ts- Added--skillsand--scopeCLI argumentssrc/agent-skills/agent-skills-wizard.ts- Main wizard implementationsrc/agent-skills/editor-configs.ts- Editor configuration and path helperssrc/run.ts- Route to agent skills wizard when--skillsflag is presentTesting