Skip to content

Commit d4a7de3

Browse files
committed
refactor: unify dry-run and normal output in project create
Both paths now construct a ProjectCreatedResult and return { data } through buildCommand's output wrapper. The single formatProjectCreated formatter handles both modes — dry-run adds a dryRun flag that adjusts the heading and team source note wording. Removes DryRunData type, formatDryRun function, and writeOutput import. Net -32 lines.
1 parent 849aad2 commit d4a7de3

File tree

3 files changed

+53
-85
lines changed

3 files changed

+53
-85
lines changed

src/commands/project/create.ts

Lines changed: 14 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,7 @@ import {
3434
formatProjectCreated,
3535
type ProjectCreatedResult,
3636
} from "../../lib/formatters/human.js";
37-
import {
38-
escapeMarkdownInline,
39-
isPlainOutput,
40-
mdKvTable,
41-
renderMarkdown,
42-
safeCodeSpan,
43-
} from "../../lib/formatters/markdown.js";
44-
import { writeOutput } from "../../lib/formatters/output.js";
37+
import { isPlainOutput } from "../../lib/formatters/markdown.js";
4538
import { buildMarkdownTable, type Column } from "../../lib/formatters/table.js";
4639
import { renderTextTable } from "../../lib/formatters/text-table.js";
4740
import { logger } from "../../lib/logger.js";
@@ -65,50 +58,6 @@ const log = logger.withTag("project.create");
6558
/** Usage hint template — base command without positionals */
6659
const USAGE_HINT = "sentry project create <org>/<name> <platform>";
6760

