@aponiajs/cli 0.0.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,15 @@
1
+ # @aponiajs/cli
2
+
3
+ The Bun-native Aponia command-line interface.
4
+
5
+ ## Create a project
6
+
7
+ ```bash
8
+ aponia new my-api
9
+ aponia n my-api --dry-run
10
+ aponia new my-api --skip-install
11
+ ```
12
+
13
+ The initial CLI implements the standard application generator. Component and
14
+ resource schematics will be added behind the same command architecture after
15
+ their module-registration transforms are safe and testable.
package/bin/aponia.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { runCli } from "../src/index.ts";
4
+
5
+ const exitCode = await runCli(Bun.argv.slice(2));
6
+ process.exitCode = exitCode;
@@ -0,0 +1,33 @@
1
+ //#region src/arguments.d.ts
2
+ interface NewCommandOptions {
3
+ readonly command: "new";
4
+ readonly name: string;
5
+ readonly dryRun: boolean;
6
+ readonly skipInstall: boolean;
7
+ }
8
+ type CliCommand = NewCommandOptions | {
9
+ readonly command: "help";
10
+ } | {
11
+ readonly command: "version";
12
+ };
13
+ declare function parseArguments(arguments_: readonly string[]): CliCommand;
14
+ //#endregion
15
+ //#region src/project-generator.d.ts
16
+ interface GenerateProjectOptions {
17
+ readonly name: string;
18
+ readonly cwd?: string;
19
+ readonly dryRun?: boolean;
20
+ readonly skipInstall?: boolean;
21
+ }
22
+ interface GenerateProjectResult {
23
+ readonly projectDirectory: string;
24
+ readonly files: readonly string[];
25
+ readonly installed: boolean;
26
+ readonly dryRun: boolean;
27
+ }
28
+ declare function generateProject(options: GenerateProjectOptions): Promise<GenerateProjectResult>;
29
+ //#endregion
30
+ //#region src/index.d.ts
31
+ declare function runCli(arguments_: readonly string[]): Promise<number>;
32
+ //#endregion
33
+ export { type CliCommand, type GenerateProjectOptions, type GenerateProjectResult, generateProject, parseArguments, runCli };
package/dist/index.mjs ADDED
@@ -0,0 +1,122 @@
1
+ import { mkdir, readdir } from "node:fs/promises";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/arguments.ts
5
+ function parseArguments(arguments_) {
6
+ const [command = "help", ...rest] = arguments_;
7
+ if (command === "help" || command === "--help" || command === "-h") return { command: "help" };
8
+ if (command === "version" || command === "--version" || command === "-v") return { command: "version" };
9
+ if (command !== "new" && command !== "n") throw new Error(`Unknown command "${command}".`);
10
+ const name = rest.find((argument) => !argument.startsWith("-"));
11
+ if (!name) throw new Error("Project name is required.");
12
+ const supportedOptions = /* @__PURE__ */ new Set([
13
+ "--dry-run",
14
+ "-d",
15
+ "--skip-install",
16
+ "-s"
17
+ ]);
18
+ const unknownOption = rest.find((argument) => argument.startsWith("-") && !supportedOptions.has(argument));
19
+ if (unknownOption) throw new Error(`Unknown option "${unknownOption}".`);
20
+ return {
21
+ command: "new",
22
+ name,
23
+ dryRun: rest.includes("--dry-run") || rest.includes("-d"),
24
+ skipInstall: rest.includes("--skip-install") || rest.includes("-s")
25
+ };
26
+ }
27
+ //#endregion
28
+ //#region src/project-generator.ts
29
+ const projectNamePattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
30
+ async function generateProject(options) {
31
+ validateProjectName(options.name);
32
+ const projectDirectory = join(options.cwd ?? process.cwd(), options.name);
33
+ const templateDirectory = fileURLToPath(new URL("../templates/application", import.meta.url));
34
+ const templateFiles = await listFiles(templateDirectory);
35
+ const relativeFiles = templateFiles.map((file) => outputPath(relative(templateDirectory, file)));
36
+ if (options.dryRun) return {
37
+ projectDirectory,
38
+ files: relativeFiles,
39
+ installed: false,
40
+ dryRun: true
41
+ };
42
+ if (await Bun.file(projectDirectory).exists()) throw new Error(`Target directory "${basename(projectDirectory)}" already exists.`);
43
+ await mkdir(projectDirectory, { recursive: false });
44
+ for (const templateFile of templateFiles) {
45
+ const outputFile = join(projectDirectory, outputPath(relative(templateDirectory, templateFile)));
46
+ const output = (await Bun.file(templateFile).text()).replaceAll("{{PROJECT_NAME}}", options.name);
47
+ await mkdir(dirname(outputFile), { recursive: true });
48
+ await Bun.write(outputFile, output);
49
+ }
50
+ const shouldInstall = !options.skipInstall;
51
+ if (shouldInstall) {
52
+ const exitCode = await Bun.spawn(["bun", "install"], {
53
+ cwd: projectDirectory,
54
+ stdin: "inherit",
55
+ stdout: "inherit",
56
+ stderr: "inherit"
57
+ }).exited;
58
+ if (exitCode !== 0) throw new Error(`Bun install failed with exit code ${exitCode}.`);
59
+ }
60
+ return {
61
+ projectDirectory,
62
+ files: relativeFiles,
63
+ installed: shouldInstall,
64
+ dryRun: false
65
+ };
66
+ }
67
+ function outputPath(templatePath) {
68
+ if (templatePath === "_gitignore") return ".gitignore";
69
+ return templatePath.endsWith(".tmpl") ? templatePath.slice(0, -5) : templatePath;
70
+ }
71
+ function validateProjectName(name) {
72
+ if (!projectNamePattern.test(name)) throw new Error("Project name must use lowercase kebab-case and start with a letter.");
73
+ }
74
+ async function listFiles(directory) {
75
+ const entries = await readdir(directory, { withFileTypes: true });
76
+ return (await Promise.all(entries.toSorted((left, right) => left.name.localeCompare(right.name)).map(async (entry) => {
77
+ const path = join(directory, entry.name);
78
+ return entry.isDirectory() ? listFiles(path) : [path];
79
+ }))).flat();
80
+ }
81
+ //#endregion
82
+ //#region src/index.ts
83
+ async function runCli(arguments_) {
84
+ try {
85
+ const command = parseArguments(arguments_);
86
+ if (command.command === "help") {
87
+ console.log(helpText);
88
+ return 0;
89
+ }
90
+ if (command.command === "version") {
91
+ console.log("0.0.0");
92
+ return 0;
93
+ }
94
+ const result = await generateProject(command);
95
+ if (result.dryRun) {
96
+ console.log(`CREATE ${result.projectDirectory}`);
97
+ for (const file of result.files) console.log(` ${file}`);
98
+ return 0;
99
+ }
100
+ console.log(`Created ${command.name}`);
101
+ console.log(`Next: cd ${command.name} && bun run dev`);
102
+ return 0;
103
+ } catch (error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ console.error(`Aponia CLI error: ${message}`);
106
+ return 1;
107
+ }
108
+ }
109
+ const helpText = `Aponia CLI
110
+
111
+ Usage:
112
+ aponia new <name> [options]
113
+ aponia n <name> [options]
114
+
115
+ Options:
116
+ -d, --dry-run Report files without writing them
117
+ -s, --skip-install Generate without running bun install
118
+ -h, --help Show command help
119
+ -v, --version Show CLI version
120
+ `;
121
+ //#endregion
122
+ export { generateProject, parseArguments, runCli };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@aponiajs/cli",
3
+ "version": "0.0.0",
4
+ "description": "Bun-native project generator and command-line interface for Aponia.",
5
+ "homepage": "https://github.com/aponiajs/aponiajs#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/aponiajs/aponiajs/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "AponiaJS contributors",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/aponiajs/aponiajs.git",
14
+ "directory": "packages/cli"
15
+ },
16
+ "bin": {
17
+ "aponia": "./bin/aponia.ts"
18
+ },
19
+ "files": [
20
+ "bin",
21
+ "dist",
22
+ "src",
23
+ "templates"
24
+ ],
25
+ "type": "module",
26
+ "exports": {
27
+ ".": "./dist/index.mjs",
28
+ "./package.json": "./package.json"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "bunx --bun vp pack",
35
+ "test": "bun test",
36
+ "check": "bunx --bun vp check",
37
+ "prepublishOnly": "bun run build"
38
+ }
39
+ }
@@ -0,0 +1,47 @@
1
+ export interface NewCommandOptions {
2
+ readonly command: "new";
3
+ readonly name: string;
4
+ readonly dryRun: boolean;
5
+ readonly skipInstall: boolean;
6
+ }
7
+
8
+ export type CliCommand =
9
+ | NewCommandOptions
10
+ | { readonly command: "help" }
11
+ | { readonly command: "version" };
12
+
13
+ export function parseArguments(arguments_: readonly string[]): CliCommand {
14
+ const [command = "help", ...rest] = arguments_;
15
+
16
+ if (command === "help" || command === "--help" || command === "-h") {
17
+ return { command: "help" };
18
+ }
19
+
20
+ if (command === "version" || command === "--version" || command === "-v") {
21
+ return { command: "version" };
22
+ }
23
+
24
+ if (command !== "new" && command !== "n") {
25
+ throw new Error(`Unknown command "${command}".`);
26
+ }
27
+
28
+ const name = rest.find((argument) => !argument.startsWith("-"));
29
+ if (!name) {
30
+ throw new Error("Project name is required.");
31
+ }
32
+
33
+ const supportedOptions = new Set(["--dry-run", "-d", "--skip-install", "-s"]);
34
+ const unknownOption = rest.find(
35
+ (argument) => argument.startsWith("-") && !supportedOptions.has(argument),
36
+ );
37
+ if (unknownOption) {
38
+ throw new Error(`Unknown option "${unknownOption}".`);
39
+ }
40
+
41
+ return {
42
+ command: "new",
43
+ name,
44
+ dryRun: rest.includes("--dry-run") || rest.includes("-d"),
45
+ skipInstall: rest.includes("--skip-install") || rest.includes("-s"),
46
+ };
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { parseArguments } from "./arguments.ts";
2
+ import { generateProject } from "./project-generator.ts";
3
+
4
+ export { parseArguments, type CliCommand } from "./arguments.ts";
5
+ export {
6
+ generateProject,
7
+ type GenerateProjectOptions,
8
+ type GenerateProjectResult,
9
+ } from "./project-generator.ts";
10
+
11
+ export async function runCli(arguments_: readonly string[]): Promise<number> {
12
+ try {
13
+ const command = parseArguments(arguments_);
14
+
15
+ if (command.command === "help") {
16
+ console.log(helpText);
17
+ return 0;
18
+ }
19
+
20
+ if (command.command === "version") {
21
+ console.log("0.0.0");
22
+ return 0;
23
+ }
24
+
25
+ const result = await generateProject(command);
26
+ if (result.dryRun) {
27
+ console.log(`CREATE ${result.projectDirectory}`);
28
+ for (const file of result.files) {
29
+ console.log(` ${file}`);
30
+ }
31
+ return 0;
32
+ }
33
+
34
+ console.log(`Created ${command.name}`);
35
+ console.log(`Next: cd ${command.name} && bun run dev`);
36
+ return 0;
37
+ } catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ console.error(`Aponia CLI error: ${message}`);
40
+ return 1;
41
+ }
42
+ }
43
+
44
+ const helpText = `Aponia CLI
45
+
46
+ Usage:
47
+ aponia new <name> [options]
48
+ aponia n <name> [options]
49
+
50
+ Options:
51
+ -d, --dry-run Report files without writing them
52
+ -s, --skip-install Generate without running bun install
53
+ -h, --help Show command help
54
+ -v, --version Show CLI version
55
+ `;
@@ -0,0 +1,104 @@
1
+ import { mkdir, readdir } from "node:fs/promises";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export interface GenerateProjectOptions {
6
+ readonly name: string;
7
+ readonly cwd?: string;
8
+ readonly dryRun?: boolean;
9
+ readonly skipInstall?: boolean;
10
+ }
11
+
12
+ export interface GenerateProjectResult {
13
+ readonly projectDirectory: string;
14
+ readonly files: readonly string[];
15
+ readonly installed: boolean;
16
+ readonly dryRun: boolean;
17
+ }
18
+
19
+ const projectNamePattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
20
+
21
+ export async function generateProject(
22
+ options: GenerateProjectOptions,
23
+ ): Promise<GenerateProjectResult> {
24
+ validateProjectName(options.name);
25
+
26
+ const workingDirectory = options.cwd ?? process.cwd();
27
+ const projectDirectory = join(workingDirectory, options.name);
28
+ const templateDirectory = fileURLToPath(new URL("../templates/application", import.meta.url));
29
+ const templateFiles = await listFiles(templateDirectory);
30
+ const relativeFiles = templateFiles.map((file) => outputPath(relative(templateDirectory, file)));
31
+
32
+ if (options.dryRun) {
33
+ return {
34
+ projectDirectory,
35
+ files: relativeFiles,
36
+ installed: false,
37
+ dryRun: true,
38
+ };
39
+ }
40
+
41
+ if (await Bun.file(projectDirectory).exists()) {
42
+ throw new Error(`Target directory "${basename(projectDirectory)}" already exists.`);
43
+ }
44
+
45
+ await mkdir(projectDirectory, { recursive: false });
46
+ for (const templateFile of templateFiles) {
47
+ const relativeFile = outputPath(relative(templateDirectory, templateFile));
48
+ const outputFile = join(projectDirectory, relativeFile);
49
+ const template = await Bun.file(templateFile).text();
50
+ const output = template.replaceAll("{{PROJECT_NAME}}", options.name);
51
+
52
+ await mkdir(dirname(outputFile), { recursive: true });
53
+ await Bun.write(outputFile, output);
54
+ }
55
+
56
+ const shouldInstall = !options.skipInstall;
57
+ if (shouldInstall) {
58
+ const process = Bun.spawn(["bun", "install"], {
59
+ cwd: projectDirectory,
60
+ stdin: "inherit",
61
+ stdout: "inherit",
62
+ stderr: "inherit",
63
+ });
64
+ const exitCode = await process.exited;
65
+ if (exitCode !== 0) {
66
+ throw new Error(`Bun install failed with exit code ${exitCode}.`);
67
+ }
68
+ }
69
+
70
+ return {
71
+ projectDirectory,
72
+ files: relativeFiles,
73
+ installed: shouldInstall,
74
+ dryRun: false,
75
+ };
76
+ }
77
+
78
+ function outputPath(templatePath: string): string {
79
+ if (templatePath === "_gitignore") {
80
+ return ".gitignore";
81
+ }
82
+
83
+ return templatePath.endsWith(".tmpl") ? templatePath.slice(0, -5) : templatePath;
84
+ }
85
+
86
+ function validateProjectName(name: string): void {
87
+ if (!projectNamePattern.test(name)) {
88
+ throw new Error("Project name must use lowercase kebab-case and start with a letter.");
89
+ }
90
+ }
91
+
92
+ async function listFiles(directory: string): Promise<string[]> {
93
+ const entries = await readdir(directory, { withFileTypes: true });
94
+ const files = await Promise.all(
95
+ entries
96
+ .toSorted((left, right) => left.name.localeCompare(right.name))
97
+ .map(async (entry) => {
98
+ const path = join(directory, entry.name);
99
+ return entry.isDirectory() ? listFiles(path) : [path];
100
+ }),
101
+ );
102
+
103
+ return files.flat();
104
+ }
@@ -0,0 +1 @@
1
+ PORT=3000
@@ -0,0 +1,36 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A Bun-first Aponia application generated by `@aponiajs/cli`.
4
+
5
+ ```bash
6
+ bun install
7
+ bun run dev
8
+ ```
9
+
10
+ The generated request flow is:
11
+
12
+ ```text
13
+ main.ts
14
+ -> AponiaFactory
15
+ -> AppModule
16
+ -> AppController
17
+ -> AppService
18
+ ```
19
+
20
+ Open `http://localhost:3000/` after starting the application.
21
+
22
+ The starter follows Nest standard mode:
23
+
24
+ ```text
25
+ src/
26
+ |-- app.controller.spec.ts
27
+ |-- app.controller.ts
28
+ |-- app.module.ts
29
+ |-- app.service.ts
30
+ `-- main.ts
31
+ test/
32
+ `-- app.e2e-spec.ts
33
+ ```
34
+
35
+ Add later features under `src/<feature>`, with the feature controller, module,
36
+ service, DTOs, entities, and unit tests kept together.
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.*
5
+ !.env.example
6
+ *.log
@@ -0,0 +1,9 @@
1
+ {
2
+ "sourceRoot": "src",
3
+ "entryFile": "main",
4
+ "platform": "elysia",
5
+ "language": "typescript",
6
+ "generateOptions": {
7
+ "spec": true
8
+ }
9
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --watch src/main.ts",
8
+ "start": "bun src/main.ts",
9
+ "build": "bun build ./src/main.ts --outdir ./dist --target bun",
10
+ "test": "bun test",
11
+ "test:e2e": "bun test ./test/app.e2e-spec.ts",
12
+ "check": "vp check"
13
+ },
14
+ "dependencies": {
15
+ "@aponiajs/common": "latest",
16
+ "@aponiajs/core": "latest",
17
+ "@aponiajs/platform-elysia": "latest",
18
+ "elysia": "^1.4.29"
19
+ },
20
+ "devDependencies": {
21
+ "@types/bun": "^1.3.14",
22
+ "typescript": "^7.0.2",
23
+ "vite-plus": "^0.2.4"
24
+ },
25
+ "packageManager": "bun@1.3.14"
26
+ }
@@ -0,0 +1,11 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { AppController } from "./app.controller.ts";
3
+ import { AppService } from "./app.service.ts";
4
+
5
+ describe("AppController", () => {
6
+ test("returns the application greeting", () => {
7
+ const controller = new AppController(new AppService());
8
+
9
+ expect(controller.getGreeting()).toBe("Hello from {{PROJECT_NAME}}!");
10
+ });
11
+ });
@@ -0,0 +1,12 @@
1
+ import { Controller, Get } from "@aponiajs/common";
2
+ import { AppService } from "./app.service.ts";
3
+
4
+ @Controller()
5
+ export class AppController {
6
+ constructor(private readonly appService: AppService) {}
7
+
8
+ @Get()
9
+ getGreeting(): string {
10
+ return this.appService.getGreeting();
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ import { Module } from "@aponiajs/common";
2
+ import { AppController } from "./app.controller.ts";
3
+ import { AppService } from "./app.service.ts";
4
+
5
+ @Module({
6
+ controllers: [AppController],
7
+ providers: [AppService],
8
+ })
9
+ export class AppModule {}
@@ -0,0 +1,8 @@
1
+ import { Injectable } from "@aponiajs/common";
2
+
3
+ @Injectable()
4
+ export class AppService {
5
+ getGreeting(): string {
6
+ return "Hello from {{PROJECT_NAME}}!";
7
+ }
8
+ }
@@ -0,0 +1,10 @@
1
+ import { AponiaFactory } from "@aponiajs/platform-elysia";
2
+ import { AppModule } from "./app.module.ts";
3
+
4
+ async function bootstrap(): Promise<void> {
5
+ const application = await AponiaFactory.create(AppModule);
6
+ const port = Number(Bun.env.PORT ?? 3000);
7
+ await application.listen(port);
8
+ }
9
+
10
+ await bootstrap();
@@ -0,0 +1,12 @@
1
+ import { expect, test } from "bun:test";
2
+ import { AponiaFactory } from "@aponiajs/platform-elysia";
3
+ import { AppModule } from "../src/app.module.ts";
4
+
5
+ test("GET / routes through AppController and AppService", async () => {
6
+ const application = await AponiaFactory.create(AppModule, { logger: false });
7
+ const response = await application.handle(new Request("http://localhost/"));
8
+
9
+ expect(response.status).toBe(200);
10
+ expect(await response.text()).toBe("Hello from {{PROJECT_NAME}}!");
11
+ await application.close();
12
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["ESNext"],
5
+ "module": "Preserve",
6
+ "moduleResolution": "Bundler",
7
+ "moduleDetection": "force",
8
+ "types": ["bun"],
9
+ "strict": true,
10
+ "experimentalDecorators": true,
11
+ "emitDecoratorMetadata": true,
12
+ "noUnusedLocals": true,
13
+ "noEmit": true,
14
+ "allowImportingTsExtensions": true,
15
+ "isolatedModules": true,
16
+ "verbatimModuleSyntax": true,
17
+ "skipLibCheck": true
18
+ },
19
+ "include": ["src", "test", "vite.config.ts"]
20
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "vite-plus";
2
+
3
+ export default defineConfig({
4
+ lint: {
5
+ options: {
6
+ typeAware: true,
7
+ typeCheck: true,
8
+ },
9
+ },
10
+ fmt: {},
11
+ });