Skip to content

Commit 16edb37

Browse files
fix(init): reject ambiguous bare target and validate org/project slugs
Bare strings like 'sentry init acme' are ambiguous (could be org or project slug). Now throws a ContextError with a disambiguation hint telling users to use 'acme/' for org or 'acme/<project>' for both. Also validates explicit org and project slugs via validateResourceId to catch malformed input (control chars, whitespace, etc.) early before API calls.
1 parent 8cf50ce commit 16edb37

File tree

2 files changed

+63
-21
lines changed

2 files changed

+63
-21
lines changed

src/commands/init.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*
88
* Supports org/project positional syntax to pin org and/or project name:
99
* sentry init — auto-detect everything
10-
* sentry init acme — explicit org, wizard picks project name
10+
* sentry init acme/ — explicit org, wizard picks project name
1111
* sentry init acme/my-app — explicit org + project name override
1212
* sentry init --directory ./dir — specify project directory
1313
*/
@@ -18,6 +18,7 @@ import { parseOrgProjectArg } from "../lib/arg-parsing.js";
1818
import { buildCommand } from "../lib/command.js";
1919
import { ContextError } from "../lib/errors.js";
2020
import { runWizard } from "../lib/init/wizard-runner.js";
21+
import { validateResourceId } from "../lib/input-validation.js";
2122

2223
const FEATURE_DELIMITER = /[,+ ]+/;
2324

@@ -39,7 +40,7 @@ export const initCommand = buildCommand<InitFlags, [string?], SentryContext>({
3940
"If omitted, the org is auto-detected from config defaults.\n\n" +
4041
"Examples:\n" +
4142
" sentry init\n" +
42-
" sentry init acme\n" +
43+
" sentry init acme/\n" +
4344
" sentry init acme/my-app\n" +
4445
" sentry init acme/my-app --directory ./my-project\n" +
4546
" sentry init --directory ./my-project",
@@ -50,7 +51,7 @@ export const initCommand = buildCommand<InitFlags, [string?], SentryContext>({
5051
parameters: [
5152
{
5253
placeholder: "target",
53-
brief: "<org>/<project>, <org>, or omit for auto-detect",
54+
brief: "<org>/<project>, <org>/, or omit for auto-detect",
5455
parse: String,
5556
optional: true,
5657
},
@@ -115,15 +116,14 @@ export const initCommand = buildCommand<InitFlags, [string?], SentryContext>({
115116
explicitProject = parsed.project;
116117
break;
117118
case "org-all":
118-
// "acme/" or bare "acme" — org only, no project name override
119119
explicitOrg = parsed.org;
120120
break;
121121
case "project-search":
122-
// Bare string without "/" — could be an org slug or a project name.
123-
// Treat it as an org slug since `sentry init <org>` is the primary use case.
124-
// Users who want to override the project name should use org/project syntax.
125-
explicitOrg = parsed.projectSlug;
126-
break;
122+
// Bare string without "/" is ambiguous — could be an org or project slug.
123+
// Require the trailing slash to disambiguate (consistent with other commands).
124+
throw new ContextError("Target", `sentry init ${parsed.projectSlug}/`, [
125+
`'${parsed.projectSlug}' is ambiguous. Use '${parsed.projectSlug}/' for org or '${parsed.projectSlug}/<project>' for org + project.`,
126+
]);
127127
case "auto-detect":
128128
// No target provided — auto-detect everything
129129
break;
@@ -133,6 +133,14 @@ export const initCommand = buildCommand<InitFlags, [string?], SentryContext>({
133133
}
134134
}
135135

136+
// Validate explicit org slug format before passing to API calls
137+
if (explicitOrg) {
138+
validateResourceId(explicitOrg, "organization slug");
139+
}
140+
if (explicitProject) {
141+
validateResourceId(explicitProject, "project name");
142+
}
143+
136144
await runWizard({
137145
directory: targetDir,
138146
yes: flags.yes,

test/commands/init.test.ts

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
99
import path from "node:path";
1010
import { initCommand } from "../../src/commands/init.js";
11+
import { ContextError } from "../../src/lib/errors.js";
1112
// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
1213
import * as wizardRunner from "../../src/lib/init/wizard-runner.js";
1314

@@ -193,19 +194,18 @@ describe("init command func", () => {
193194
expect(capturedArgs?.project).toBe("my-app");
194195
});
195196

196-
test("parses bare string as org (no project override)", async () => {
197+
test("throws on bare string (ambiguous — could be org or project)", async () => {
197198
const ctx = makeContext();
198-
await func.call(
199-
ctx,
200-
{
201-
yes: true,
202-
"dry-run": false,
203-
},
204-
"acme"
205-
);
206-
207-
expect(capturedArgs?.org).toBe("acme");
208-
expect(capturedArgs?.project).toBeUndefined();
199+
expect(
200+
func.call(
201+
ctx,
202+
{
203+
yes: true,
204+
"dry-run": false,
205+
},
206+
"acme"
207+
)
208+
).rejects.toThrow(ContextError);
209209
});
210210

211211
test("parses org/ as org-only (no project override)", async () => {
@@ -258,5 +258,39 @@ describe("init command func", () => {
258258
expect(capturedArgs?.project).toBe("my-app");
259259
expect(capturedArgs?.team).toBe("backend");
260260
});
261+
262+
test("rejects org slug with invalid characters", async () => {
263+
const ctx = makeContext();
264+
expect(
265+
func.call(
266+
ctx,
267+
{
268+
yes: true,
269+
"dry-run": false,
270+
},
271+
"acme corp/"
272+
)
273+
).rejects.toThrow();
274+
});
275+
276+
test("error message for bare string includes disambiguation hint", async () => {
277+
const ctx = makeContext();
278+
try {
279+
await func.call(
280+
ctx,
281+
{
282+
yes: true,
283+
"dry-run": false,
284+
},
285+
"myorg"
286+
);
287+
expect.unreachable("should have thrown");
288+
} catch (error) {
289+
expect(error).toBeInstanceOf(ContextError);
290+
const msg = (error as ContextError).message;
291+
expect(msg).toContain("myorg/");
292+
expect(msg).toContain("myorg/<project>");
293+
}
294+
});
261295
});
262296
});

0 commit comments

Comments
 (0)