kavoru 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.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # kavoru (CLI)
2
+
3
+ Scaffold a new [Kavoru](https://github.com/mertthesamael/Kavoru) backend — ElysiaJS, Bun, TypeScript, Prisma, and the full production starter stack.
4
+
5
+ ## Usage
6
+
7
+ After publishing to npm:
8
+
9
+ ```bash
10
+ bunx kavoru my-api
11
+ cd my-api
12
+ bun run dev
13
+ ```
14
+
15
+ Equivalent to `bunx --bun kavoru` (Bun runs the `kavoru` binary from the npm package).
16
+
17
+ ### Options
18
+
19
+ | Flag | Description |
20
+ | --- | --- |
21
+ | `-h, --help` | Show help |
22
+ | `-V, --version` | Show CLI version |
23
+ | `-f, --force` | Scaffold into a non-empty directory |
24
+ | `--no-install` | Skip `bun install` |
25
+ | `--repo owner/name` | Override template repo (default: `mertthesamael/Kavoru`) |
26
+ | `--branch name` | Template branch (default: `master`) |
27
+
28
+ ### Examples
29
+
30
+ ```bash
31
+ # Interactive (prompts for project name)
32
+ bunx kavoru
33
+
34
+ # Current directory
35
+ bunx kavoru .
36
+
37
+ # Custom template fork (local dev)
38
+ bunx kavoru demo --repo your-user/Kavoru --no-install
39
+ ```
40
+
41
+ ## Development
42
+
43
+ ```bash
44
+ cd elysia-template-initializer
45
+ bun install
46
+ bun test
47
+
48
+ # Run locally without publishing
49
+ bun run src/index.ts my-test-app
50
+ # or
51
+ bun link
52
+ bunx kavoru my-test-app
53
+ ```
54
+
55
+ ## Publish to npm
56
+
57
+ 1. Ensure the [Kavoru](https://github.com/mertthesamael/Kavoru) template repo is public on `master`.
58
+ 2. Create a **Granular Access Token** at [npm → Access Tokens](https://www.npmjs.com/settings/~/tokens) with:
59
+ - **Bypass two-factor authentication** — checked (required for first publish without 2FA)
60
+ - **Packages** — All packages, **Read and write**
61
+ 3. Configure auth (do not commit the token):
62
+
63
+ ```bash
64
+ echo "//registry.npmjs.org/:_authToken=YOUR_TOKEN" > ~/.npmrc
65
+ unset NPM_CONFIG_TOKEN
66
+ npm whoami
67
+ ```
68
+
69
+ 4. Publish: `npm publish` (or `bun publish`)
70
+
71
+ The `bin/kavoru.js` shim uses `#!/usr/bin/env bun` so `bunx kavoru` runs with Bun.
72
+
73
+ ## What the CLI does
74
+
75
+ 1. Shallow-clones the GitHub template (or downloads a zip if `git` is missing)
76
+ 2. Removes `.git` so the new project starts fresh
77
+ 3. Sets `package.json` `name`, copies `.env` from `.env.example`, and adjusts default service IDs
78
+ 4. Runs `bun install` (unless `--no-install`)
79
+
80
+ ## License
81
+
82
+ MIT
package/bin/kavoru.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import "../src/index.ts";
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "kavoru",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new Kavoru (Elysia + Bun) backend from the official template",
5
+ "type": "module",
6
+ "bin": {
7
+ "kavoru": "bin/kavoru.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "scripts": {
14
+ "dev": "bun run src/index.ts",
15
+ "test": "bun test"
16
+ },
17
+ "keywords": [
18
+ "elysia",
19
+ "elysiajs",
20
+ "bun",
21
+ "scaffold",
22
+ "template",
23
+ "kavoru"
24
+ ],
25
+ "author": "https://github.com/mertthesamael",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/mertthesamael/kavoru-cli.git"
30
+ },
31
+ "homepage": "https://github.com/mertthesamael/Kavoru",
32
+ "bugs": {
33
+ "url": "https://github.com/mertthesamael/Kavoru/issues"
34
+ },
35
+ "engines": {
36
+ "bun": ">=1.1.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "devDependencies": {
42
+ "@types/bun": "latest"
43
+ }
44
+ }
package/src/args.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { PACKAGE_VERSION } from "./constants";
2
+
3
+ export type CliOptions = {
4
+ targetDir: string | undefined;
5
+ help: boolean;
6
+ version: boolean;
7
+ install: boolean;
8
+ force: boolean;
9
+ repo: string;
10
+ branch: string;
11
+ };
12
+
13
+ const HELP = `\
14
+ Usage: kavoru [options] [directory]
15
+
16
+ Create a new project from the Kavoru Elysia + Bun template.
17
+
18
+ Arguments:
19
+ directory Project folder (use "." for current directory)
20
+
21
+ Options:
22
+ -h, --help Show help
23
+ -V, --version Show version
24
+ -f, --force Overwrite / use a non-empty target directory
25
+ --no-install Skip "bun install" after scaffolding
26
+ --repo <owner/name> GitHub template repo (default: mertthesamael/Kavoru)
27
+ --branch <name> Template branch (default: master)
28
+
29
+ Examples:
30
+ bunx kavoru my-api
31
+ bunx kavoru .
32
+ `;
33
+
34
+ export function parseArgs(argv: string[]): CliOptions {
35
+ const options: CliOptions = {
36
+ targetDir: undefined,
37
+ help: false,
38
+ version: false,
39
+ install: true,
40
+ force: false,
41
+ repo: "mertthesamael/Kavoru",
42
+ branch: "master",
43
+ };
44
+
45
+ const positional: string[] = [];
46
+
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const arg = argv[i];
49
+ if (!arg) continue;
50
+
51
+ switch (arg) {
52
+ case "-h":
53
+ case "--help":
54
+ options.help = true;
55
+ break;
56
+ case "-V":
57
+ case "--version":
58
+ options.version = true;
59
+ break;
60
+ case "-f":
61
+ case "--force":
62
+ options.force = true;
63
+ break;
64
+ case "--no-install":
65
+ options.install = false;
66
+ break;
67
+ case "--repo": {
68
+ const value = argv[++i];
69
+ if (!value) throw new Error("--repo requires a value (owner/name).");
70
+ options.repo = value;
71
+ break;
72
+ }
73
+ case "--branch": {
74
+ const value = argv[++i];
75
+ if (!value) throw new Error("--branch requires a value.");
76
+ options.branch = value;
77
+ break;
78
+ }
79
+ default:
80
+ if (arg.startsWith("-")) {
81
+ throw new Error(`Unknown option: ${arg}`);
82
+ }
83
+ positional.push(arg);
84
+ }
85
+ }
86
+
87
+ if (positional[0]) {
88
+ options.targetDir = positional[0];
89
+ }
90
+
91
+ return options;
92
+ }
93
+
94
+ export function printHelp(): void {
95
+ console.log(HELP.trim());
96
+ }
97
+
98
+ export function printVersion(): void {
99
+ console.log(PACKAGE_VERSION);
100
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { existsSync } from "node:fs";
2
+ import { cp, mkdir, readdir, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import * as readline from "node:readline/promises";
6
+ import { stdin as input, stdout as output } from "node:process";
7
+ import type { CliOptions } from "./args";
8
+ import { log } from "./log";
9
+ import {
10
+ customizeProject,
11
+ fetchTemplate,
12
+ installDependencies,
13
+ removeGitMetadata,
14
+ resolveTemplateSource,
15
+ } from "./template";
16
+ import {
17
+ assertValidPackageName,
18
+ isDirectoryEmpty,
19
+ toPackageName,
20
+ } from "./validate";
21
+
22
+ async function promptProjectName(): Promise<string> {
23
+ const rl = readline.createInterface({ input, output });
24
+ try {
25
+ const answer = await rl.question("Project name: ");
26
+ const name = toPackageName(answer);
27
+ if (!name) {
28
+ throw new Error("Project name cannot be empty.");
29
+ }
30
+ return name;
31
+ } finally {
32
+ rl.close();
33
+ }
34
+ }
35
+
36
+ async function copyTemplateIntoTarget(
37
+ tempDir: string,
38
+ targetDir: string,
39
+ ): Promise<void> {
40
+ await mkdir(targetDir, { recursive: true });
41
+ const entries = await readdir(tempDir, { withFileTypes: true });
42
+
43
+ for (const entry of entries) {
44
+ await cp(
45
+ path.join(tempDir, entry.name),
46
+ path.join(targetDir, entry.name),
47
+ { recursive: true, force: true },
48
+ );
49
+ }
50
+ }
51
+
52
+ export async function runCli(options: CliOptions): Promise<void> {
53
+ let targetArg = options.targetDir;
54
+
55
+ if (!targetArg) {
56
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
57
+ if (!interactive) {
58
+ throw new Error("Missing project directory. Usage: bunx kavoru <directory>");
59
+ }
60
+ targetArg = await promptProjectName();
61
+ }
62
+
63
+ const isCurrentDir = targetArg === ".";
64
+ const packageName = isCurrentDir
65
+ ? toPackageName(path.basename(process.cwd()))
66
+ : toPackageName(path.basename(targetArg));
67
+
68
+ assertValidPackageName(packageName);
69
+
70
+ const targetDir = isCurrentDir
71
+ ? process.cwd()
72
+ : path.resolve(process.cwd(), targetArg);
73
+
74
+ if (existsSync(targetDir) && !options.force && !isDirectoryEmpty(targetDir)) {
75
+ throw new Error(
76
+ `Target directory "${targetDir}" is not empty. Use --force to scaffold anyway.`,
77
+ );
78
+ }
79
+
80
+ const source = resolveTemplateSource(options.repo, options.branch);
81
+ const tempDir = path.join(os.tmpdir(), `kavoru-${Date.now()}`);
82
+
83
+ log.info(`Creating Kavoru project "${packageName}"`);
84
+
85
+ try {
86
+ await fetchTemplate(source, tempDir);
87
+ await removeGitMetadata(tempDir);
88
+ await customizeProject(tempDir, packageName);
89
+ await copyTemplateIntoTarget(tempDir, targetDir);
90
+
91
+ if (options.install) {
92
+ await installDependencies(targetDir);
93
+ }
94
+ } finally {
95
+ await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
96
+ }
97
+
98
+ log.success(`Project ready at ${targetDir}`);
99
+ console.log();
100
+ console.log("Next steps:");
101
+ if (!isCurrentDir) {
102
+ console.log(` cd ${targetArg}`);
103
+ }
104
+ if (!options.install) {
105
+ console.log(" bun install");
106
+ }
107
+ console.log(" bun run dev");
108
+ console.log();
109
+ console.log(" API: http://localhost:3131");
110
+ console.log(" OpenAPI: http://localhost:3131/help");
111
+ }
@@ -0,0 +1,8 @@
1
+ export const PACKAGE_VERSION = "0.1.0";
2
+
3
+ export const TEMPLATE_REPO = "mertthesamael/Kavoru";
4
+ export const TEMPLATE_BRANCH = "master";
5
+
6
+ export const TEMPLATE_GIT_URL = `https://github.com/${TEMPLATE_REPO}.git`;
7
+
8
+ export const TEMPLATE_ZIP_URL = `https://github.com/${TEMPLATE_REPO}/archive/refs/heads/${TEMPLATE_BRANCH}.zip`;
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { parseArgs, printHelp, printVersion } from "./args";
4
+ import { runCli } from "./cli";
5
+ import { log } from "./log";
6
+
7
+ async function main(): Promise<void> {
8
+ try {
9
+ const options = parseArgs(process.argv.slice(2));
10
+
11
+ if (options.help) {
12
+ printHelp();
13
+ return;
14
+ }
15
+
16
+ if (options.version) {
17
+ printVersion();
18
+ return;
19
+ }
20
+
21
+ await runCli(options);
22
+ } catch (error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ log.error(message);
25
+ process.exit(1);
26
+ }
27
+ }
28
+
29
+ await main();
package/src/log.ts ADDED
@@ -0,0 +1,24 @@
1
+ const reset = "\x1b[0m";
2
+ const dim = "\x1b[2m";
3
+ const cyan = "\x1b[36m";
4
+ const green = "\x1b[32m";
5
+ const yellow = "\x1b[33m";
6
+ const red = "\x1b[31m";
7
+
8
+ export const log = {
9
+ info(message: string) {
10
+ console.log(`${cyan}◆${reset} ${message}`);
11
+ },
12
+ step(message: string) {
13
+ console.log(`${dim}…${reset} ${message}`);
14
+ },
15
+ success(message: string) {
16
+ console.log(`${green}✔${reset} ${message}`);
17
+ },
18
+ warn(message: string) {
19
+ console.warn(`${yellow}!${reset} ${message}`);
20
+ },
21
+ error(message: string) {
22
+ console.error(`${red}✖${reset} ${message}`);
23
+ },
24
+ };
@@ -0,0 +1,172 @@
1
+ import { cp, mkdir, rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { TEMPLATE_BRANCH } from "./constants";
4
+ import { log } from "./log";
5
+
6
+ export type TemplateSource = {
7
+ repo: string;
8
+ branch: string;
9
+ };
10
+
11
+ function gitUrl(repo: string): string {
12
+ return `https://github.com/${repo}.git`;
13
+ }
14
+
15
+ function zipUrl(repo: string, branch: string): string {
16
+ return `https://github.com/${repo}/archive/refs/heads/${branch}.zip`;
17
+ }
18
+
19
+ async function commandExists(command: string): Promise<boolean> {
20
+ const which = process.platform === "win32" ? "where" : "which";
21
+ const proc = Bun.spawn([which, command], { stdout: "pipe", stderr: "ignore" });
22
+ const code = await proc.exited;
23
+ return code === 0;
24
+ }
25
+
26
+ async function runCommand(cmd: string[], cwd?: string): Promise<void> {
27
+ const proc = Bun.spawn(cmd, {
28
+ cwd,
29
+ stdout: "inherit",
30
+ stderr: "inherit",
31
+ });
32
+ const code = await proc.exited;
33
+ if (code !== 0) {
34
+ throw new Error(`Command failed (${code}): ${cmd.join(" ")}`);
35
+ }
36
+ }
37
+
38
+ async function cloneWithGit(
39
+ source: TemplateSource,
40
+ targetDir: string,
41
+ ): Promise<void> {
42
+ await runCommand([
43
+ "git",
44
+ "clone",
45
+ "--depth",
46
+ "1",
47
+ "--branch",
48
+ source.branch,
49
+ gitUrl(source.repo),
50
+ targetDir,
51
+ ]);
52
+ }
53
+
54
+ async function downloadZip(
55
+ source: TemplateSource,
56
+ targetDir: string,
57
+ ): Promise<void> {
58
+ const url = zipUrl(source.repo, source.branch);
59
+ const repoName = source.repo.split("/")[1] ?? "template";
60
+ const extractedFolder = `${repoName}-${source.branch}`;
61
+
62
+ log.step(`Downloading ${url}`);
63
+
64
+ const response = await fetch(url);
65
+ if (!response.ok) {
66
+ throw new Error(`Failed to download template (${response.status}): ${url}`);
67
+ }
68
+
69
+ const buffer = await response.arrayBuffer();
70
+ const stamp = Date.now();
71
+ const zipPath = path.join(path.dirname(targetDir), `.kavoru-${stamp}.zip`);
72
+ const extractDir = path.join(path.dirname(targetDir), `.kavoru-${stamp}-extract`);
73
+
74
+ await mkdir(path.dirname(targetDir), { recursive: true });
75
+ await Bun.write(zipPath, buffer);
76
+
77
+ try {
78
+ await mkdir(extractDir, { recursive: true });
79
+
80
+ if (process.platform === "win32") {
81
+ await runCommand([
82
+ "powershell",
83
+ "-NoProfile",
84
+ "-Command",
85
+ `Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${extractDir.replace(/'/g, "''")}' -Force`,
86
+ ]);
87
+ } else {
88
+ await runCommand(["tar", "-xf", zipPath, "-C", extractDir]);
89
+ }
90
+
91
+ const extractedRoot = path.join(extractDir, extractedFolder);
92
+ await cp(extractedRoot, targetDir, { recursive: true });
93
+ } finally {
94
+ await rm(zipPath, { force: true }).catch(() => undefined);
95
+ await rm(extractDir, { recursive: true, force: true }).catch(() => undefined);
96
+ }
97
+ }
98
+
99
+ export async function fetchTemplate(
100
+ source: TemplateSource,
101
+ targetDir: string,
102
+ ): Promise<void> {
103
+ await mkdir(path.dirname(targetDir), { recursive: true });
104
+
105
+ if (await commandExists("git")) {
106
+ log.step(`Cloning ${source.repo} (${source.branch})`);
107
+ await cloneWithGit(source, targetDir);
108
+ return;
109
+ }
110
+
111
+ log.warn("git not found — downloading template as zip");
112
+ await downloadZip(source, targetDir);
113
+ }
114
+
115
+ export async function removeGitMetadata(projectDir: string): Promise<void> {
116
+ await rm(path.join(projectDir, ".git"), { recursive: true, force: true });
117
+ }
118
+
119
+ export async function customizeProject(
120
+ projectDir: string,
121
+ packageName: string,
122
+ ): Promise<void> {
123
+ const pkgPath = path.join(projectDir, "package.json");
124
+ const pkgFile = Bun.file(pkgPath);
125
+ if (!(await pkgFile.exists())) {
126
+ throw new Error("Template is missing package.json");
127
+ }
128
+
129
+ const pkg = (await pkgFile.json()) as Record<string, unknown>;
130
+ pkg.name = packageName;
131
+ await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
132
+
133
+ const envExamplePath = path.join(projectDir, ".env.example");
134
+ const envExample = Bun.file(envExamplePath);
135
+ if (await envExample.exists()) {
136
+ let envText = await envExample.text();
137
+ envText = envText
138
+ .replace(/^OTEL_SERVICE_NAME=kavoru$/m, `OTEL_SERVICE_NAME=${packageName}`)
139
+ .replace(/^KAFKA_CLIENT_ID=kavoru$/m, `KAFKA_CLIENT_ID=${packageName}`)
140
+ .replace(
141
+ /^KAFKA_GROUP_ID=kavoru-consumer$/m,
142
+ `KAFKA_GROUP_ID=${packageName}-consumer`,
143
+ );
144
+ await Bun.write(envExamplePath, envText);
145
+ await Bun.write(path.join(projectDir, ".env"), envText);
146
+ }
147
+
148
+ const modulesIndex = path.join(projectDir, "src", "modules", "index.ts");
149
+ const modulesFile = Bun.file(modulesIndex);
150
+ if (await modulesFile.exists()) {
151
+ const text = await modulesFile.text();
152
+ const title = packageName
153
+ .replace(/-/g, " ")
154
+ .replace(/\b\w/g, (char) => char.toUpperCase());
155
+ await Bun.write(
156
+ modulesIndex,
157
+ text.replace('title: "🦊 Kavoru"', `title: "🦊 ${title}"`),
158
+ );
159
+ }
160
+ }
161
+
162
+ export async function installDependencies(projectDir: string): Promise<void> {
163
+ log.step("Installing dependencies (bun install)");
164
+ await runCommand(["bun", "install"], projectDir);
165
+ }
166
+
167
+ export function resolveTemplateSource(
168
+ repo: string,
169
+ branch: string,
170
+ ): TemplateSource {
171
+ return { repo, branch: branch || TEMPLATE_BRANCH };
172
+ }
@@ -0,0 +1,34 @@
1
+ import { readdirSync } from "node:fs";
2
+
3
+ const NPM_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9][a-z0-9-._~]*$/;
4
+
5
+ export function toPackageName(input: string): string {
6
+ return input
7
+ .trim()
8
+ .toLowerCase()
9
+ .replace(/\s+/g, "-")
10
+ .replace(/[^a-z0-9-._~@/]/g, "-")
11
+ .replace(/-+/g, "-")
12
+ .replace(/^-+|-+$/g, "");
13
+ }
14
+
15
+ export function assertValidPackageName(name: string): void {
16
+ if (!name) {
17
+ throw new Error("Project name cannot be empty.");
18
+ }
19
+ if (!NPM_NAME_RE.test(name)) {
20
+ throw new Error(
21
+ `"${name}" is not a valid package name. Use lowercase letters, numbers, hyphens, or dots.`,
22
+ );
23
+ }
24
+ }
25
+
26
+ export function isDirectoryEmpty(dir: string): boolean {
27
+ try {
28
+ const entries = readdirSync(dir);
29
+ const ignored = new Set([".git", ".gitignore"]);
30
+ return entries.every((entry) => ignored.has(entry));
31
+ } catch {
32
+ return true;
33
+ }
34
+ }