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 +82 -0
- package/bin/kavoru.js +3 -0
- package/package.json +44 -0
- package/src/args.ts +100 -0
- package/src/cli.ts +111 -0
- package/src/constants.ts +8 -0
- package/src/index.ts +29 -0
- package/src/log.ts +24 -0
- package/src/template.ts +172 -0
- package/src/validate.ts +34 -0
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
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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
};
|
package/src/template.ts
ADDED
|
@@ -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
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -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
|
+
}
|