UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

338 lines (334 loc) 11.3 kB
// @bun import { open_default } from "./chunk-fr1k79kd.js"; import { identifyUser } from "./chunk-6zha718h.js"; import { select } from "./chunk-seyt2a5p.js"; import { adkLogo } from "./chunk-5agyx08n.js"; import { userInput } from "./chunk-tefbm840.js"; import { getLoginUrl, startCallbackServer } from "./chunk-4cgz94zj.js"; import { telemetry_default } from "./chunk-kwmsaz7n.js"; import { bind, box, fg, getActiveTheme, mount, onKey, router, signal, t, text, timeout } from "./chunk-m2h26j5f.js"; import { createCliLogger } from "./chunk-gzwt1qdr.js"; import { auth } from "./chunk-p0hjqn4r.js"; // src/components/loading-box.ts function loadingBox(ctx, props = {}, children = []) { const { width = 80, borderColor = getActiveTheme().ui.border, borderStyle = "rounded", paddingX = 1, paddingY, marginBottom, minHeight } = props; return box(ctx, { border: true, borderStyle, borderColor, width, paddingX, paddingY, marginBottom, minHeight }, children); } // src/commands/adk-login.ts import { spawn } from "child_process"; var flushTelemetry = () => Promise.race([telemetry_default.shutdown(), new Promise((resolve) => setTimeout(resolve, 2000))]).catch(() => {}); function loginView(renderer, scope, deps) { const { options, onSuccess, onCancel } = deps; const theme = getActiveTheme(); const view = signal(options.profile ? "select-method" : "loading-profiles"); const loginUrl = signal(undefined); let existingProfiles = []; let chosenProfile = options.profile; let errorMsg = ""; let callbackCancel = null; const eff = () => ({ ...options, profile: chosenProfile }); if (!options.profile) { (async () => { const profiles = await auth.listProfiles(); if (profiles.length === 0) { chosenProfile = "default"; view.set("select-method"); } else { existingProfiles = profiles; view.set("select-profile"); } })(); } const handleProfileSelected = (profileName) => { chosenProfile = profileName; view.set("select-method"); }; const handleBrowserLogin = async () => { errorMsg = ""; view.set("browser-waiting"); try { const { url: callbackUrl, waitForToken, cancel } = await startCallbackServer({ timeout: 300000 }); callbackCancel = cancel; const url = getLoginUrl(callbackUrl, eff().apiUrl); loginUrl.set(url); if (process.platform === "win32") { const cp = spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }); cp.unref(); } else { await open_default(url); } const result = await waitForToken(); callbackCancel = null; if (!result.success || !result.token) { if (view() !== "browser-waiting") return; errorMsg = "Login timed out. Please try again."; view.set("select-method"); return; } view.set("authenticating"); await auth.login(result.token, { profile: chosenProfile, apiUrl: eff().apiUrl }); await identifyUser().catch(() => {}); view.set("success"); } catch (error) { callbackCancel = null; errorMsg = error instanceof Error ? error.message : "Browser login failed"; view.set("select-method"); } }; const handleTokenSubmit = async (token) => { view.set("authenticating"); errorMsg = ""; try { await auth.login(token, { profile: chosenProfile, apiUrl: eff().apiUrl }); await identifyUser().catch(() => {}); view.set("success"); } catch (error) { errorMsg = error instanceof Error ? error.message : "Login failed"; view.set("manual-entry"); } }; const logo = () => adkLogo(renderer, { subtitle: "Login", logoColor: theme.accent.purple, titleColor: theme.text.primary, subtitleColor: theme.text.dim }); const body = router(renderer, scope, view, (v, viewScope) => { if (v === "loading-profiles") { return box(renderer, { flexDirection: "column" }, [ loadingBox(renderer, {}, [text(renderer, t`${fg(theme.text.dim)("Loading profiles\u2026")}`)]) ]); } if (v === "select-profile") { const profileOptions = [ ...existingProfiles.map((p) => ({ id: p.name, label: p.name, value: p.name, description: p.apiUrl.replace("https://", "") })), { id: "__new__", label: "Create new profile", value: "__new__", description: "Save login under a new profile name" } ]; return box(renderer, { flexDirection: "column" }, [ logo(), text(renderer, t`${fg(theme.text.dim)("Which profile should these credentials be saved to?")}`), select(renderer, viewScope, { options: profileOptions, onSubmit: (value) => { if (value === "__new__") view.set("new-profile"); else handleProfileSelected(value); }, onCancel }) ]); } if (v === "new-profile") { return box(renderer, { flexDirection: "column" }, [ logo(), text(renderer, t`${fg(theme.text.dim)("Enter a name for the new profile")}`), box(renderer, { marginTop: 1 }, [ userInput(renderer, viewScope, { prompt: "Profile name:", placeholder: "e.g. staging, production", validate: (value) => value.trim() ? null : "Profile name is required", onSubmit: (value) => handleProfileSelected(value.trim()), onCancel: () => view.set("select-profile") }) ]) ]); } if (v === "browser-waiting") { const urlLine = text(renderer, ""); viewScope.add(bind(() => { const url = loginUrl(); urlLine.content = url ? t`${fg(theme.text.dim)("If the browser didn't open, visit:")}\n${fg(theme.text.link)(url)}` : t``; }, [loginUrl])); viewScope.add(onKey(renderer, (key) => { if (key.name === "escape") { callbackCancel?.(); callbackCancel = null; view.set("select-method"); } })); return box(renderer, { flexDirection: "column" }, [ logo(), text(renderer, t`${fg(theme.text.dim)("Waiting for browser authentication...")}`), box(renderer, { marginTop: 1 }, [ loadingBox(renderer, {}, [text(renderer, t`${fg(theme.text.dim)("Opening browser\u2026")}`)]) ]), box(renderer, { marginTop: 1, flexDirection: "column" }, [urlLine]), box(renderer, { marginTop: 1 }, [ text(renderer, t`${fg(theme.text.dim)("Press Esc to cancel \u2022 Ctrl-C to quit")}`) ]) ]); } if (v === "manual-entry") { const children2 = [ logo(), text(renderer, t`${fg(theme.text.dim)("Enter your Botpress Personal Access Token")}`), box(renderer, { marginTop: 1, flexDirection: "column" }, [ userInput(renderer, viewScope, { prompt: "Enter your Botpress API token:", placeholder: "bp_xxx...", type: "password", validate: (value) => value.trim() ? null : "Token is required", onSubmit: (value) => handleTokenSubmit(value), onCancel: () => view.set("select-method") }) ]) ]; if (errorMsg) children2.push(text(renderer, t`${fg(theme.status.error)(errorMsg)}`)); return box(renderer, { flexDirection: "column" }, children2); } if (v === "authenticating") { return box(renderer, { flexDirection: "column" }, [ loadingBox(renderer, {}, [text(renderer, t`${fg(theme.text.dim)("Authenticating\u2026")}`)]) ]); } if (v === "success") { const userLine = text(renderer, t`${fg(theme.text.primary)("Logged in as Loading...")}`); auth.getCurrentProfileDetails().then((details) => { const userDisplay = details?.displayName ? `${details.displayName} (${details.email})` : details?.email || "Unknown user"; userLine.content = t`${fg(theme.text.primary)(`Logged in as ${userDisplay}`)}`; }); timeout(viewScope, 1500, onSuccess); return box(renderer, { flexDirection: "column" }, [ text(renderer, t`${fg(theme.status.success)(`${theme.symbols.checkmark} Successfully logged in to Botpress`)}`), userLine, text(renderer, t`${fg(theme.text.dim)(`Profile: ${eff().profile || "default"}`)}`), text(renderer, t`${fg(theme.text.dim)(`API URL: ${eff().apiUrl || "https://api.botpress.cloud"}`)}`) ]); } const children = [ logo(), text(renderer, t`${fg(theme.text.dim)("Log in to Botpress")}`), select(renderer, viewScope, { options: [ { id: "browser", label: "Continue with Browser", value: "browser" }, { id: "pat", label: "Continue with Personal Access Token (PAT)", value: "pat" } ], onSubmit: (value) => { if (value === "browser") handleBrowserLogin(); else view.set("manual-entry"); }, onCancel: () => { if (existingProfiles.length > 0) view.set("select-profile"); else onCancel(); } }) ]; if (errorMsg) { children.push(box(renderer, { marginTop: 1 }, [text(renderer, t`${fg(theme.status.error)(errorMsg)}`)])); } return box(renderer, { flexDirection: "column" }, children); }); return body; } var adkLogin = async (options = {}) => { const logger = createCliLogger(); const token = options.token || process.env.BOTPRESS_TOKEN; const nonInteractiveProfile = options.profile || "default"; if (token && (!process.stdout.isTTY || options.token)) { try { await auth.login(token, { ...options, profile: nonInteractiveProfile }); await identifyUser().catch(() => {}); logger.info("\u2713 Successfully logged in to Botpress", "green"); const profileDetails = await auth.getCurrentProfileDetails(); const userDisplay = profileDetails?.displayName ? `${profileDetails.displayName} (${profileDetails.email})` : profileDetails?.email || "Unknown user"; logger.info(`Logged in as ${userDisplay}`); logger.info(`Profile: ${nonInteractiveProfile}`, "gray"); logger.info(`API URL: ${options.apiUrl || "https://api.botpress.cloud"}`, "gray"); return; } catch (error) { logger.fatal(error); } } return new Promise((resolve, reject) => { let appRef = null; const onSuccess = () => { appRef?.unmount(); if (options.exitAfterLogin === false) { resolve(); } else { flushTelemetry().finally(() => process.exit(0)); } }; const onCancel = () => { appRef?.unmount(); logger.info("Login cancelled.", "yellow"); flushTelemetry().finally(() => { if (options.exitAfterLogin === false) reject(new Error("Login cancelled")); else process.exit(0); }); }; mount((renderer, scope) => loginView(renderer, scope, { options, onSuccess, onCancel }), { exitOnCtrlC: true }).then((app) => { appRef = app; }, reject); }); }; export { adkLogin };