@@ -218,6 +218,143 @@ export async function runCli(cliArgs: string[]): Promise<void> {
218218 }
219219 } ;
220220
221+ /**
222+ * Attempt to import `.sentryclirc` settings when the user is unauthenticated.
223+ *
224+ * Returns `"imported"` if a trusted token was found, imported, and validated.
225+ * Returns `"declined"` if the user said no (marked as declined).
226+ * Returns `"skip"` if no eligible files, trust gate fails, or any error.
227+ */
228+ /**
229+ * Build a trusted import plan from non-project-local .sentryclirc files,
230+ * or return null if no eligible import is available.
231+ */
232+ async function buildEligibleImportPlan ( ) {
233+ const { discoverRcFiles, buildImportPlan, isImportNeededAsync } =
234+ await import ( "./lib/sentryclirc-import.js" ) ;
235+
236+ if ( ! ( await isImportNeededAsync ( ) ) ) {
237+ return null ;
238+ }
239+ const files = await discoverRcFiles ( process . cwd ( ) ) ;
240+ const eligible = files . filter ( ( f ) => f . location !== "project-local" ) ;
241+ if ( eligible . length === 0 || ! eligible . some ( ( f ) => f . token ) ) {
242+ return null ;
243+ }
244+ const plan = buildImportPlan ( eligible ) ;
245+ if (
246+ ! (
247+ plan . trusted &&
248+ plan . effective . token &&
249+ plan . newFields . includes ( "token" )
250+ )
251+ ) {
252+ return null ;
253+ }
254+ return plan ;
255+ }
256+
257+ async function tryRcImport ( ) : Promise < "imported" | "declined" | "skip" > {
258+ const plan = await buildEligibleImportPlan ( ) ;
259+ if ( ! plan ) {
260+ return "skip" ;
261+ }
262+
263+ const source = plan . sources . find ( ( s ) => s . token ) ?. path ?? "~/.sentryclirc" ;
264+ process . stderr . write (
265+ `\nFound auth token in ${ source } \n` +
266+ "Import settings to the new CLI? This stores your token with proper host scoping.\n\n"
267+ ) ;
268+
269+ const consent = await promptImportConsent ( ) ;
270+ if ( consent === "declined" ) {
271+ const { markImportDeclined } = await import (
272+ "./lib/sentryclirc-import.js"
273+ ) ;
274+ markImportDeclined ( ) ;
275+ return "declined" ;
276+ }
277+ if ( consent !== "accepted" ) {
278+ return "skip" ;
279+ }
280+
281+ const { executeImport } = await import ( "./lib/sentryclirc-import.js" ) ;
282+ const result = await executeImport ( plan , { validateToken : true } ) ;
283+ return result . imported && result . tokenValid !== false ? "imported" : "skip" ;
284+ }
285+
286+ /**
287+ * Prompt the user to accept/decline the import.
288+ * Returns "accepted", "declined" (explicit no), or "cancelled" (Ctrl+C).
289+ * Only "declined" permanently suppresses future prompts.
290+ */
291+ async function promptImportConsent ( ) : Promise <
292+ "accepted" | "declined" | "cancelled"
293+ > {
294+ const { logger : logModule } = await import ( "./lib/logger.js" ) ;
295+ const confirmed = await logModule
296+ . withTag ( "import" )
297+ . prompt ( "Import from .sentryclirc?" , { type : "confirm" , initial : true } ) ;
298+ if ( confirmed === true ) {
299+ return "accepted" ;
300+ }
301+ // false = explicit "no"; Symbol(clack:cancel) = Ctrl+C
302+ return confirmed === false ? "declined" : "cancelled" ;
303+ }
304+
305+ /** Log import middleware errors at an appropriate level */
306+ async function logImportError ( importErr : unknown ) : Promise < void > {
307+ const { logger : logModule } = await import ( "./lib/logger.js" ) ;
308+ const { HostScopeError : HSE } = await import ( "./lib/errors.js" ) ;
309+ const importLog = logModule . withTag ( "import" ) ;
310+ if ( importErr instanceof HSE ) {
311+ importLog . warn ( "Import middleware error" , importErr ) ;
312+ } else {
313+ importLog . debug ( "Import middleware error" , importErr ) ;
314+ }
315+ }
316+
317+ /**
318+ * `.sentryclirc` import middleware.
319+ *
320+ * When a command fails with `not_authenticated` and a non-project-local
321+ * `.sentryclirc` file has a token that passes the same-file trust gate,
322+ * offers to import it into the new CLI's SQLite store. On success, retries
323+ * the command. On decline, marks as declined (never asks again) and
324+ * re-throws so the auto-auth middleware can offer OAuth login instead.
325+ *
326+ * Only fires in interactive TTYs (disabled in CI). Project-local files
327+ * are excluded to avoid prompting in every cloned repo.
328+ */
329+ const rcImportMiddleware : ErrorMiddleware = async ( next , argv ) => {
330+ try {
331+ await next ( argv ) ;
332+ } catch ( err ) {
333+ let imported = false ;
334+ if (
335+ err instanceof AuthError &&
336+ err . reason === "not_authenticated" &&
337+ ! err . skipAutoAuth &&
338+ isatty ( 0 )
339+ ) {
340+ try {
341+ imported = ( await tryRcImport ( ) ) === "imported" ;
342+ } catch ( importErr ) {
343+ await logImportError ( importErr ) ;
344+ }
345+ }
346+ if ( imported ) {
347+ // Retry outside the import try/catch so retry errors propagate
348+ // naturally instead of being swallowed and re-throwing the
349+ // original AuthError.
350+ process . stderr . write ( "Import successful! Retrying command...\n\n" ) ;
351+ await next ( argv ) ;
352+ return ;
353+ }
354+ throw err ;
355+ }
356+ } ;
357+
221358 /**
222359 * Auto-authentication middleware.
223360 *
@@ -269,6 +406,7 @@ export async function runCli(cliArgs: string[]): Promise<void> {
269406 */
270407 const errorMiddlewares : ErrorMiddleware [ ] = [
271408 seerTrialMiddleware ,
409+ rcImportMiddleware ,
272410 autoAuthMiddleware ,
273411 ] ;
274412
0 commit comments