Skip to content

Commit 54fad1a

Browse files
authored
refactor(init): split tools and preflight (#764)
## Summary - rename init local operations to tools with an explicit file-per-tool registry - extract org/project/team/auth resolution into preflight and pass an immutable init context into the runner - move init-specific tests to preflight/tool suites and preserve existing project reuse behavior ## Testing - bun run typecheck - bun test test/lib/init/interactive.test.ts test/lib/init/preflight.test.ts test/lib/init/wizard-runner.test.ts test/lib/init/tools/run-commands.test.ts test/lib/init/tools/registry.test.ts test/lib/init/tools/filesystem-tools.test.ts test/lib/init/tools/create-sentry-project.test.ts
1 parent cf8ffb8 commit 54fad1a

36 files changed

Lines changed: 3818 additions & 4855 deletions

‎src/commands/init.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { findProjectsBySlug } from "../lib/api/projects.js";
2323
import { looksLikePath, parseOrgProjectArg } from "../lib/arg-parsing.js";
2424
import { buildCommand } from "../lib/command.js";
2525
import { ContextError, ValidationError } from "../lib/errors.js";
26-
import { warmOrgDetection } from "../lib/init/prefetch.js";
26+
import { warmOrgDetection } from "../lib/init/org-prefetch.js";
2727
import { runWizard } from "../lib/init/wizard-runner.js";
2828
import { validateResourceId } from "../lib/input-validation.js";
2929
import { logger } from "../lib/logger.js";
@@ -104,7 +104,7 @@ function classifyArgs(
104104
*
105105
* For `project-search` (bare slug), searches for an existing project first.
106106
* If not found, treats the slug as a **new project name** to create —
107-
* org will be resolved later by the wizard's `resolveOrgSlug()`.
107+
* org will be resolved later by init preflight before the workflow starts.
108108
* If the slug matches an org name, treats it as org-only (like `slug/`).
109109
*/
110110
async function resolveTarget(targetArg: string | undefined): Promise<{
@@ -154,7 +154,7 @@ async function resolveTarget(targetArg: string | undefined): Promise<{
154154
}
155155

156156
// Truly not found — treat as the name for a new project to create.
157-
// Org will be resolved later by the wizard via resolveOrgSlug().
157+
// Org will be resolved later by init preflight before the workflow starts.
158158
log.info(
159159
`No existing project "${parsed.projectSlug}" found — will create a new project with this name.`
160160
);

‎src/commands/project/create.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
import { resolveOrg } from "../../lib/resolve-target.js";
5050
import {
5151
buildOrgNotFoundError,
52-
type ResolvedTeam,
52+
type ResolvedConcreteTeam,
5353
resolveOrCreateTeam,
5454
} from "../../lib/resolve-team.js";
5555
import { slugify } from "../../lib/utils.js";
@@ -380,7 +380,7 @@ export const createCommand = buildCommand({
380380
const orgSlug = resolved.org;
381381

382382
// Resolve team — auto-creates a team if the org has none
383-
const team: ResolvedTeam = await resolveOrCreateTeam(orgSlug, {
383+
const team: ResolvedConcreteTeam = await resolveOrCreateTeam(orgSlug, {
384384
team: flags.team,
385385
detectedFrom: resolved.detectedFrom,
386386
usageHint: USAGE_HINT,

‎src/lib/init/existing-project.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { getProject, tryGetPrimaryDsn } from "../api-client.js";
2+
import { ApiError } from "../errors.js";
3+
import { buildProjectUrl } from "../sentry-urls.js";
4+
import type { ExistingProjectData } from "./types.js";
5+
6+
/**
7+
* Fetch Sentry metadata for an existing project.
8+
*
9+
* Returns `null` when the project does not exist, while allowing other API
10+
* errors to propagate so callers can decide whether the lookup is best-effort
11+
* or should fail the current operation.
12+
*/
13+
export async function tryGetExistingProjectData(
14+
orgSlug: string,
15+
projectSlug: string
16+
): Promise<ExistingProjectData | null> {
17+
try {
18+
const project = await getProject(orgSlug, projectSlug);
19+
const dsn = await tryGetPrimaryDsn(orgSlug, project.slug);
20+
return {
21+
orgSlug,
22+
projectSlug: project.slug,
23+
projectId: project.id,
24+
dsn: dsn ?? "",
25+
url: buildProjectUrl(orgSlug, project.slug),
26+
};
27+
} catch (error) {
28+
if (error instanceof ApiError && error.status === 404) {
29+
return null;
30+
}
31+
throw error;
32+
}
33+
}

‎src/lib/init/interactive.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ import {
1717
import { REQUIRED_FEATURE } from "./constants.js";
1818
import type {
1919
ConfirmPayload,
20+
InteractiveContext,
2021
InteractivePayload,
2122
MultiSelectPayload,
2223
SelectPayload,
23-
WizardOptions,
2424
} from "./types.js";
2525

2626
export async function handleInteractive(
2727
payload: InteractivePayload,
28-
options: WizardOptions
28+
options: InteractiveContext
2929
): Promise<Record<string, unknown>> {
3030
switch (payload.kind) {
3131
case "select":
@@ -41,7 +41,7 @@ export async function handleInteractive(
4141

4242
async function handleSelect(
4343
payload: SelectPayload,
44-
options: WizardOptions
44+
options: InteractiveContext
4545
): Promise<Record<string, unknown>> {
4646
const apps = payload.apps ?? [];
4747
const items = payload.options ?? apps.map((a) => a.name);
@@ -78,7 +78,7 @@ async function handleSelect(
7878

7979
async function handleMultiSelect(
8080
payload: MultiSelectPayload,
81-
options: WizardOptions
81+
options: InteractiveContext
8282
): Promise<Record<string, unknown>> {
8383
const available = payload.availableFeatures ?? payload.options ?? [];
8484

@@ -137,7 +137,7 @@ async function handleMultiSelect(
137137

138138
async function handleConfirm(
139139
payload: ConfirmPayload,
140-
options: WizardOptions
140+
options: InteractiveContext
141141
): Promise<Record<string, unknown>> {
142142
if (options.yes) {
143143
log.info("Auto-confirmed: continuing");

0 commit comments

Comments
 (0)