import { log } from "@clack/prompts" import { blue } from "ansis" import { setTimeout } from "node:timers/promises" import open from "open" import { pick } from "remeda" import { joinURL, withQuery } from "ufo" import { z } from "zod" import { authClient } from "../lib/auth-client" import { defineCommand } from "../lib/command" import { env } from "../lib/env" import { globalOptions, globalOptionsSchema } from "../lib/global-options" import { accent } from "../lib/io" import { clearSavedTokenSync, saveNewLoginTokenSync } from "../lib/session" import { clearSpinner, startSpinner } from "../lib/spinner" import { output, sessionIntro } from "../utils" const DEVICE_CLIENT_ID = "automate-cli" export const loginCommand = defineCommand({ name: "login", description: "Authenticate with Automate.ax", options: { ...globalOptions, email: { type: "string", short: "e", }, }, optionDescriptions: { email: "Prefill the browser login email" }, schema: globalOptionsSchema.extend({ email: z.email().optional(), positionals: z.tuple([]), }), run: async ({ email }) => { const session = await authClient.getSession() output.normal(() => sessionIntro(session)) startSpinner("Preparing login") const { device_code, user_code } = await authClient.device.code({ client_id: DEVICE_CLIENT_ID, }) const authPageUrl = withQuery(joinURL(env.APP_ORIGIN, "/login"), { user_code, email: email?.toLowerCase(), }) clearSpinner() output.normal(() => log.message(blue.underline.link(authPageUrl, "Open in browser"), { symbol: blue`↗`, }), ) await open(authPageUrl) startSpinner("Waiting for authorization") let interval = 2000 while (true) { await setTimeout(interval) try { const { access_token } = await authClient.device.token({ device_code, client_id: DEVICE_CLIENT_ID, grant_type: "urn:ietf:params:oauth:grant-type:device_code", fetchOptions: { headers: { "user-agent": `Automate.ax CLI`, }, }, }) saveNewLoginTokenSync(access_token) break } catch (error) { switch ( z.object({ error: z.object({ error: z.string() }) }).safeParse(error) .data?.error.error ) { case "authorization_pending": continue case "slow_down": interval *= 1.5 continue case "access_denied": throw new Error("Authorization was denied by the user", { cause: error, }) case "expired_token": throw new Error("Device code expired", { cause: error, }) default: throw error } } } clearSpinner() const authenticatedSession = await authClient.getSession() if (!authenticatedSession) { clearSavedTokenSync() throw new Error("Failed to login") } output .normal(() => log.success(`Logged in as ${accent(authenticatedSession.user.email)}`), ) .json({ loggedIn: true, success: true, user: pick(authenticatedSession.user, [ "id", "name", "email", "emailVerified", "image", "createdAt", "updatedAt", ]), }) }, })