Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ Lint enforcement: `stderr.write()` is banned in command files (GritQL rule). Use

- **API fields** keep their `@sentry/api` type and come from the generated `@sentry/api/valibot` schema (`vXxxResponse.entries`, wrapped in `pipe(field, description(…))` when it needs a description).
- **Fields the CLI adds** (target context such as `org`/`orgSlug`, flags such as `dryRun`/`deleted`, computed values such as `url`, `trace`, `period`) are written once as a valibot `object` of output fields. Spread its `entries` into the schema, and get the type with `InferOutput<typeof Fields>`, or `WithOutputFields<ApiType, typeof Fields>` (`src/types/output.ts`) when the fields sit on (or override) an API object.
- Type the command's data with the derived type: the `human` formatter's parameter, and `yield new CommandOutput<Output>(…)` where the yielded value is not already typed. A field added to the schema without the code that sets it, or removed while code still sets it, is then a compile error.
- Type the `human` formatter's parameter (and `jsonTransform`'s, when set) with the derived type. `buildCommand`, `buildListCommand`, and `buildDeleteCommand` infer the output type from `output` and check every `CommandOutput` that `func` yields against it. Annotate a yield (`new CommandOutput<Output>(…)`) only when it builds a discriminated-union literal (`kind: "endpoints"`) that would otherwise widen to `string`.
- Without `jsonTransform`, the data (each item, for array data) must also satisfy `output.schema`: a field the schema documents but the command doesn't produce, or documents with another type, is a compile error. With `jsonTransform`, the schema describes the transform's item, which the types can't see, so it isn't checked. Keep the data typed: an API response typed `Record<string, unknown>` can't satisfy a schema built from `@sentry/api/valibot`, so type it with its `@sentry/api` response type.
- Reuse the shared fields: `DryRunOutputField`, `orgListItemSchema`/`WithOrg` (`lib/org-list.ts`) for org-scoped lists, `opaque<T>()` for a field the docs leave opaque but the code types (a full event payload).
- Data only the human formatter needs (pre-rendered lines, a resolved target) stays out of the schema; keep it outside the derived type or strip it with `jsonExclude`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ Create a release
| `lastDeploy` | object \| null | Most recent deploy |
| `authors` | array | Commit authors |
| `owner` | object \| null | Release owner |
| `projects` | array | Projects in the release |
| `projects` | array | Projects in the release (only slug with --dry-run) |
| `adoptionStages` | object \| null | Adoption stage per project |
| `dryRun` | boolean | True when --dry-run made no change |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ List product trials

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | CLI trial name (e.g. seer), or "plan" for the plan trial |
| `category` | string | Trial category (e.g. seerUsers, seerAutofix) |
| `startDate` | string \| null | Start date (ISO 8601) |
| `endDate` | string \| null | End date (ISO 8601) |
| `reasonCode` | number | Reason code |
| `status` | string | Trial status |
| `daysRemaining` | number \| null | Days left on an active trial |
| `isStarted` | boolean | Whether the trial has started |
| `lengthDays` | number \| null | Trial duration in days |
| `startDate` | string \| null | Start date (ISO 8601) |
| `endDate` | string \| null | End date (ISO 8601) |

