-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: new intro in CLI #84
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
15d9592
initial draft for the new intro
MathurAditya724 7304099
fix: minor adjustments
MathurAditya724 0bb8781
chore: minor changes
MathurAditya724 5e31c41
chore: minor change
MathurAditya724 c67e355
chore: minor change
MathurAditya724 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ export const authRoute = buildRouteMap({ | |
| "Manage authentication with Sentry. Use 'sentry auth login' to authenticate, " + | ||
| "'sentry auth logout' to remove credentials, 'sentry auth refresh' to manually refresh your token, " + | ||
| "and 'sentry auth status' to check your authentication status.", | ||
| hideRoute: {}, | ||
| hideRoute: { | ||
| refresh: true, // Advanced command, hide from top-level help | ||
|
Member
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. Erm, why?
Member
Author
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. Just for the display stuff, that is |
||
| }, | ||
| }, | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| /** | ||
| * Custom Help Output | ||
| * | ||
| * Provides a branded, styled help output for the CLI. | ||
| * Shows custom formatting when running `sentry` with no arguments. | ||
| * Commands are auto-generated from Stricli's route structure. | ||
| */ | ||
|
|
||
| import chalk from "chalk"; | ||
| import { routes } from "../app.js"; | ||
| import type { Writer } from "../types/index.js"; | ||
| import { isAuthenticated } from "./config.js"; | ||
| import { cyan, magenta, muted } from "./formatters/colors.js"; | ||
|
|
||
| /** ASCII art banner rows for gradient coloring */ | ||
| const BANNER_ROWS = [ | ||
| " ███████╗███████╗███╗ ██╗████████╗██████╗ ██╗ ██╗", | ||
| " ██╔════╝██╔════╝████╗ ██║╚══██╔══╝██╔══██╗╚██╗ ██╔╝", | ||
| " ███████╗█████╗ ██╔██╗ ██║ ██║ ██████╔╝ ╚████╔╝ ", | ||
| " ╚════██║██╔══╝ ██║╚██╗██║ ██║ ██╔══██╗ ╚██╔╝ ", | ||
| " ███████║███████╗██║ ╚████║ ██║ ██║ ██║ ██║ ", | ||
| " ╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ", | ||
| ]; | ||
|
|
||
| /** Purple gradient colors from bright to dark (Sentry brand-inspired) */ | ||
| const BANNER_GRADIENT = [ | ||
| "#B4A4DE", | ||
| "#9C84D4", | ||
| "#8468C8", | ||
| "#6C4EBA", | ||
| "#5538A8", | ||
| "#432B8A", | ||
| ]; | ||
|
|
||
| /** | ||
| * Format the banner with a vertical gradient effect. | ||
| * Each row gets progressively darker purple. | ||
| */ | ||
| function formatBanner(): string { | ||
| return BANNER_ROWS.map((row, i) => { | ||
| const color = BANNER_GRADIENT[i] ?? "#B4A4DE"; | ||
| return chalk.hex(color)(row); | ||
| }).join("\n"); | ||
| } | ||
|
|
||
| const TAGLINE = "The command-line interface for Sentry"; | ||
|
|
||
| type HelpCommand = { | ||
| usage: string; | ||
| description: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Type guard to check if a routing target is a RouteMap (has subcommands). | ||
| * RouteMap has getAllEntries(), Command does not. | ||
| */ | ||
| function isRouteMap( | ||
| target: unknown | ||
| ): target is { getAllEntries: () => RouteMapEntry[]; brief: string } { | ||
| return ( | ||
| typeof target === "object" && | ||
| target !== null && | ||
| "getAllEntries" in target && | ||
| typeof (target as { getAllEntries: unknown }).getAllEntries === "function" | ||
| ); | ||
| } | ||
|
|
||
| /** Minimal type for route map entries returned by getAllEntries() */ | ||
| type RouteMapEntry = { | ||
| name: { original: string }; | ||
| target: { brief: string }; | ||
| hidden: boolean; | ||
| }; | ||
|
|
||
| /** Minimal type for positional parameter with optional placeholder */ | ||
| type PositionalParam = { placeholder?: string }; | ||
|
|
||
| /** Stricli positional parameters structure */ | ||
| type PositionalParams = | ||
| | { kind: "tuple"; parameters: PositionalParam[] } | ||
| | { kind: "array"; parameter: PositionalParam }; | ||
|
|
||
| /** | ||
| * Type guard to check if a target is a Command (has parameters). | ||
| */ | ||
| function isCommand(target: unknown): target is { | ||
| brief: string; | ||
| parameters: { positional?: PositionalParams }; | ||
| } { | ||
| return ( | ||
| typeof target === "object" && | ||
| target !== null && | ||
| "parameters" in target && | ||
| !("getAllEntries" in target) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Extract placeholder text from a command's positional parameters. | ||
| * Returns placeholders like "<endpoint>" or defaults to "<...>". | ||
| */ | ||
| function getPositionalPlaceholder(target: unknown): string { | ||
| if (!isCommand(target)) { | ||
| return "<...>"; | ||
| } | ||
|
|
||
| const positional = target.parameters.positional; | ||
| if (!positional) { | ||
| return ""; | ||
| } | ||
|
|
||
| if (positional.kind === "tuple" && positional.parameters.length > 0) { | ||
| // Get placeholders from tuple parameters, default to "arg" if not specified | ||
| const placeholders = positional.parameters.map( | ||
| (p, i) => `<${p.placeholder ?? `arg${i}`}>` | ||
| ); | ||
| return placeholders.join(" "); | ||
| } | ||
|
|
||
| if (positional.kind === "array") { | ||
| const placeholder = positional.parameter.placeholder ?? "args"; | ||
| return `<${placeholder}...>`; | ||
| } | ||
|
|
||
| return "<...>"; | ||
| } | ||
|
|
||
| /** | ||
| * Generate the commands list dynamically from Stricli's route structure. | ||
| * This ensures help text stays in sync with actual registered commands. | ||
| */ | ||
| function generateCommands(): HelpCommand[] { | ||
| const entries = routes.getAllEntries(); | ||
|
|
||
| return entries | ||
| .filter((entry: RouteMapEntry) => !entry.hidden) | ||
| .map((entry: RouteMapEntry) => { | ||
| const routeName = entry.name.original; | ||
| const brief = entry.target.brief; | ||
|
|
||
| if (isRouteMap(entry.target)) { | ||
| // Get visible subcommand names and join with pipes | ||
| const subEntries = entry.target | ||
| .getAllEntries() | ||
| .filter((sub: RouteMapEntry) => !sub.hidden); | ||
| const subNames = subEntries | ||
| .map((sub: RouteMapEntry) => sub.name.original) | ||
| .join(" | "); | ||
| return { | ||
| usage: `sentry ${routeName} ${subNames}`, | ||
| description: brief, | ||
| }; | ||
| } | ||
|
|
||
| // Direct command - extract placeholder from positional parameters | ||
| const placeholder = getPositionalPlaceholder(entry.target); | ||
| const usageSuffix = placeholder ? ` ${placeholder}` : ""; | ||
| return { | ||
| usage: `sentry ${routeName}${usageSuffix}`, | ||
| description: brief, | ||
| }; | ||
| }); | ||
| } | ||
|
|
||
| const EXAMPLE_LOGGED_OUT = "sentry auth login"; | ||
| const EXAMPLE_LOGGED_IN = "sentry issue list"; | ||
| const DOCS_URL = "https://docs.sentry.io/cli/"; | ||
|
|
||
| /** | ||
| * Format the command list with aligned descriptions. | ||
| * | ||
| * @param commands - Array of commands to format | ||
| * @returns Formatted string with aligned columns | ||
| */ | ||
| function formatCommands(commands: HelpCommand[]): string { | ||
| const maxUsageLength = Math.max(...commands.map((cmd) => cmd.usage.length)); | ||
| const padding = 4; | ||
|
|
||
| return commands | ||
| .map((cmd) => { | ||
| const usagePadded = cmd.usage.padEnd(maxUsageLength + padding); | ||
| return ` $ ${cyan(usagePadded)}${muted(cmd.description)}`; | ||
| }) | ||
| .join("\n"); | ||
| } | ||
|
|
||
| /** | ||
| * Print the custom branded help output. | ||
| * Shows a contextual example based on authentication status. | ||
| * | ||
| * @param stdout - Writer to output help text | ||
| */ | ||
| export async function printCustomHelp(stdout: Writer): Promise<void> { | ||
| const loggedIn = await isAuthenticated(); | ||
| const example = loggedIn ? EXAMPLE_LOGGED_IN : EXAMPLE_LOGGED_OUT; | ||
|
|
||
| const lines: string[] = []; | ||
|
|
||
| // Banner with gradient | ||
| lines.push(""); | ||
| lines.push(formatBanner()); | ||
| lines.push(""); | ||
|
|
||
| // Tagline | ||
| lines.push(` ${TAGLINE}`); | ||
| lines.push(""); | ||
|
|
||
| // Commands (auto-generated from Stricli routes) | ||
| lines.push(formatCommands(generateCommands())); | ||
| lines.push(""); | ||
|
|
||
| // Example | ||
| lines.push(` ${muted("try:")} ${magenta(example)}`); | ||
| lines.push(""); | ||
|
|
||
| // Footer | ||
| lines.push(` ${muted(`Learn more at ${DOCS_URL}`)}`); | ||
| lines.push(""); | ||
| lines.push(""); | ||
|
|
||
| stdout.write(lines.join("\n")); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
What does stricli do when you don't pass any arguments?
Uh oh!
There was an error while loading. Please reload this page.
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.
Print the help menu, the inbuilt one