@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
338 lines (334 loc) • 11.3 kB
JavaScript
// @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 };