**Examples:**

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/agent-conversation/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ export const listCommand = buildListCommand("agent-conversation", {
const hasPrev = hasPreviousPage(PAGINATION_KEY, contextKey);
const hasMore = !!nextCursor;

yield new CommandOutput<ConversationListResult>({
yield new CommandOutput({
conversations,
hasMore,
hasPrev,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/agent-conversation/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,6 @@ export const viewCommand = buildCommand({
title,
});
result.truncated = truncated;
yield new CommandOutput<TranscriptResult>(result);
yield new CommandOutput(result);
},
});
4 changes: 1 addition & 3 deletions packages/cli/src/commands/alert/issues/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,7 @@ export const editCommand = buildCommand({
id: stringifyUnknown(updated.id ?? rule.id),
// The workflows endpoint returns `enabled` rather than `status`; map it
// back to a status label so human/JSON output matches the create path.
status: stringifyUnknown(
updated.status ?? (updated.enabled === false ? "disabled" : "active"),
),
status: updated.enabled === false ? "disabled" : "active",
} satisfies IssueAlertEditOutput);
},
});
4 changes: 2 additions & 2 deletions packages/cli/src/commands/alert/metrics/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import {
} from "../mutation-utils.js";
import type { AlertRuleArgument } from "../arguments.js";
import { metricRuleParameter, resolveMetricAlertRule } from "./rule-resolve.js";
import { metricAlertStatusLabel } from "./status.js";
import {
type MetricAlertEditOutput,
MetricAlertEditOutputSchema,
Expand Down Expand Up @@ -329,7 +328,8 @@ export const editCommand = buildCommand({
...updated,
org: orgSlug,
id: stringifyUnknown(updated.id ?? rule.id),
status: metricAlertStatusLabel(updated.status),
// The detectors endpoint returns `enabled` rather than `status`.
status: updated.enabled === false ? "disabled" : "active",
} satisfies MetricAlertEditOutput);
},
});
27 changes: 15 additions & 12 deletions packages/cli/src/commands/alert/output-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
* `project`, the `status` label, `dryRun`, `deleted`) are written here.
*/

