Skip to content

Commit 80c87b1

Browse files
betegonclaude
andauthored
feat(project): display platform suggestions in multi-column tables (#365)
## Summary Replaces the plain indented text lists in `project create` error messages with bordered 3-column tables using `renderTextTable`. Both the "Did you mean?" suggestions and "Common platforms" sections now render compactly instead of taking 30+ lines of vertical space. Also extracts platform validation into `src/lib/platforms.ts` with fuzzy matching (`suggestPlatform`) and validates client-side before making the API call for faster feedback on typos. ## Changes - New `src/lib/platforms.ts` module with `COMMON_PLATFORMS`, `isValidPlatform`, and `suggestPlatform` (Levenshtein distance + platform family matching) - `buildPlatformError` now renders both sections as 3-col `renderTextTable` tables with `headerSeparator: false` - Client-side validation rejects invalid platforms before the API roundtrip - Tests updated to match new output format and validate the platforms module ## Test Plan - `bun test test/commands/project/` — 109 tests pass - `bun test test/lib/platforms.test.ts` — all pass - `npx biome check src/commands/project/create.ts` — clean --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 453b52b commit 80c87b1

File tree

4 files changed

+474
-50
lines changed

4 files changed

+474
-50
lines changed

src/commands/project/create.ts

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ import {
3131
withAuthGuard,
3232
} from "../../lib/errors.js";
3333
import { formatProjectCreated, writeJson } from "../../lib/formatters/index.js";
34+
import { isPlainOutput } from "../../lib/formatters/markdown.js";
35+
import { buildMarkdownTable, type Column } from "../../lib/formatters/table.js";
36+
import { renderTextTable } from "../../lib/formatters/text-table.js";
37+
import {
38+
COMMON_PLATFORMS,
39+
isValidPlatform,
40+
suggestPlatform,
41+
} from "../../lib/platforms.js";
3442
import { resolveOrg } from "../../lib/resolve-target.js";
3543
import {
3644
buildOrgNotFoundError,
@@ -48,42 +56,34 @@ type CreateFlags = {
4856
readonly json: boolean;
4957
};
5058

51-
/**
52-
* Common Sentry platform identifiers shown when platform arg is missing or invalid.
53-
*
54-
* These use hyphen-separated format matching Sentry's internal platform registry
55-
* (see sentry/src/sentry/utils/platform_categories.py). This is a curated subset
56-
* of the ~120 supported values — the full list is available via the API endpoint
57-
* referenced in `buildPlatformError`.
58-
*/
59-
const PLATFORMS = [
60-
"javascript",
61-
"javascript-react",
62-
"javascript-nextjs",
63-
"javascript-vue",
64-
"javascript-angular",
65-
"javascript-svelte",
66-
"javascript-remix",
67-
"javascript-astro",
68-
"node",
69-
"node-express",
70-
"python",
71-
"python-django",
72-
"python-flask",
73-
"python-fastapi",
74-
"go",
75-
"ruby",
76-
"ruby-rails",
77-
"php",
78-
"php-laravel",
79-
"java",
80-
"android",
81-
"dotnet",
82-
"react-native",
83-
"apple-ios",
84-
"rust",
85-
"elixir",
86-
] as const;
59+
/** Build a 3-column grid string from a flat list of platforms. */
60+
function platformGrid(items: readonly string[]): string {
61+
const COLS = 3;
62+
const rows: string[][] = [];
63+
for (let i = 0; i < items.length; i += COLS) {
64+
const row = items.slice(i, i + COLS);
65+
while (row.length < COLS) {
66+
row.push("");
67+
}
68+
rows.push(row);
69+
}
70+
71+
if (isPlainOutput()) {
72+
const columns: Column<string[]>[] = Array.from(
73+
{ length: COLS },
74+
(_, ci) => ({
75+
header: " ",
76+
value: (row: string[]) => row[ci] ?? "",
77+
})
78+
);
79+
return buildMarkdownTable(rows, columns);
80+
}
81+
82+
const [first, ...rest] = rows;
83+
return renderTextTable(first ?? [], rest, {
84+
headerSeparator: false,
85+
});
86+
}
8787

8888
/**
8989
* Normalize common platform format mistakes.
@@ -142,16 +142,26 @@ function isPlatformError(error: ApiError): boolean {
142142
* @param platform - The invalid platform string, if provided
143143
*/
144144
function buildPlatformError(nameArg: string, platform?: string): string {
145-
const list = PLATFORMS.map((p) => ` ${p}`).join("\n");
146145
const heading = platform
147146
? `Invalid platform '${platform}'.`
148147
: "Platform is required.";
149148

149+
let didYouMean = "";
150+
if (platform) {
151+
const suggestions = suggestPlatform(platform);
152+
if (suggestions.length > 0) {
153+
didYouMean = `\nDid you mean?\n${platformGrid(suggestions)}`;
154+
}
155+
}
156+
157+
const platformTable = platformGrid([...COMMON_PLATFORMS]);
158+
150159
return (
151-
`${heading}\n\n` +
152-
"Usage:\n" +
160+
`${heading}\n` +
161+
didYouMean +
162+
"\nUsage:\n" +
153163
` sentry project create ${nameArg} <platform>\n\n` +
154-
`Available platforms:\n\n${list}\n\n` +
164+
`Common platforms:\n\n${platformTable}\n` +
155165
"Run 'sentry project create <name> <platform>' with any valid Sentry platform identifier."
156166
);
157167
}
@@ -332,6 +342,10 @@ export const createCommand = buildCommand({
332342

333343
const platform = normalizePlatform(platformArg, this.stderr);
334344

345+
if (!isValidPlatform(platform)) {
346+
throw new CliError(buildPlatformError(nameArg, platform));
347+
}
348+
335349
const parsed = parseOrgProjectArg(nameArg);
336350

337351
let explicitOrg: string | undefined;

0 commit comments

Comments
 (0)