68-
type DryRunData = {
69-
organization: string;
70-
team: string;
71-
teamSource: string;
72-
name: string;
73-
slug: string;
74-
platform: string;
75-
};
76-
77-
/** Format dry-run preview as human-readable markdown */
78-
function formatDryRun(data: DryRunData): string {
79-
const lines: string[] = [];
80-
81-
lines.push(
82-
`## <muted>Dry run</muted> — project '${escapeMarkdownInline(data.name)}' in ${escapeMarkdownInline(data.organization)}`
83-
);
84-
lines.push("");
85-
86-
// Team source notes (same pattern as formatProjectCreated)
87-
if (data.teamSource === "auto-created") {
88-
lines.push(
89-
`> **Note:** Would create team '${escapeMarkdownInline(data.team)}' (org has no teams).`
90-
);
91-
lines.push("");
92-
} else if (data.teamSource === "auto-selected") {
93-
lines.push(
94-
`> **Note:** Would use team '${escapeMarkdownInline(data.team)}'. See all teams: \`sentry team list\``
95-
);
96-
lines.push("");
97-
}
98-
99-
const kvRows: [string, string][] = [
100-
["Name", escapeMarkdownInline(data.name)],
101-
["Slug", safeCodeSpan(data.slug)],
102-
["Org", safeCodeSpan(data.organization)],
103-
["Team", safeCodeSpan(data.team)],
104-
["Platform", data.platform],
105-
];
106-
107-
lines.push(mdKvTable(kvRows));
108-
109-
return renderMarkdown(lines.join("\n"));
110-
}
111-
11261
type CreateFlags = {
11362
readonly team?: string;
11463
readonly "dry-run": boolean;
@@ -440,23 +389,23 @@ export const createCommand = buildCommand({
440389
dryRun: flags["dry-run"],
441390
});
442391

392+
const expectedSlug = slugify(name);
393+
443394
// Dry-run mode: show what would be created without creating it
444395
if (flags["dry-run"]) {
445-
const dryRunData = {
446-
organization: orgSlug,
447-
team: team.slug,
396+
const result: ProjectCreatedResult = {
397+
project: { id: "", slug: expectedSlug, name, platform },
398+
orgSlug,
399+
teamSlug: team.slug,
448400
teamSource: team.source,
449-
name,
450-
slug: slugify(name),
451-
platform,
401+
requestedPlatform: platform,
402+
dsn: null,
403+
url: "",
404+
slugDiverged: false,
405+
expectedSlug,
406+
dryRun: true,
452407
};
453-
454-
writeOutput(this.stdout, dryRunData, {
455-
json: flags.json,
456-
fields: flags.fields,
457-
formatHuman: formatDryRun,
458-
});
459-
return;
408+
return { data: result };
460409
}
461410

462411
// Create the project
@@ -471,8 +420,6 @@ export const createCommand = buildCommand({
471420
// Fetch DSN (best-effort)
472421
const dsn = await tryGetPrimaryDsn(orgSlug, project.slug);
473422

474-
const expectedSlug = slugify(name);
475-
476423
const result: ProjectCreatedResult = {
477424
project,
478425
orgSlug,

src/lib/formatters/human.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,6 +1631,8 @@ export type ProjectCreatedResult = {
16311631
slugDiverged: boolean;
16321632
/** The slug the user expected (derived from the project name) */
16331633
expectedSlug: string;
1634+
/** When true, nothing was actually created — output uses tentative wording */
1635+
dryRun?: boolean;
16341636
};
16351637

16361638
/**
@@ -1644,35 +1646,45 @@ export type ProjectCreatedResult = {
16441646
*/
16451647
export function formatProjectCreated(result: ProjectCreatedResult): string {
16461648
const lines: string[] = [];
1649+
const dry = result.dryRun === true;
1650+
const nameEsc = escapeMarkdownInline(result.project.name);
1651+
const orgEsc = escapeMarkdownInline(result.orgSlug);
16471652

1648-
lines.push(
1649-
`## Created project '${escapeMarkdownInline(result.project.name)}' in ${escapeMarkdownInline(result.orgSlug)}`
1650-
);
1653+
// Heading
1654+
if (dry) {
1655+
lines.push(`## <muted>Dry run</muted> — project '${nameEsc}' in ${orgEsc}`);
1656+
} else {
1657+
lines.push(`## Created project '${nameEsc}' in ${orgEsc}`);
1658+
}
16511659
lines.push("");
16521660

1653-
// Slug divergence note
1661+
// Slug divergence note (never applies in dry-run — we can't predict server renames)
16541662
if (result.slugDiverged) {
16551663
lines.push(
16561664
`> **Note:** Slug \`${result.project.slug}\` was assigned because \`${result.expectedSlug}\` is already taken.`
16571665
);
16581666
lines.push("");
16591667
}
16601668

1661-
// Team source notes
1669+
// Team source notes — tentative wording in dry-run
16621670
if (result.teamSource === "auto-created") {
16631671
lines.push(
1664-
`> **Note:** Created team '${escapeMarkdownInline(result.teamSlug)}' (org had no teams).`
1672+
dry
1673+
? `> **Note:** Would create team '${escapeMarkdownInline(result.teamSlug)}' (org has no teams).`
1674+
: `> **Note:** Created team '${escapeMarkdownInline(result.teamSlug)}' (org had no teams).`
16651675
);
16661676
lines.push("");
16671677
} else if (result.teamSource === "auto-selected") {
16681678
lines.push(
1669-
`> **Note:** Using team '${escapeMarkdownInline(result.teamSlug)}'. See all teams: \`sentry team list\``
1679+
dry
1680+
? `> **Note:** Would use team '${escapeMarkdownInline(result.teamSlug)}'. See all teams: \`sentry team list\``
1681+
: `> **Note:** Using team '${escapeMarkdownInline(result.teamSlug)}'. See all teams: \`sentry team list\``
16701682
);
16711683
lines.push("");
16721684
}
16731685

16741686
const kvRows: [string, string][] = [
1675-
["Project", escapeMarkdownInline(result.project.name)],
1687+
["Project", nameEsc],
16761688
["Slug", safeCodeSpan(result.project.slug)],
16771689
["Org", safeCodeSpan(result.orgSlug)],
16781690
["Team", safeCodeSpan(result.teamSlug)],
@@ -1681,13 +1693,19 @@ export function formatProjectCreated(result: ProjectCreatedResult): string {
16811693
if (result.dsn) {
16821694
kvRows.push(["DSN", safeCodeSpan(result.dsn)]);
16831695
}
1684-
kvRows.push(["URL", result.url]);
1696+
if (result.url) {
1697+
kvRows.push(["URL", result.url]);
1698+
}
16851699

16861700
lines.push(mdKvTable(kvRows));
1687-
lines.push("");
1688-
lines.push(
1689-
`*Tip: Use \`sentry project view ${result.orgSlug}/${result.project.slug}\` for details*`
1690-
);
1701+
1702+
// Tip footer — only when a real project exists to view
1703+
if (!dry) {
1704+
lines.push("");
1705+
lines.push(
1706+
`*Tip: Use \`sentry project view ${result.orgSlug}/${result.project.slug}\` for details*`
1707+
);
1708+
}
16911709

16921710
return renderMarkdown(lines.join("\n"));
16931711
}

test/commands/project/create.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -691,11 +691,14 @@ describe("project create", () => {
691691

692692
const output = stdoutWrite.mock.calls.map((c) => c[0]).join("");
693693
const parsed = JSON.parse(output);
694-
expect(parsed.organization).toBe("acme-corp");
695-
expect(parsed.team).toBe("engineering");
696-
expect(parsed.name).toBe("my-app");
697-
expect(parsed.slug).toBe("my-app");
698-
expect(parsed.platform).toBe("node");
694+
// Same ProjectCreatedResult shape as normal path
695+
expect(parsed.orgSlug).toBe("acme-corp");
696+
expect(parsed.teamSlug).toBe("engineering");
697+
expect(parsed.project.name).toBe("my-app");
698+
expect(parsed.project.slug).toBe("my-app");
699+
expect(parsed.project.platform).toBe("node");
700+
expect(parsed.dsn).toBeNull();
701+
expect(parsed.dryRun).toBe(true);
699702

700703
// Should NOT call createProject
701704
expect(createProjectSpy).not.toHaveBeenCalled();

0 commit comments

Comments
 (0)