create a bot
create-yaebal scaffolds a ready-to-run bot — a beautiful terminal ui when your
terminal supports it, plain prompts when it doesn't, and a one-shot flags mode for ci.
scaffold
run it with your package manager's create shortcut. it works the same under node,
bun and deno — the runtime that launches it is just the default, you can pick any target.
pnpm create yaebalthe interactive ui
in a real terminal you get a centred, keyboard-driven wizard rendered with pure ansi — no react,
no solid, no native dependencies, just node:readline and escape
codes. it walks seven steps: name → template → runtime → package manager → plugins → deploy →
review, with a text input, single-selects and a scrollable multi-select; everything is one
key away. picking the plugin template skips runtime, plugins and deploy entirely —
none of them apply to a plugin package.
| key | does |
|---|---|
↑ / ↓ | move within a list |
enter | confirm the step / create the project |
space | toggle a plugin (or a git/install/ci switch) |
a / n | select all / none on the plugins step |
← | go back a step |
esc | back, or cancel on the first step |
if the terminal can't host a ui — piped output, CI, a dumb terminal, or --no-tui — it transparently falls back to plain one-line prompts (and, failing that,
to defaults). nothing about the output changes; only how you answer does.
because it's pure ansi, the ui runs everywhere out of the box — node 20+, bun and deno, no flags, no prebuilt binaries. the only requirement is an interactive terminal. it also repaints cleanly on a terminal resize, and always restores your cursor/raw-mode/screen — even if the process is killed outright rather than exited through a keypress.
flags mode
pass any answer as a flag and the wizard skips that question; pass --yes and it skips
all of them. this is the path to use in scripts and ci. a flag given a missing or invalid value
(--runtime with no value, --runtime rust) is reported as a warning
instead of silently swallowing whatever comes after it on the command line.
# non-interactive — pass everything up front
pnpm create yaebal my-bot --runtime bun --template commands --plugins session,again,fmt
# webhook bot, deployed to cloudflare workers, with a ci workflow
pnpm create yaebal my-bot -t webhook -d cloudflare --ci --yes
# take every default, no questions (great for ci / scripts)
pnpm create yaebal my-bot --yes
# pull in the entire plugin catalog
pnpm create yaebal my-bot --plugins all --no-installname the project (folder) name
-r, --runtime <node|bun|deno> target runtime (default: detected)
-m, --pm <npm|pnpm|yarn|bun|deno> package manager (default: detected)
-t, --template <name> minimal · echo · commands · buttons ·
conversation · i18n · session-counter ·
webhook · runner · rich-message · broadcast ·
toml · plugin
-p, --plugins <a,b | all | none> comma list of @yaebal plugins
-d, --deploy <target> none · docker · compose · fly · railway ·
cloudflare · vercel
--ci / --no-ci add a github actions ci workflow
--git / --no-git initialise a git repo (+ first commit)
--install / --no-install install dependencies after scaffolding
--tui / --no-tui force the interactive ui on/off
-c, --config <path> read defaults from a config file
--no-config skip config-file autodetect
-y, --yes accept defaults, no prompts
--json print the result as json (with --yes)
-h, --help · -v, --versionconfig file
drop a create-yaebal.json next to where you run the command — or add a "create-yaebal" key to a local package.json — to pre-fill any answer.
cli flags always win; the file only fills in what a flag left unset.
{
"runtime": "bun",
"template": "commands",
"plugins": ["session", "again", "fmt"],
"deploy": "cloudflare",
"ci": true
}templates
a template is more than a bot body — it pulls in the plugins it needs, adds the real imports and
wiring, and (for webhook / runner) even swaps the bootstrap. everything a
template generates type-checks against the real @yaebal/* apis — enforced by a
compile smoke test that renders every template, every deploy target and the full plugin catalog
against the workspace packages.
| template | what you get |
|---|---|
minimal | just /start + a text echo |
echo | echo text, photos and stickers back |
commands | /start /help /ping via a typed registry + synced / menu (commands) |
buttons | inline keyboard + typed callback_data (keyboard, callback-data) |
conversation | await-style multi-step dialog (conversation) |
i18n | multi-language bot with a /lang toggle (i18n) |
session-counter | per-chat counter on ctx.session (session) |
webhook | edge/serverless deploy via serve() (web) |
runner | concurrent long-polling via run() (runner) |
rich-message | sendRichMessage block builder + a streaming draft demo (rich) |
broadcast | subscriber list + typed broadcast jobs (broadcast) |
toml | declarative routes in bot.toml + a handler registry (toml) |
plugin | reusable plugin package with src, tests and examples |
plugins listed in brackets are added and wired automatically — on top of anything you pick yourself.
deploy targets
orthogonal to the template — pick one with -d/--deploy or on the wizard's deploy step (not offered for --template plugin, which has nowhere to
deploy). docker/compose/fly/railway all run the
same long-polling bot you'd run locally, just containerized — a single-stage Dockerfile matching your runtime, since these templates run straight from .ts with no build step. cloudflare and vercel are serverless: they take over the bootstrap the same
way the webhook/runner templates already replace bot.start(),
adding @yaebal/web and a SECRET_TOKEN to .env.example.
| target | adds |
|---|---|
none | nothing — deploy however you like |
docker | a Dockerfile + .dockerignore |
compose | docker + compose.yaml for a local/vps run |
fly | docker + fly.toml |
railway | docker + railway.json |
cloudflare | wrangler.jsonc — export default { fetch: cloudflareAdapter(bot) }, no server to run |
vercel | vercel.json + api/bot.ts — an edge function via webhook(), alongside the normal polling src/index.ts |
--ci is independent of deploy: it adds a github actions workflow that installs and
typechecks on every push (plugin packages get test/build too).
plugins
every published, bot-installable @yaebal/* plugin is offered — enforced by a test
that diffs the catalog against the packages in the workspace. the ones with a clean default wiring
(session, again, throttle, ratelimiter, i18n, files, prompt, split, …) are dropped
straight into the bot chain; the rest are added to package.json with a commented
import so the starter always type-checks — you wire them in when you need them. analytics and feature-flags also leave a commented-out showcase of their
other real adapters (posthog/plausible/sqlite/clickhouse, launchdarkly/growthbook) right next to
the wired default.
what you get
my-bot/
package.json # scripts for your runtime, @yaebal deps
tsconfig.json # strict, esm, nodenext, noUncheckedIndexedAccess
src/index.ts # a working bot, wired with the plugins you picked
.env.example # BOT_TOKEN= (+ SECRET_TOKEN= for a serverless deploy)
.gitignore
README.md
# + whatever --deploy / --ci add: Dockerfile, wrangler.jsonc, api/bot.ts, .github/workflows/ci.yml…a minimal run, generated from --template minimal with session + again:
import { Bot } from "@yaebal/core";
import { session } from "@yaebal/session";
import { autoRetry } from "@yaebal/again";
const token = process.env.BOT_TOKEN;
if (!token) {
console.error("✗ set BOT_TOKEN in your environment (copy .env.example → .env)");
process.exit(1);
}
const bot = new Bot(token)
.install(session({ initial: () => ({ count: 0 }) }));
bot.command("start", (ctx) => ctx.reply("hello! i'm a yaebal bot 🤖"));
bot.on("message:text", (ctx) => ctx.reply(ctx.text));
// transformers applied to the outgoing api
autoRetry(bot.api);
await bot.start();then
cd my-bot
pnpm install
# add your BOT_TOKEN to .env
pnpm devswap pnpm for whatever you chose — the printed next-steps already use your package
manager. drop your BOT_TOKEN into .env and you're live. package names
can be scoped (@your-org/my-plugin) — the folder created is the unscoped part.