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
62 changes: 16 additions & 46 deletions .lore.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ cli/
│ ├── context.ts # Dependency injection context
│ ├── commands/ # CLI commands
│ │ ├── auth/ # login, logout, refresh, status, token, whoami
│ │ ├── cli/ # defaults, feedback, fix, setup, upgrade
│ │ ├── cli/ # defaults, feedback, fix, import, setup, upgrade
│ │ ├── dashboard/ # list, view, create, add, edit, delete, revisions, restore
│ │ ├── event/ # view, list
│ │ ├── issue/ # list, events, explain, plan, view, resolve, unresolve, archive, merge
Expand Down
1 change: 1 addition & 0 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ CLI-related commands
- `sentry cli defaults <key value...>` — View and manage default settings
- `sentry cli feedback <message...>` — Send feedback about the CLI
- `sentry cli fix` — Diagnose and repair CLI database issues
- `sentry cli import` — Import settings from legacy .sentryclirc files
- `sentry cli setup` — Configure shell integration
- `sentry cli upgrade <version>` — Update the Sentry CLI to the latest version

Expand Down
10 changes: 10 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ Diagnose and repair CLI database issues
sentry cli fix
```

### `sentry cli import`

Import settings from legacy .sentryclirc files

**Flags:**
- `-y, --yes - Skip confirmation prompt`
- `-n, --dry-run - Show what would happen without making changes`
- `--url <value> - Explicitly trust this URL (bypasses same-file trust check)`
- `--skip-validation - Skip token validation against the Sentry API`

### `sentry cli setup`

Configure shell integration
Expand Down
138 changes: 138 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,143 @@ export async function runCli(cliArgs: string[]): Promise<void> {
}
};

/**
* Attempt to import `.sentryclirc` settings when the user is unauthenticated.
*
* Returns `"imported"` if a trusted token was found, imported, and validated.
* Returns `"declined"` if the user said no (marked as declined).
* Returns `"skip"` if no eligible files, trust gate fails, or any error.
*/
/**
* Build a trusted import plan from non-project-local .sentryclirc files,
* or return null if no eligible import is available.
*/
async function buildEligibleImportPlan() {
const { discoverRcFiles, buildImportPlan, isImportNeededAsync } =
await import("./lib/sentryclirc-import.js");

if (!(await isImportNeededAsync())) {
return null;
}
const files = await discoverRcFiles(process.cwd());
const eligible = files.filter((f) => f.location !== "project-local");
if (eligible.length === 0 || !eligible.some((f) => f.token)) {
return null;
}
const plan = buildImportPlan(eligible);
Comment thread
BYK marked this conversation as resolved.
if (
!(
plan.trusted &&
plan.effective.token &&
plan.newFields.includes("token")
)
) {
return null;
}
return plan;
}

async function tryRcImport(): Promise<"imported" | "declined" | "skip"> {
const plan = await buildEligibleImportPlan();
if (!plan) {
return "skip";
}

const source = plan.sources.find((s) => s.token)?.path ?? "~/.sentryclirc";
process.stderr.write(
`\nFound auth token in ${source}\n` +
"Import settings to the new CLI? This stores your token with proper host scoping.\n\n"
);

const consent = await promptImportConsent();
if (consent === "declined") {
const { markImportDeclined } = await import(
"./lib/sentryclirc-import.js"
);
markImportDeclined(plan.sources);
return "declined";
}
if (consent !== "accepted") {
return "skip";
}

const { executeImport } = await import("./lib/sentryclirc-import.js");
const result = await executeImport(plan, { validateToken: true });
return result.imported && result.tokenValid !== false ? "imported" : "skip";
}

/**
* Prompt the user to accept/decline the import.
* Returns "accepted", "declined" (explicit no), or "cancelled" (Ctrl+C).
* Only "declined" permanently suppresses future prompts.
*/
async function promptImportConsent(): Promise<
"accepted" | "declined" | "cancelled"
> {
const { logger: logModule } = await import("./lib/logger.js");
const confirmed = await logModule
.withTag("import")
.prompt("Import from .sentryclirc?", { type: "confirm", initial: true });
if (confirmed === true) {
return "accepted";
}
// false = explicit "no"; Symbol(clack:cancel) = Ctrl+C
return confirmed === false ? "declined" : "cancelled";
}

/** Log import middleware errors at an appropriate level */
async function logImportError(importErr: unknown): Promise<void> {
const { logger: logModule } = await import("./lib/logger.js");
const { HostScopeError: HSE } = await import("./lib/errors.js");
const importLog = logModule.withTag("import");
if (importErr instanceof HSE) {
importLog.warn("Import middleware error", importErr);
} else {
importLog.debug("Import middleware error", importErr);
}
}

/**
* `.sentryclirc` import middleware.
*
* When a command fails with `not_authenticated` and a non-project-local
* `.sentryclirc` file has a token that passes the same-file trust gate,
* offers to import it into the new CLI's SQLite store. On success, retries
* the command. On decline, marks as declined (never asks again) and
* re-throws so the auto-auth middleware can offer OAuth login instead.
*
* Only fires in interactive TTYs (disabled in CI). Project-local files
* are excluded to avoid prompting in every cloned repo.
*/
const rcImportMiddleware: ErrorMiddleware = async (next, argv) => {
try {
await next(argv);
} catch (err) {
let imported = false;
if (
err instanceof AuthError &&
err.reason === "not_authenticated" &&
!err.skipAutoAuth &&
isatty(0)
) {
try {
imported = (await tryRcImport()) === "imported";
} catch (importErr) {
await logImportError(importErr);
}
Comment thread
BYK marked this conversation as resolved.
}
if (imported) {
// Retry outside the import try/catch so retry errors propagate
// naturally instead of being swallowed and re-throwing the
// original AuthError.
process.stderr.write("Import successful! Retrying command...\n\n");
await next(argv);
return;
}
throw err;
}
};

/**
* Auto-authentication middleware.
*
Expand Down Expand Up @@ -269,6 +406,7 @@ export async function runCli(cliArgs: string[]): Promise<void> {
*/
const errorMiddlewares: ErrorMiddleware[] = [
seerTrialMiddleware,
rcImportMiddleware,
Comment thread
sentry-warden[bot] marked this conversation as resolved.
autoAuthMiddleware,
];

Expand Down
4 changes: 3 additions & 1 deletion src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ export function rcTokenHint(
: ` --url ${effectiveHost}`;
return (
`Found a token in .sentryclirc (${rcConfig.sources.token}). ` +
`To skip OAuth next time: sentry auth login --token <token>${urlHint}`
"To import it: sentry cli import | " +
`To pass it directly: sentry auth login --token <token>${urlHint}`
);
}

Expand Down Expand Up @@ -374,6 +375,7 @@ export const loginCommand = buildCommand({

refuseLoginToUntrustedHost(flags, effectiveHost, urlFromRc);

// Check if already authenticated and handle re-authentication
if (isAuthenticated()) {
const shouldProceed = await handleExistingAuth(flags.force);
if (!shouldProceed) {
Expand Down
Loading
Loading