create-yaebal 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +78 -13
  2. package/lib/args.d.ts +27 -0
  3. package/lib/args.d.ts.map +1 -0
  4. package/lib/args.js +122 -0
  5. package/lib/args.js.map +1 -0
  6. package/lib/args.test.d.ts +2 -0
  7. package/lib/args.test.d.ts.map +1 -0
  8. package/lib/args.test.js +38 -0
  9. package/lib/args.test.js.map +1 -0
  10. package/lib/catalog.d.ts +56 -0
  11. package/lib/catalog.d.ts.map +1 -0
  12. package/lib/catalog.js +241 -0
  13. package/lib/catalog.js.map +1 -0
  14. package/lib/config.d.ts +21 -0
  15. package/lib/config.d.ts.map +1 -0
  16. package/lib/config.js +35 -0
  17. package/lib/config.js.map +1 -0
  18. package/lib/index.d.ts +14 -17
  19. package/lib/index.d.ts.map +1 -1
  20. package/lib/index.js +112 -144
  21. package/lib/index.js.map +1 -1
  22. package/lib/prompts.d.ts +10 -0
  23. package/lib/prompts.d.ts.map +1 -0
  24. package/lib/prompts.js +95 -0
  25. package/lib/prompts.js.map +1 -0
  26. package/lib/scaffold.d.ts +29 -0
  27. package/lib/scaffold.d.ts.map +1 -0
  28. package/lib/scaffold.js +296 -0
  29. package/lib/scaffold.js.map +1 -0
  30. package/lib/scaffold.test.d.ts +2 -0
  31. package/lib/scaffold.test.d.ts.map +1 -0
  32. package/lib/scaffold.test.js +96 -0
  33. package/lib/scaffold.test.js.map +1 -0
  34. package/lib/tui/ansi.d.ts +39 -0
  35. package/lib/tui/ansi.d.ts.map +1 -0
  36. package/lib/tui/ansi.js +84 -0
  37. package/lib/tui/ansi.js.map +1 -0
  38. package/lib/tui/app.d.ts +20 -0
  39. package/lib/tui/app.d.ts.map +1 -0
  40. package/lib/tui/app.js +381 -0
  41. package/lib/tui/app.js.map +1 -0
  42. package/lib/tui/app.test.d.ts +2 -0
  43. package/lib/tui/app.test.d.ts.map +1 -0
  44. package/lib/tui/app.test.js +87 -0
  45. package/lib/tui/app.test.js.map +1 -0
  46. package/lib/tui/theme.d.ts +23 -0
  47. package/lib/tui/theme.d.ts.map +1 -0
  48. package/lib/tui/theme.js +23 -0
  49. package/lib/tui/theme.js.map +1 -0
  50. package/lib/util.d.ts +27 -0
  51. package/lib/util.d.ts.map +1 -0
  52. package/lib/util.js +95 -0
  53. package/lib/util.js.map +1 -0
  54. package/package.json +1 -1
  55. package/lib/index.test.d.ts +0 -2
  56. package/lib/index.test.d.ts.map +0 -1
  57. package/lib/index.test.js +0 -29
  58. package/lib/index.test.js.map +0 -1