import type {
UpdateOrganizationDetectorResponse,
UpdateOrganizationWorkflowResponse,
} from "@sentry/api";
import {
vCreateOrganizationProjectDetectorResponse,
vCreateOrganizationWorkflowResponse,
Expand All @@ -40,6 +44,7 @@ import {
unknown,
} from "valibot";
import { DryRunOutputField } from "../../types/index.js";
import type { WithOutputFields } from "../../types/output.js";

// Fields the CLI adds to every alert mutation's output.
const org = pipe(string(), description("Organization slug"));
Expand Down Expand Up @@ -193,12 +198,11 @@ export const IssueAlertEditOutputSchema = looseObject({
...IssueAlertEditFields.entries,
});

/**
* JSON output of `alert issues edit`. The API layer hands the updated
* workflow over untyped, so only the CLI's fields are typed.
*/
export type IssueAlertEditOutput = Record<string, unknown> &
InferOutput<typeof IssueAlertEditFields>;
/** JSON output of `alert issues edit`. */
export type IssueAlertEditOutput = WithOutputFields<
UpdateOrganizationWorkflowResponse,
typeof IssueAlertEditFields
>;

/** Fields `alert metrics edit` sets on the updated detector. */
const MetricAlertEditFields = object({ org, id: editedId, status });
Expand All @@ -210,12 +214,11 @@ export const MetricAlertEditOutputSchema = looseObject({
...MetricAlertEditFields.entries,
});

/**
* JSON output of `alert metrics edit`. The API layer hands the updated
* detector over untyped, so only the CLI's fields are typed.
*/
export type MetricAlertEditOutput = Record<string, unknown> &
InferOutput<typeof MetricAlertEditFields>;
/** JSON output of `alert metrics edit`. */
export type MetricAlertEditOutput = WithOutputFields<
UpdateOrganizationDetectorResponse,
typeof MetricAlertEditFields
>;

/** `delete` output: the deleted (or, with `--dry-run`, matched) rule. */
function deleteEntries<R extends RuleEntries>(rule: R) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/auth/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const listCommand = buildCommand({
const accounts = envAccount
? [...stored, await describeEnvAccount(envAccount)]
: stored;
yield new CommandOutput<AccountListResult>({ accounts });
yield new CommandOutput({ accounts });

if (getAuthConfig()?.source.startsWith("env:")) {
return {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/auth/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export const logoutCommand = buildCommand({
const accounts = listAccounts();
if (accounts.length === 0) {
assertNotEnvOnly();
return yield new CommandOutput<LogoutResult>({
return yield new CommandOutput({
loggedOut: false,
message: "Not currently authenticated.",
});
Expand Down Expand Up @@ -186,7 +186,7 @@ export const logoutCommand = buildCommand({
targets = active ? [active] : [];
}
if (targets.length === 0) {
return yield new CommandOutput<LogoutResult>({
return yield new CommandOutput({
loggedOut: false,
message:
"No account is active. Run 'sentry auth list' and pass an account to log out of, or 'all'.",
Expand Down Expand Up @@ -232,7 +232,7 @@ async function* finishLogout(
if (removed.some((a) => a.active) && activeAccount) {
pointDefaultUrlAtAccount(activeAccount.host);
}
yield new CommandOutput<LogoutResult>({
yield new CommandOutput({
loggedOut: removed.length > 0,
...(removed.length === 0 ? { message: "No stored account matched." } : {}),
configPath,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/auth/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const switchCommand = buildCommand({
const active = activeRow ? toAccountSummary(activeRow) : target;
pointDefaultUrlAtAccount(active.host);
const switched = !target.active;
yield new CommandOutput<AccountSwitchResult>({
yield new CommandOutput({
switched,
account: { ...active, active: true },
});
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/bash-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,10 +333,10 @@ export const bashHookCommand = buildCommand({
},
async *func(this: SentryContext, flags: BashHookFlags) {
if (flags["send-event"]) {
yield new CommandOutput<SendEventResult>(await handleSendEvent(flags));
yield new CommandOutput(await handleSendEvent(flags));
return;
}

yield new CommandOutput<BashHookResult>(handleScriptOutput(flags));
yield new CommandOutput(handleScriptOutput(flags));
},
});
2 changes: 1 addition & 1 deletion packages/cli/src/commands/build/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const downloadCommand = buildCommand({
const regionUrl = await resolveOrgRegion(org);
await downloadBuildArtifact(regionUrl, url, output);

yield new CommandOutput<BuildDownloadResult>({
yield new CommandOutput({
buildId,
org,
output,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/build/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ export const uploadCommand = buildCommand({
}

const uploadedCount = builds.filter((b) => b.error === null).length;
yield new CommandOutput<BuildUploadResult>({ builds, uploadedCount });
yield new CommandOutput({ builds, uploadedCount });

// Deliberate deviation from the legacy CLI, which only exits non-zero when
// *every* build fails: we fail loud on *any* failure so CI/agents never
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/cli/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,6 @@ export const completionCommand = buildCommand({
);
}

yield new CommandOutput<string>(script);
yield new CommandOutput(script);
},
});
2 changes: 1 addition & 1 deletion packages/cli/src/commands/cli/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ export const setupCommand = buildCommand({
printWelcomeMessage(emit, CLI_VERSION, binaryPath);
}

yield new CommandOutput<SetupResult>({
yield new CommandOutput({
messages,
warnings,
freshInstall,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/code-mappings/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ export const uploadCommand = buildCommand({
}));

// 7. Yield result
yield new CommandOutput<CodeMappingsUploadResult>({
yield new CommandOutput({
org,
project,
repository,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/dart-symbol-map/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export const uploadCommand = buildCommand({

// 3. --no-upload: validate only, no auth needed
if (flags["no-upload"]) {
yield new CommandOutput<DartSymbolMapUploadResult>({
yield new CommandOutput({
path: mappingPath,
debugId: flags["debug-id"],
uploaded: false,
Expand Down Expand Up @@ -276,7 +276,7 @@ export const uploadCommand = buildCommand({
});

// 6. Yield result
yield new CommandOutput<DartSymbolMapUploadResult>({
yield new CommandOutput({
org,
project,
path: mappingPath,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/dashboard/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export const createCommand = buildCommand({
);
const url = buildDashboardUrl(orgSlug, dashboard.id);

yield new CommandOutput<CreateResult>({ ...dashboard, url });
yield new CommandOutput({ ...dashboard, url });
return { hint: `Dashboard: ${url}` };
},
});
4 changes: 2 additions & 2 deletions packages/cli/src/commands/dashboard/widget/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const deleteCommand = buildDeleteCommand(

// Dry-run mode: show what would be removed without removing it
if (flags["dry-run"]) {
yield new CommandOutput<DashboardWidgetDeleteOutput>({
yield new CommandOutput({
dryRun: true,
widgetTitle,
widgetCount: widgets.length,
Expand All @@ -140,7 +140,7 @@ export const deleteCommand = buildDeleteCommand(
() => updateDashboard(orgSlug, dashboardId, updateBody),
);

yield new CommandOutput<DashboardWidgetDeleteOutput>({
yield new CommandOutput({
deleted: true,
widgetTitle,
widgetCount: updated.widgets?.length ?? 0,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/debug-files/bundle-jvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export const bundleJvmCommand = buildCommand({
}

// 5. Yield result
yield new CommandOutput<BundleJvmResult>({
yield new CommandOutput({
outputPath: result.outputPath,
debugId: flags["debug-id"],
fileCount: result.fileCount,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/debug-files/bundle-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const bundleSourcesCommand = buildCommand({
// this.process === the global process in production (see buildContext);
// using it here keeps the exit-code observable in tests.
this.process.exitCode = 1;
yield new CommandOutput<BundleSourcesResult>({
yield new CommandOutput({
path,
outputPath: null,
debugId: result.debugId,
Expand All @@ -174,7 +174,7 @@ export const bundleSourcesCommand = buildCommand({
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, result.bundle);

yield new CommandOutput<BundleSourcesResult>({
yield new CommandOutput({
path,
outputPath,
debugId: result.debugId,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/debug-files/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export const checkCommand = buildCommand({
}

const usable = isUsable(archive);
yield new CommandOutput<DebugFilesCheckResult>({
yield new CommandOutput({
path,
fileFormat: archive.fileFormat,
objects: archive.objects,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/debug-files/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export const findCommand = buildCommand({
},
async *func(this: SentryContext, flags: FindFlags, ...ids: string[]) {
if (ids.length === 0) {
yield new CommandOutput<FindResult>({ matches: [], missing: [] });
yield new CommandOutput({ matches: [], missing: [] });
return { hint: "Provide one or more debug identifiers to search for." };
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/debug-files/print-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export const printSourcesCommand = buildCommand({
);
}

yield new CommandOutput<PrintSourcesResult>({ path, objects });
yield new CommandOutput({ path, objects });

if (objects.length === 0) {
return { hint: `No objects found in '${path}'. Try: ${USAGE_HINT}` };
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/debug-files/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ function* doDryRun(
},
) {
const { difs, missingIds, requireAll, oversizedCount } = params;
yield new CommandOutput<DebugFilesUploadResult>({
yield new CommandOutput({
uploaded: false,
files: difs.map((d) => ({ name: d.name, debugId: d.debugId })),
filesUploaded: 0,
Expand Down Expand Up @@ -408,7 +408,7 @@ function* doNothingToUpload(
},
) {
const { missingIds, requireAll, oversizedCount, maxFileSize } = params;
yield new CommandOutput<DebugFilesUploadResult>({
yield new CommandOutput({
uploaded: false,
files: [],
filesUploaded: 0,
Expand Down Expand Up @@ -464,7 +464,7 @@ async function* doUpload(
(r) => r.state === "error" || r.state === "not_found",
);

yield new CommandOutput<DebugFilesUploadResult>({
yield new CommandOutput({
org: params.org,
project: params.project,
uploaded: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/docs/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ export const queryCommand = buildCommand({
},
async () => await queryDocs(query, detectedContext),
);
yield new CommandOutput<QueryOutput>({ ...result, detectedContext });
yield new CommandOutput({ ...result, detectedContext });
},
});
4 changes: 2 additions & 2 deletions packages/cli/src/commands/event/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ built entirely from the file contents.
dsnComponents,
);
await sendEnvelopeRequest(dsn, body);
yield new CommandOutput<SendEventResult>({ eventId, file });
yield new CommandOutput({ eventId, file });
}
} else {
if (flags.raw) {
Expand All @@ -397,7 +397,7 @@ built entirely from the file contents.
);
}
await sendEnvelopeRequest(dsn, body);
yield new CommandOutput<SendEventResult>({
yield new CommandOutput({
eventId: event.event_id ?? "",
});
}
Expand Down
Loading
Loading