Skip to content

Commit 112c078

Browse files
betegonclaude
andauthored
fix(skill): include widget subcommands in generated skill files (#519)
## Summary - `extractRouteGroupCommands` only processed direct `Command` children, skipping nested `RouteMap` entries like `dashboard > widget` - Added recursion so nested route maps (e.g. `sentry dashboard widget add/edit/delete`) are included in generated SKILL.md and reference files - Regenerated skill files with the fix applied ## Test plan - [x] `bun run typecheck` passes - [x] `bun run lint` passes - [x] `bun test test/lib/introspect.test.ts test/commands/dashboard/ test/commands/help.test.ts` — 95 tests pass - [x] `bun run generate:skill` produces `dashboards.md` with widget commands - [x] SKILL.md includes `sentry dashboard widget add/edit/delete` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f66785d commit 112c078

File tree

5 files changed

+101
-30
lines changed

5 files changed

+101
-30
lines changed

plugins/sentry-cli/skills/sentry-cli/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ Manage Sentry dashboards
226226
- `sentry dashboard list <org/project>` — List dashboards
227227
- `sentry dashboard view <args...>` — View a dashboard
228228
- `sentry dashboard create <args...>` — Create a dashboard
229+
- `sentry dashboard widget add <args...>` — Add a widget to a dashboard
230+
- `sentry dashboard widget edit <args...>` — Edit a widget in a dashboard
231+
- `sentry dashboard widget delete <args...>` — Delete a widget from a dashboard
229232

230233
→ Full flags and examples: `references/dashboards.md`
231234

plugins/sentry-cli/skills/sentry-cli/references/dashboards.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,41 @@ sentry dashboard create 'Frontend Performance'
8585
sentry dashboard widget add 'Frontend Performance' "Error Count" --display big_number --query count
8686
```
8787

88+
### `sentry dashboard widget add <args...>`
89+
90+
Add a widget to a dashboard
91+
92+
**Flags:**
93+
- `-d, --display <value> - Display type (line, bar, table, big_number, ...)`
94+
- `--dataset <value> - Widget dataset (default: spans)`
95+
- `-q, --query <value>... - Aggregate expression (e.g. count, p95:span.duration)`
96+
- `-w, --where <value> - Search conditions filter (e.g. is:unresolved)`
97+
- `-g, --group-by <value>... - Group-by column (repeatable)`
98+
- `-s, --sort <value> - Order by (prefix - for desc, e.g. -count)`
99+
- `-n, --limit <value> - Result limit`
100+
101+
### `sentry dashboard widget edit <args...>`
102+
103+
Edit a widget in a dashboard
104+
105+
**Flags:**
106+
- `-i, --index <value> - Widget index (0-based)`
107+
- `-t, --title <value> - Widget title to match`
108+
- `--new-title <value> - New widget title`
109+
- `-d, --display <value> - Display type (line, bar, table, big_number, ...)`
110+
- `--dataset <value> - Widget dataset (default: spans)`
111+
- `-q, --query <value>... - Aggregate expression (e.g. count, p95:span.duration)`
112+
- `-w, --where <value> - Search conditions filter (e.g. is:unresolved)`
113+
- `-g, --group-by <value>... - Group-by column (repeatable)`
114+
- `-s, --sort <value> - Order by (prefix - for desc, e.g. -count)`
115+
- `-n, --limit <value> - Result limit`
116+
117+
### `sentry dashboard widget delete <args...>`
118+
119+
Delete a widget from a dashboard
120+
121+
**Flags:**
122+
- `-i, --index <value> - Widget index (0-based)`
123+
- `-t, --title <value> - Widget title to match`
124+
88125
All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.

src/lib/help.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,12 +281,17 @@ function formatGroupHuman(group: RouteInfo): string {
281281
return lines.join("\n");
282282
}
283283

284-
const maxName = Math.max(
285-
...group.commands.map((c) => (c.path.split(" ").at(-1) ?? "").length)
286-
);
284+
// Strip "sentry <group> " prefix to get subcommand name.
285+
// For nested routes like "sentry dashboard widget add", this yields "widget add".
286+
const prefix = `sentry ${group.name} `;
287+
const subName = (cmd: CommandInfo) =>
288+
cmd.path.startsWith(prefix)
289+
? cmd.path.slice(prefix.length)
290+
: (cmd.path.split(" ").at(-1) ?? "");
291+
292+
const maxName = Math.max(...group.commands.map((c) => subName(c).length));
287293
for (const cmd of group.commands) {
288-
const name = cmd.path.split(" ").at(-1) ?? "";
289-
lines.push(` ${name.padEnd(maxName + 2)}${cmd.brief}`);
294+
lines.push(` ${subName(cmd).padEnd(maxName + 2)}${cmd.brief}`);
290295
}
291296

292297
return lines.join("\n");

src/lib/introspect.ts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,11 @@ export function extractRouteGroupCommands(
250250
const path = `sentry ${routeName} ${subEntry.name.original}`;
251251
const examples = docExamples.get(path) ?? [];
252252
commands.push(buildCommandInfo(subTarget, path, examples));
253+
} else if (isRouteMap(subTarget)) {
254+
const nestedPrefix = `${routeName} ${subEntry.name.original}`;
255+
commands.push(
256+
...extractRouteGroupCommands(subTarget, nestedPrefix, docExamples)
257+
);
253258
}
254259
}
255260

@@ -294,6 +299,9 @@ export function extractAllRoutes(routeMap: RouteMap): RouteInfo[] {
294299
return result;
295300
}
296301

302+
/** Matches the "sentry " prefix at the start of a command path. */
303+
const SENTRY_PREFIX_RE = /^sentry /;
304+
297305
/** Maximum number of fuzzy suggestions to include in an UnresolvedPath. */
298306
const MAX_SUGGESTIONS = 3;
299307

@@ -369,40 +377,36 @@ export function resolveCommandPath(
369377
return null;
370378
}
371379

372-
// Only support exactly 2 levels deep (group + command).
373-
// Extra segments (e.g. ["issue", "list", "extra"]) are rejected.
374-
if (rest.length !== 1) {
380+
// Recurse into the sub-route map with remaining path segments
381+
const subResult = resolveCommandPath(target, rest);
382+
if (!subResult) {
375383
return null;
376384
}
377385

378-
// Find the subcommand, with fuzzy fallback
379-
const subName = rest[0];
380-
if (subName === undefined) {
381-
return null;
382-
}
383-
const visibleSubEntries = target.getAllEntries().filter((e) => !e.hidden);
384-
const subEntry = visibleSubEntries.find((e) => e.name.original === subName);
386+
// Prepend the parent route segment to all paths in the result
387+
const parentPrefix = entry.name.original;
388+
const prependPrefix = (p: string) =>
389+
p.replace(SENTRY_PREFIX_RE, `sentry ${parentPrefix} `);
385390

386-
if (!subEntry) {
387-
const subNames = visibleSubEntries.map((e) => e.name.original);
391+
if (subResult.kind === "command") {
388392
return {
389-
kind: "unresolved",
390-
input: subName,
391-
suggestions: fuzzyMatch(subName, subNames, {
392-
maxResults: MAX_SUGGESTIONS,
393-
}),
393+
kind: "command",
394+
info: { ...subResult.info, path: prependPrefix(subResult.info.path) },
394395
};
395396
}
396-
397-
if (isCommand(subEntry.target)) {
397+
if (subResult.kind === "group") {
398398
return {
399-
kind: "command",
400-
info: buildCommandInfo(
401-
subEntry.target,
402-
`sentry ${entry.name.original} ${subEntry.name.original}`
403-
),
399+
kind: "group",
400+
info: {
401+
...subResult.info,
402+
name: `${parentPrefix} ${subResult.info.name}`,
403+
commands: subResult.info.commands.map((cmd) => ({
404+
...cmd,
405+
path: prependPrefix(cmd.path),
406+
})),
407+
},
404408
};
405409
}
406410

407-
return null;
411+
return subResult;
408412
}

test/commands/help.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ describe("sentry help --json <group> <command>", () => {
146146
});
147147
});
148148

149+
describe("sentry help --json nested routes (dashboard widget)", () => {
150+
test("nested group has correct name and commands", async () => {
151+
const output = await runHelp(["--json", "dashboard", "widget"]);
152+
const parsed = JSON.parse(output);
153+
154+
expect(parsed).toHaveProperty("name", "dashboard widget");
155+
expect(parsed).toHaveProperty("commands");
156+
const paths = parsed.commands.map((c: { path: string }) => c.path);
157+
expect(paths).toContain("sentry dashboard widget add");
158+
expect(paths).toContain("sentry dashboard widget edit");
159+
expect(paths).toContain("sentry dashboard widget delete");
160+
});
161+
162+
test("nested command resolves with full path", async () => {
163+
const output = await runHelp(["--json", "dashboard", "widget", "add"]);
164+
const parsed = JSON.parse(output);
165+
166+
expect(parsed).toHaveProperty("path", "sentry dashboard widget add");
167+
expect(parsed).toHaveProperty("flags");
168+
});
169+
});
170+
149171
describe("introspectCommand error cases", () => {
150172
// Error cases throw OutputError (which calls process.exit) through the
151173
// framework, so we test the introspection functions directly here.

0 commit comments

Comments
 (0)