@@ -0,0 +1,296 @@
1
+ /**
2
+ * project generation. `renderFiles` is pure — options in, a `path → contents`
3
+ * map out — so it is trivially testable. `writeProject` is the thin fs wrapper
4
+ * that flushes that map to disk.
5
+ *
6
+ * a template is more than a bot body: it can pull in plugins, add real imports,
7
+ * declare consts before the bot, extend the `.install()` chain and even replace
8
+ * the bootstrap (`bot.start()` → `serve()` / `run()`). everything it generates
9
+ * type-checks against the real `@yaebal/*` apis.
10
+ */
11
+ import { mkdir, writeFile } from "node:fs/promises";
12
+ import { dirname, join } from "node:path";
13
+ import { findPlugin } from "./catalog.js";
14
+ const SCRIPTS = {
15
+ node: {
16
+ dev: "node --watch --experimental-strip-types src/index.ts",
17
+ start: "node --experimental-strip-types src/index.ts",
18
+ },
19
+ bun: { dev: "bun --watch src/index.ts", start: "bun src/index.ts" },
20
+ deno: {
21
+ dev: "deno run --watch --allow-net --allow-env src/index.ts",
22
+ start: "deno run --allow-net --allow-env src/index.ts",
23
+ },
24
+ };
25
+ const DEFAULT_BOOTSTRAP = `await bot.start();
26
+ console.log("✓ bot is running — press ctrl-c to stop");`;
27
+ const TEMPLATES = {
28
+ minimal: {
29
+ body: `bot.command("start", (ctx) => ctx.reply("hello! i'm a yaebal bot 🤖"));
30
+ bot.on("message:text", (ctx) => ctx.reply(ctx.text));`,
31
+ },
32
+ echo: {
33
+ body: `bot.command("start", (ctx) => ctx.reply("send me anything and i'll echo it back ✨"));
34
+ bot.on("message:text", (ctx) => ctx.reply(ctx.text));
35
+ bot.on("message:photo", (ctx) => ctx.reply("nice photo! 📸"));
36
+ bot.on("message:sticker", (ctx) => ctx.reply("cool sticker 🎨"));`,
37
+ },
38
+ commands: {
39
+ body: `bot.command("start", (ctx) => ctx.reply("hello! i'm a yaebal bot 🤖"));
40
+ bot.command("help", (ctx) => ctx.reply("commands: /start /help /ping"));
41
+ bot.command("ping", (ctx) => ctx.reply("pong 🏓"));
42
+ bot.on("message:text", (ctx) => ctx.reply(\`you said: \${ctx.text}\`));`,
43
+ },
44
+ buttons: {
45
+ plugins: ["keyboard", "callback-data"],
46
+ imports: [
47
+ 'import { InlineKeyboard } from "@yaebal/keyboard";',
48
+ 'import { callbackData } from "@yaebal/callback-data";',
49
+ ],
50
+ pre: 'const vote = callbackData("vote", { choice: String });',
51
+ body: `bot.command("start", (ctx) =>
52
+ \tctx.reply("tap a button 👇", {
53
+ \t\treply_markup: new InlineKeyboard()
54
+ \t\t\t.text("👍 like", vote.pack({ choice: "up" }))
55
+ \t\t\t.text("👎 dislike", vote.pack({ choice: "down" }))
56
+ \t\t\t.build(),
57
+ \t}),
58
+ );
59
+
60
+ bot.callbackQuery(vote.pattern, async (ctx) => {
61
+ \tconst choice = vote.unpack(ctx.callbackQuery.data ?? "")?.choice;
62
+ \tawait ctx.answerCallbackQuery({ text: choice === "up" ? "liked 👍" : "disliked 👎" });
63
+ });`,
64
+ },
65
+ conversation: {
66
+ plugins: ["conversation"],
67
+ imports: ['import { conversation, createConversation } from "@yaebal/conversation";'],
68
+ pre: `const greet = createConversation("greet", async (cv, ctx) => {
69
+ \tawait ctx.reply("what's your name?");
70
+ \tconst name = await cv.wait();
71
+ \tawait name.reply(\`hi, \${name.text}! how old are you?\`);
72
+ \tconst age = await cv.wait();
73
+ \tawait age.reply(\`\${age.text} — nice to meet you ✨\`);
74
+ });`,
75
+ install: ["conversation([greet])"],
76
+ body: `bot.command("start", (ctx) => ctx.reply("type /greet to start a short chat"));
77
+ bot.command("greet", (ctx) => ctx.conversation.enter("greet"));`,
78
+ },
79
+ i18n: {
80
+ plugins: ["i18n"],
81
+ imports: ['import { i18n } from "@yaebal/i18n";'],
82
+ pre: `const locales = {
83
+ \ten: { hi: "hi there! 🇬🇧", switched: "language set to english" },
84
+ \tru: { hi: "привет! 🇷🇺", switched: "язык переключён на русский" },
85
+ };`,
86
+ install: ['i18n({ defaultLocale: "en", locales })'],
87
+ body: `bot.command("start", (ctx) => ctx.reply(ctx.t("hi")));
88
+ bot.command("lang", async (ctx) => {
89
+ \tawait ctx.changeLanguage(ctx.locale === "ru" ? "en" : "ru");
90
+ \treturn ctx.reply(ctx.t("switched"));
91
+ });`,
92
+ },
93
+ "session-counter": {
94
+ plugins: ["session"],
95
+ imports: ['import { session } from "@yaebal/session";'],
96
+ install: ["session({ initial: () => ({ count: 0 }) })"],
97
+ body: `bot.command("start", (ctx) => ctx.reply("send /count to bump your per-chat counter"));
98
+ bot.command("count", (ctx) => {
99
+ \tctx.session.count++;
100
+ \treturn ctx.reply(\`you've hit /count \${ctx.session.count} time(s)\`);
101
+ });`,
102
+ },
103
+ webhook: {
104
+ plugins: ["web"],
105
+ imports: ['import { serve } from "@yaebal/web";'],
106
+ body: `bot.command("start", (ctx) => ctx.reply("hello from a webhook bot 🤖"));
107
+ bot.on("message:text", (ctx) => ctx.reply(ctx.text));`,
108
+ bootstrap: `const port = Number(process.env.PORT ?? 8080);
109
+
110
+ // point telegram at your public https url once (run this separately or here):
111
+ // await bot.api.call("setWebhook", { url: "https://your.domain/" });
112
+
113
+ serve(bot, { port });
114
+ console.log(\`✓ webhook server listening on :\${port}\`);`,
115
+ },
116
+ runner: {
117
+ plugins: ["runner"],
118
+ imports: ['import { run } from "@yaebal/runner";'],
119
+ body: `bot.command("start", (ctx) => ctx.reply("hello! your updates run concurrently ⚡"));
120
+ bot.on("message:text", (ctx) => ctx.reply(ctx.text));`,
121
+ bootstrap: `const handle = run(bot, { concurrency: 50 });
122
+ process.once("SIGINT", () => handle.stop());
123
+ console.log("✓ runner polling (concurrency 50) — press ctrl-c to stop");`,
124
+ },
125
+ };
126
+ /** default package manager that matches a runtime when the user didn't pick one */
127
+ export function defaultPackageManager(runtime) {
128
+ if (runtime === "bun")
129
+ return "bun";
130
+ if (runtime === "deno")
131
+ return "deno";
132
+ return "pnpm";
133
+ }
134
+ const pkgOf = (importLine) => importLine.match(/from "([^"]+)"/)?.[1];
135
+ /** turn options into a `relative path → file content` map. pure. */
136
+ export function renderFiles(opts) {
137
+ const templateId = opts.template ?? "minimal";
138
+ const spec = TEMPLATES[templateId];
139
+ const pm = opts.packageManager ?? defaultPackageManager(opts.runtime);
140
+ const chosen = opts.plugins.map(findPlugin).filter((p) => !!p);
141
+ const templatePlugins = (spec.plugins ?? [])
142
+ .map(findPlugin)
143
+ .filter((p) => !!p);
144
+ const templateIds = new Set(templatePlugins.map((p) => p.id));
145
+ // the template owns the wiring for its own plugins, so the user-selected
146
+ // ones are partitioned with those removed (no double installs).
147
+ const wired = chosen.filter((p) => !templateIds.has(p.id));
148
+ const installs = wired.filter((p) => p.wire === "install");
149
+ const setups = wired.filter((p) => p.wire === "setup");
150
+ const depOnly = wired.filter((p) => p.wire === "dep");
151
+ // ── imports ──────────────────────────────────────────────────────────
152
+ const imports = [
153
+ 'import { Bot } from "@yaebal/core";',
154
+ ...installs.map((p) => p.import),
155
+ ...setups.map((p) => p.import),
156
+ ...(spec.imports ?? []),
157
+ ];
158
+ const importBlock = [...new Set(imports)].join("\n");
159
+ // dep-only plugins get a commented import — unless the template already
160
+ // imports that package for real.
161
+ const importedPackages = new Set([
162
+ ...installs.map((p) => p.dep),
163
+ ...setups.map((p) => p.dep),
164
+ ...(spec.imports ?? []).map(pkgOf).filter((p) => !!p),
165
+ ]);
166
+ const depHints = depOnly.filter((p) => !importedPackages.has(p.dep));
167
+ const depHint = depHints.length > 0
168
+ ? `\n\n// extra plugins you added — import & wire them as you need:\n${depHints
169
+ .map((p) => `// ${p.import}`)
170
+ .join("\n")}`
171
+ : "";
172
+ // ── bot construction ───────────────────────────────────────────────────
173
+ const installExprs = [...installs.map((p) => p.install), ...(spec.install ?? [])];
174
+ const installChain = installExprs.length > 0 ? `\n${installExprs.map((e) => `\t.install(${e})`).join("\n")}` : "";
175
+ const setupStatements = [...setups.map((p) => p.setup)];
176
+ if (spec.setup)
177
+ setupStatements.push(spec.setup);
178
+ const setupBlock = setupStatements.length > 0
179
+ ? `\n\n// transformers applied to the outgoing api\n${setupStatements.join("\n")}`
180
+ : "";
181
+ const pre = spec.pre ? `${spec.pre}\n\n` : "";
182
+ const bootstrap = spec.bootstrap ?? DEFAULT_BOOTSTRAP;
183
+ const index = `${importBlock}${depHint}
184
+
185
+ const token = process.env.BOT_TOKEN;
186
+ if (!token) {
187
+ \tconsole.error("✗ set BOT_TOKEN in your environment (copy .env.example → .env)");
188
+ \tprocess.exit(1);
189
+ }
190
+
191
+ ${pre}const bot = new Bot(token)${installChain};
192
+
193
+ ${spec.body}${setupBlock}
194
+
195
+ ${bootstrap}
196
+ `;
197
+ // ── package.json ──────────────────────────────────────────────────────
198
+ const dependencies = { "@yaebal/core": "latest" };
199
+ for (const p of [...chosen, ...templatePlugins])
200
+ dependencies[p.dep] = "latest";
201
+ const pkg = {
202
+ name: opts.name,
203
+ version: "0.0.0",
204
+ private: true,
205
+ type: "module",
206
+ scripts: SCRIPTS[opts.runtime],
207
+ dependencies,
208
+ devDependencies: { "@types/node": "^22.0.0", typescript: "^5.7.0" },
209
+ engines: opts.runtime === "node" ? { node: ">=22.6" } : undefined,
210
+ };
211
+ const tsconfig = {
212
+ compilerOptions: {
213
+ target: "ES2022",
214
+ module: "NodeNext",
215
+ moduleResolution: "NodeNext",
216
+ lib: ["ES2023"],
217
+ strict: true,
218
+ noUncheckedIndexedAccess: true,
219
+ verbatimModuleSyntax: true,
220
+ skipLibCheck: true,
221
+ allowImportingTsExtensions: true,
222
+ noEmit: true,
223
+ types: ["node"],
224
+ },
225
+ include: ["src"],
226
+ };
227
+ const runCmd = runCommand(pm, "dev");
228
+ const installCmd = installCommand(pm);
229
+ const allPlugins = [...new Set([...chosen.map((p) => p.id), ...templateIds])];
230
+ const readme = `# ${opts.name}
231
+
232
+ a telegram bot built with [yaebal](https://github.com/neverlane/yaebal) — type-safe, runtime-agnostic.
233
+
234
+ ## quick start
235
+
236
+ \`\`\`sh
237
+ # 1. install deps
238
+ ${installCmd}
239
+
240
+ # 2. add your token
241
+ cp .env.example .env # then put your BOT_TOKEN in it
242
+
243
+ # 3. run (watch mode)
244
+ ${runCmd}
245
+ \`\`\`
246
+
247
+ runtime: **${opts.runtime}** · template: **${templateId}**${allPlugins.length ? ` · plugins: ${allPlugins.join(", ")}` : ""}
248
+ `;
249
+ return {
250
+ "package.json": `${JSON.stringify(pkg, null, "\t")}\n`,
251
+ "tsconfig.json": `${JSON.stringify(tsconfig, null, "\t")}\n`,
252
+ "src/index.ts": index,
253
+ ".env.example": "BOT_TOKEN=\n",
254
+ ".gitignore": "node_modules\nlib\n.env\n*.log\n",
255
+ "README.md": readme,
256
+ };
257
+ }
258
+ /** the install command for a package manager (deno has none) */
259
+ export function installCommand(pm) {
260
+ switch (pm) {
261
+ case "npm":
262
+ return "npm install";
263
+ case "yarn":
264
+ return "yarn";
265
+ case "bun":
266
+ return "bun install";
267
+ case "deno":
268
+ return "deno cache src/index.ts";
269
+ default:
270
+ return "pnpm install";
271
+ }
272
+ }
273
+ /** the `<pm> run <script>` invocation (npm/yarn/bun) or runtime command (deno) */
274
+ export function runCommand(pm, script) {
275
+ switch (pm) {
276
+ case "npm":
277
+ return `npm run ${script}`;
278
+ case "yarn":
279
+ return `yarn ${script}`;
280
+ case "bun":
281
+ return `bun run ${script}`;
282
+ case "deno":
283
+ return script === "dev" ? SCRIPTS.deno.dev : SCRIPTS.deno.start;
284
+ default:
285
+ return `pnpm ${script}`;
286
+ }
287
+ }
288
+ /** flush a rendered file map to `targetDir`, creating parent dirs as needed. */
289
+ export async function writeProject(targetDir, files) {
290
+ for (const [rel, content] of Object.entries(files)) {
291
+ const full = join(targetDir, rel);
292
+ await mkdir(dirname(full), { recursive: true });
293
+ await writeFile(full, content);
294
+ }
295
+ }
296
+ //# sourceMappingURL=scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scaffold.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.test.d.ts","sourceRoot":"","sources":["../src/scaffold.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,96 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { PLUGIN_IDS } from "./catalog.js";
4
+ import { defaultPackageManager, installCommand, renderFiles, runCommand } from "./scaffold.js";
5
+ test("renderFiles: bare project has core only", () => {
6
+ const f = renderFiles({ name: "bot", runtime: "node", plugins: [] });
7
+ const pkg = JSON.parse(f["package.json"] ?? "{}");
8
+ assert.equal(pkg.name, "bot");
9
+ assert.deepEqual(Object.keys(pkg.dependencies), ["@yaebal/core"]);
10
+ assert.match(f["src/index.ts"] ?? "", /new Bot\(token\)/);
11
+ assert.doesNotMatch(f["src/index.ts"] ?? "", /\.install\(/);
12
+ });
13
+ test("renderFiles: install/setup plugins are wired, dep plugins commented", () => {
14
+ const f = renderFiles({
15
+ name: "bot",
16
+ runtime: "node",
17
+ plugins: ["session", "again", "fmt", "bogus"],
18
+ });
19
+ const pkg = JSON.parse(f["package.json"] ?? "{}");
20
+ assert.ok(pkg.dependencies["@yaebal/session"]);
21
+ assert.ok(pkg.dependencies["@yaebal/again"]);
22
+ assert.ok(pkg.dependencies["@yaebal/fmt"]);
23
+ assert.equal(pkg.dependencies.bogus, undefined); // unknown plugin ignored
24
+ const src = f["src/index.ts"] ?? "";
25
+ assert.match(src, /import \{ session \} from "@yaebal\/session";/);
26
+ assert.match(src, /\.install\(session\(/);
27
+ assert.match(src, /autoRetry\(bot\.api\);/); // `again` is a setup-style plugin
28
+ assert.match(src, /\/\/ import \{ html, md \} from "@yaebal\/fmt";/); // fmt is dep-only
29
+ });
30
+ test("renderFiles: every catalog plugin generates valid, importable code", () => {
31
+ const f = renderFiles({ name: "bot", runtime: "node", plugins: [...PLUGIN_IDS] });
32
+ const src = f["src/index.ts"] ?? "";
33
+ const pkg = JSON.parse(f["package.json"] ?? "{}");
34
+ // all plugin deps present
35
+ for (const id of PLUGIN_IDS) {
36
+ assert.ok(Object.values(pkg.dependencies).length > 0);
37
+ assert.ok(src.includes(id) || JSON.stringify(pkg.dependencies).includes(id), `missing ${id}`);
38
+ }
39
+ // no duplicate import lines for @yaebal/core
40
+ assert.equal((src.match(/from "@yaebal\/core"/g) ?? []).length, 1);
41
+ });
42
+ test("renderFiles: template changes the bot body", () => {
43
+ const cmd = renderFiles({ name: "b", runtime: "node", plugins: [], template: "commands" })["src/index.ts"] ?? "";
44
+ assert.match(cmd, /bot\.command\("ping"/);
45
+ const echo = renderFiles({ name: "b", runtime: "node", plugins: [], template: "echo" })["src/index.ts"] ??
46
+ "";
47
+ assert.match(echo, /message:photo/);
48
+ });
49
+ test("renderFiles: rich templates pull in plugins, imports and wiring", () => {
50
+ const buttons = renderFiles({ name: "b", runtime: "node", plugins: [], template: "buttons" });
51
+ const bsrc = buttons["src/index.ts"] ?? "";
52
+ const bpkg = JSON.parse(buttons["package.json"] ?? "{}");
53
+ assert.ok(bpkg.dependencies["@yaebal/keyboard"]);
54
+ assert.ok(bpkg.dependencies["@yaebal/callback-data"]);
55
+ assert.match(bsrc, /import \{ InlineKeyboard \} from "@yaebal\/keyboard";/);
56
+ assert.match(bsrc, /bot\.callbackQuery\(vote\.pattern/);
57
+ // webhook & runner replace the default bootstrap
58
+ const wh = renderFiles({ name: "b", runtime: "bun", plugins: [], template: "webhook" })["src/index.ts"] ??
59
+ "";
60
+ assert.match(wh, /serve\(bot, \{ port \}\)/);
61
+ assert.doesNotMatch(wh, /bot\.start\(\)/);
62
+ const rn = renderFiles({ name: "b", runtime: "node", plugins: [], template: "runner" })["src/index.ts"] ??
63
+ "";
64
+ assert.match(rn, /run\(bot, \{ concurrency: 50 \}\)/);
65
+ assert.doesNotMatch(rn, /bot\.start\(\)/);
66
+ // install-style templates add to the chain via a pre-defined const
67
+ const conv = renderFiles({ name: "b", runtime: "node", plugins: [], template: "conversation" })["src/index.ts"] ?? "";
68
+ assert.match(conv, /const greet = createConversation\(/);
69
+ assert.match(conv, /\.install\(conversation\(\[greet\]\)\)/);
70
+ });
71
+ test("renderFiles: a template plugin is never wired twice", () => {
72
+ // user explicitly picks session AND the session-counter template
73
+ const f = renderFiles({
74
+ name: "b",
75
+ runtime: "node",
76
+ plugins: ["session"],
77
+ template: "session-counter",
78
+ });
79
+ const src = f["src/index.ts"] ?? "";
80
+ assert.equal((src.match(/\.install\(session\(/g) ?? []).length, 1);
81
+ assert.equal((src.match(/from "@yaebal\/session"/g) ?? []).length, 1);
82
+ });
83
+ test("renderFiles: runtime picks the right scripts", () => {
84
+ const bun = JSON.parse(renderFiles({ name: "b", runtime: "bun", plugins: [] })["package.json"] ?? "{}");
85
+ assert.match(bun.scripts.start, /^bun /);
86
+ const deno = JSON.parse(renderFiles({ name: "d", runtime: "deno", plugins: [] })["package.json"] ?? "{}");
87
+ assert.match(deno.scripts.start, /^deno run /);
88
+ });
89
+ test("commands map to package managers", () => {
90
+ assert.equal(installCommand("pnpm"), "pnpm install");
91
+ assert.equal(installCommand("deno"), "deno cache src/index.ts");
92
+ assert.equal(runCommand("npm", "dev"), "npm run dev");
93
+ assert.equal(defaultPackageManager("bun"), "bun");
94
+ assert.equal(defaultPackageManager("node"), "pnpm");
95
+ });
96
+ //# sourceMappingURL=scaffold.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.test.js","sourceRoot":"","sources":["../src/scaffold.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * tiny ansi toolkit for the wizard — truecolor styling, a rounded card, screen
3
+ * control and a keypress reader built on `node:readline`. no native deps, so it
4
+ * runs on node, bun and deno out of the box.
5
+ */
6
+ export declare const screen: {
7
+ enter: string;
8
+ leave: string;
9
+ clear: string;
10
+ hideCursor: string;
11
+ showCursor: string;
12
+ };
13
+ export declare function moveTo(row: number, col: number): string;
14
+ export interface StyleOpts {
15
+ fg?: string;
16
+ bg?: string;
17
+ bold?: boolean;
18
+ dim?: boolean;
19
+ }
20
+ /** build a styling function that wraps text in a single sgr sequence + reset. */
21
+ export declare function style(opts: StyleOpts): (s: string) => string;
22
+ export declare const fg: (hex: string) => (s: string) => string;
23
+ export declare const stripAnsi: (s: string) => string;
24
+ /** visible width (ansi-insensitive). our content is single-width throughout. */
25
+ export declare const vlen: (s: string) => number;
26
+ /** pad a (possibly styled) string to `width` visible columns. */
27
+ export declare function padTo(s: string, width: number): string;
28
+ export interface KeyEvent {
29
+ name: string;
30
+ ctrl: boolean;
31
+ shift: boolean;
32
+ sequence: string;
33
+ }
34
+ /**
35
+ * subscribe to keypresses on a stream. returns a cleanup fn that detaches the
36
+ * listener and restores the terminal's raw-mode state.
37
+ */
38
+ export declare function onKeypress(input: NodeJS.ReadStream, handler: (key: KeyEvent) => void): () => void;
39
+ //# sourceMappingURL=ansi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi.d.ts","sourceRoot":"","sources":["../../src/tui/ansi.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,eAAO,MAAM,MAAM;;;;;;CAMlB,CAAC;AAEF,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAWD,MAAM,WAAW,SAAS;IACzB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,OAAO,CAAC;CACd;AAED,iFAAiF;AACjF,wBAAgB,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAe5D;AAED,eAAO,MAAM,EAAE,GAAI,KAAK,MAAM,SAjBc,MAAM,KAAK,MAiBF,CAAC;AAItD,eAAO,MAAM,SAAS,GAAI,GAAG,MAAM,KAAG,MAAgC,CAAC;AACvE,gFAAgF;AAChF,eAAO,MAAM,IAAI,GAAI,GAAG,MAAM,KAAG,MAA6B,CAAC;AAE/D,iEAAiE;AACjE,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED,MAAM,WAAW,QAAQ;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI,CAsBjG"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * tiny ansi toolkit for the wizard — truecolor styling, a rounded card, screen
3
+ * control and a keypress reader built on `node:readline`. no native deps, so it
4
+ * runs on node, bun and deno out of the box.
5
+ */
6
+ import readline from "node:readline";
7
+ const ESC = "\x1b[";
8
+ export const screen = {
9
+ enter: `${ESC}?1049h`, // alternate buffer — leaves the scrollback untouched
10
+ leave: `${ESC}?1049l`,
11
+ clear: `${ESC}2J${ESC}H`,
12
+ hideCursor: `${ESC}?25l`,
13
+ showCursor: `${ESC}?25h`,
14
+ };
15
+ export function moveTo(row, col) {
16
+ return `${ESC}${row};${col}H`;
17
+ }
18
+ function rgb(hex) {
19
+ const h = hex.replace("#", "");
20
+ return [
21
+ Number.parseInt(h.slice(0, 2), 16),
22
+ Number.parseInt(h.slice(2, 4), 16),
23
+ Number.parseInt(h.slice(4, 6), 16),
24
+ ];
25
+ }
26
+ /** build a styling function that wraps text in a single sgr sequence + reset. */
27
+ export function style(opts) {
28
+ const codes = [];
29
+ if (opts.fg) {
30
+ const [r, g, b] = rgb(opts.fg);
31
+ codes.push(`38;2;${r};${g};${b}`);
32
+ }
33
+ if (opts.bg) {
34
+ const [r, g, b] = rgb(opts.bg);
35
+ codes.push(`48;2;${r};${g};${b}`);
36
+ }
37
+ if (opts.bold)
38
+ codes.push("1");
39
+ if (opts.dim)
40
+ codes.push("2");
41
+ if (codes.length === 0)
42
+ return (s) => s;
43
+ const prefix = `${ESC}${codes.join(";")}m`;
44
+ return (s) => `${prefix}${s}${ESC}0m`;
45
+ }
46
+ export const fg = (hex) => style({ fg: hex });
47
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matching the esc (\x1b) byte is the point
48
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
49
+ export const stripAnsi = (s) => s.replace(ANSI_RE, "");
50
+ /** visible width (ansi-insensitive). our content is single-width throughout. */
51
+ export const vlen = (s) => stripAnsi(s).length;
52
+ /** pad a (possibly styled) string to `width` visible columns. */
53
+ export function padTo(s, width) {
54
+ const gap = width - vlen(s);
55
+ return gap > 0 ? s + " ".repeat(gap) : s;
56
+ }
57
+ /**
58
+ * subscribe to keypresses on a stream. returns a cleanup fn that detaches the
59
+ * listener and restores the terminal's raw-mode state.
60
+ */
61
+ export function onKeypress(input, handler) {
62
+ readline.emitKeypressEvents(input);
63
+ const isTty = Boolean(input.isTTY);
64
+ if (isTty && input.setRawMode)
65
+ input.setRawMode(true);
66
+ input.resume?.();
67
+ const listener = (str, key) => {
68
+ handler({
69
+ name: key?.name ?? (str === " " ? "space" : (str ?? "")),
70
+ ctrl: Boolean(key?.ctrl),
71
+ shift: Boolean(key?.shift),
72
+ sequence: str ?? key?.sequence ?? "",
73
+ });
74
+ };
75
+ input.on("keypress", listener);
76
+ return () => {
77
+ input.off("keypress", listener);
78
+ if (isTty && input.setRawMode)
79
+ input.setRawMode(false);
80
+ // release stdin so the event loop can drain and the process exits.
81
+ input.pause?.();
82
+ };
83
+ }
84
+ //# sourceMappingURL=ansi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi.js","sourceRoot":"","sources":["../../src/tui/ansi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * the wizard — a centred, keyboard-driven card rendered with pure ansi (see
3
+ * `./ansi.ts`). no native deps: it runs on node, bun and deno wherever there's
4
+ * a tty. a tiny step state-machine drives a full repaint on every keystroke,
5
+ * which at this size is instant and flicker-free on the alternate screen.
6
+ *
7
+ * the renderer is injectable (input/output/size) so tests can drive it with a
8
+ * fake stdin and read frames back from a fake stdout.
9
+ */
10
+ import type { ParsedArgs } from "../args.js";
11
+ import { type Selections } from "../config.js";
12
+ export interface TuiIO {
13
+ input?: NodeJS.ReadStream;
14
+ output?: NodeJS.WriteStream;
15
+ rows?: number;
16
+ cols?: number;
17
+ }
18
+ /** run the wizard. resolves with selections, or `undefined` if cancelled. */
19
+ export declare function runTui(args: ParsedArgs, io?: TuiIO): Promise<Selections | undefined>;
20
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/tui/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAY,KAAK,UAAU,EAAmB,MAAM,cAAc,CAAC;AA8B1E,MAAM,WAAW,KAAK;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAOD,6EAA6E;AAC7E,wBAAsB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CA0V9F"}