Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions betterbase/apps/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env node

/**
* Legacy bb wrapper entrypoint.
* Entrypoint for the legacy "bb" CLI that delegates to the canonical CLI implementation.
*
* Forwards execution to the canonical CLI implementation in packages/cli.
* Dynamically imports the canonical CLI module and invokes it with the current process arguments.
*/
export async function runLegacyCli(): Promise<void> {
const cliModule = await import('../../../packages/cli/src/index');
Expand All @@ -12,4 +12,4 @@ export async function runLegacyCli(): Promise<void> {

if (import.meta.main) {
await runLegacyCli();
}
}
9 changes: 7 additions & 2 deletions betterbase/packages/cli/src/build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
/**
* Build the CLI as a standalone bundled executable output.
* Builds the CLI into a standalone bundled executable and prefixes the output with a Bun shebang.
*
* If the build fails, this function throws an Error whose message includes the number of build logs.
* On success, it writes the bundled file to ./dist/index.js and prepends "#!/usr/bin/env bun" so the file can be executed directly.
*
* @throws Error When the Bun build reports failure; the error message contains the count of build logs.
*/
export async function buildStandaloneCli(): Promise<void> {
const result = await Bun.build({
Expand All @@ -21,4 +26,4 @@ export async function buildStandaloneCli(): Promise<void> {
await Bun.write(outputPath, `#!/usr/bin/env bun\n${compiled}`);
}

await buildStandaloneCli();
await buildStandaloneCli();
71 changes: 69 additions & 2 deletions betterbase/packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ type DatabaseMode = z.infer<typeof databaseModeSchema>;

export type InitCommandOptions = z.infer<typeof initOptionsSchema>;

/**
* Get a human-readable label for a chosen database mode.
*
* @param databaseMode - The database mode; one of 'local', 'neon', or 'turso'
* @returns The label: `'Neon (serverless Postgres)'` for `'neon'`, `'Turso (edge SQLite)'` for `'turso'`, or `'SQLite (local.db)'` for `'local'` and other values
*/
function getDatabaseLabel(databaseMode: DatabaseMode): string {
if (databaseMode === 'neon') {
return 'Neon (serverless Postgres)';
Expand All @@ -32,6 +38,12 @@ function getDatabaseLabel(databaseMode: DatabaseMode): string {
return 'SQLite (local.db)';
}

/**
* Installs project dependencies in the specified project directory using Bun.
*
* @param projectPath - File system path to the project directory where dependencies will be installed
* @throws Error if the install process exits with a non-zero code; message suggests running `bun install` manually
*/
async function installDependencies(projectPath: string): Promise<void> {
const installProcess = Bun.spawn(['bun', 'install'], {
cwd: projectPath,
Expand All @@ -46,6 +58,13 @@ async function installDependencies(projectPath: string): Promise<void> {
}
}

/**
* Initializes a Git repository inside the specified project directory.
*
* Attempts to run `git init` in the provided path and logs a warning if initialization fails.
*
* @param projectPath - Filesystem path of the project directory where the repository should be initialized
*/
async function initializeGitRepository(projectPath: string): Promise<void> {
const gitProcess = Bun.spawn(['git', 'init'], {
cwd: projectPath,
Expand All @@ -60,6 +79,16 @@ async function initializeGitRepository(projectPath: string): Promise<void> {
}
}

/**
* Build the contents of a package.json tailored to the chosen project settings.
*
* The generated package JSON includes the project name, `private: true`, `"type": "module"`, standard scripts for development and Drizzle, a `dependencies` object that varies by `databaseMode` and `useAuth`, and a set of `devDependencies`.
*
* @param projectName - The package `name` field to use in package.json
* @param databaseMode - The selected database mode; determines which database client dependency is included
* @param useAuth - Whether to include the authentication library dependency
* @returns A pretty-printed JSON string representing the package.json contents (with a trailing newline)
*/
function buildPackageJson(projectName: string, databaseMode: DatabaseMode, useAuth: boolean): string {
const dependencies: Record<string, string> = {
hono: '^4.11.9',
Expand Down Expand Up @@ -103,6 +132,12 @@ function buildPackageJson(projectName: string, databaseMode: DatabaseMode, useAu
return `${JSON.stringify(json, null, 2)}\n`;
}

/**
* Builds the content of a drizzle.config.ts file configured for the given database mode.
*
* @param databaseMode - The selected database mode ('local', 'neon', or 'turso') used to choose the Drizzle dialect
* @returns The TypeScript source for a Drizzle configuration that sets schema path, output directory, chosen dialect, and `dbCredentials.url` (falling back to `file:local.db`)
*/
function buildDrizzleConfig(databaseMode: DatabaseMode): string {
const dialect: Record<DatabaseMode, 'sqlite' | 'postgresql' | 'turso'> = {
local: 'sqlite',
Expand All @@ -123,6 +158,12 @@ export default defineConfig({
`;
}

/**
* Produce the TypeScript source for a Drizzle ORM `users` schema tailored to the chosen database mode.
*
* @param databaseMode - The target database mode (`'local'`, `'neon'`, or `'turso'`) used to select the appropriate dialect and schema shape
* @returns A string containing the generated TypeScript schema file content for a `users` table compatible with the selected database dialect
*/
function buildSchema(databaseMode: DatabaseMode): string {
if (databaseMode === 'neon') {
return `import { integer, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core';
Expand All @@ -147,6 +188,12 @@ export const users = sqliteTable('users', {
`;
}

/**
* Generate the TypeScript source for a database index module configured for the specified database mode.
*
* @param databaseMode - The target database mode: `'local'` produces a Bun SQLite client, `'neon'` produces a node-postgres Pool with Drizzle, and `'turso'` produces a @libsql/client with Drizzle.
* @returns The source code string of a module that initializes and exports a configured `db` instance wired to the generated `schema`.
*/
function buildDbIndex(databaseMode: DatabaseMode): string {
if (databaseMode === 'neon') {
return `import { drizzle } from 'drizzle-orm/node-postgres';
Expand Down Expand Up @@ -184,6 +231,11 @@ export const db = drizzle(client, { schema });
`;
}

/**
* Generate TypeScript source for a minimal Hono authentication middleware placeholder.
*
* @returns A string containing TypeScript source that exports `authMiddleware` as a Hono middleware which currently forwards requests (`await next()`) and includes a `TODO` comment to implement session validation.
*/
function buildAuthMiddleware(): string {
return `import { createMiddleware } from 'hono/factory';

Expand All @@ -194,6 +246,11 @@ export const authMiddleware = createMiddleware(async (_c, next) => {
`;
}

/**
* Generate README.md content for a new project.
*
* @returns The README content string including the project title and recommended Bun scripts: `bun run dev`, `bun run db:generate`, and `bun run db:push`.
*/
function buildReadme(projectName: string): string {
return `# ${projectName}

Expand All @@ -207,6 +264,14 @@ Generated with BetterBase CLI.
`;
}

/**
* Scaffold a new project at the given path by creating directories and writing generated configuration, source, and helper files according to the chosen database mode and authentication option.
*
* @param projectPath - Filesystem path where the project will be created
* @param projectName - Name used in the generated package.json and README
* @param databaseMode - Selected database mode used to tailor DB config, schema, and wiring (`local`, `neon`, or `turso`)
* @param useAuth - Include authentication middleware and related configuration when true
*/
async function writeProjectFiles(
projectPath: string,
projectName: string,
Expand Down Expand Up @@ -381,7 +446,9 @@ export default server;
}

/**
* Run the `bb init` command.
* Scaffolds a new BetterBase project by prompting for options, creating files, installing dependencies, and optionally initializing git.
*
* @param rawOptions - Partial CLI options used to pre-fill prompts (e.g., `projectName`)
*/
export async function runInitCommand(rawOptions: InitCommandOptions): Promise<void> {
const options = initOptionsSchema.parse(rawOptions);
Expand Down Expand Up @@ -457,4 +524,4 @@ export async function runInitCommand(rawOptions: InitCommandOptions): Promise<vo
const message = error instanceof Error ? error.message : 'Unknown init error';
throw new Error(`Failed to initialize project: ${message}`);
}
}
}
8 changes: 6 additions & 2 deletions betterbase/packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import packageJson from '../package.json';

/**
* Create and configure the BetterBase CLI program.
*
* @returns The configured Commander `Command` instance for the CLI.
*/
export function createProgram(): Command {
const program = new Command();
Expand Down Expand Up @@ -35,7 +37,9 @@ export function createProgram(): Command {
}

/**
* Execute the CLI with process arguments.
* Run the BetterBase CLI with the given command-line arguments.
*
* @param argv - Argument vector to parse; defaults to `process.argv`
*/
export async function runCli(argv: string[] = process.argv): Promise<void> {
const program = createProgram();
Expand All @@ -50,4 +54,4 @@ if (import.meta.main) {
logger.error(message);
process.exitCode = 1;
}
}
}
20 changes: 15 additions & 5 deletions betterbase/packages/cli/src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
import chalk from 'chalk';

/**
* Print an informational message to stdout.
* Print an informational message prefixed with an info icon and colored blue.
*
* The message is prefixed with "ℹ" and written to stdout.
*
* @param message - The text to print as an informational message
*/
export function info(message: string): void {
console.log(chalk.blue(`ℹ ${message}`));
}

/**
* Print a warning message to stdout.
* Logs a warning message to stdout with a yellow "⚠" prefix.
*
* @param message - The warning text to display
*/
export function warn(message: string): void {
console.log(chalk.yellow(`⚠ ${message}`));
}

/**
* Print an error message to stderr.
* Print an error message to stderr prefixed with a red "✖" icon.
*
* @param message - The error message to print
*/
export function error(message: string): void {
console.error(chalk.red(`✖ ${message}`));
}

/**
* Print a success message to stdout.
* Print a success message to stdout prefixed with a check mark and colored green.
*
* @param message - The message text to display after the check mark
*/
export function success(message: string): void {
console.log(chalk.green(`✔ ${message}`));
}
}
22 changes: 18 additions & 4 deletions betterbase/packages/cli/src/utils/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const selectOptionsSchema = z.object({
});

/**
* Prompt for text input.
* Prompts the user to enter text using the provided message.
*
* @param options.message - The prompt message shown to the user
* @param options.initial - Optional default value prefilled in the input
* @returns The text entered by the user
*/
export async function text(options: { message: string; initial?: string }): Promise<string> {
const parsed = textOptionsSchema.parse(options);
Expand All @@ -41,7 +45,11 @@ export async function text(options: { message: string; initial?: string }): Prom
}

/**
* Prompt for yes/no confirmation.
* Prompt the user with a yes/no confirmation.
*
* @param options.message - The message to display to the user
* @param options.initial - The default selection if the user just presses Enter
* @returns `true` if the user confirms, `false` otherwise.
*/
export async function confirm(options: { message: string; initial?: boolean }): Promise<boolean> {
const parsed = confirmOptionsSchema.parse(options);
Expand All @@ -59,7 +67,13 @@ export async function confirm(options: { message: string; initial?: boolean }):
}

/**
* Prompt for selecting one option.
* Prompt the user to choose one option from a list.
*
* @param options - Configuration for the prompt:
* - `message`: The text displayed to the user.
* - `choices`: Array of choices where each item has a `name` (label shown) and `value` (returned value).
* - `initial`: Optional `value` to select by default.
* @returns The `value` of the selected choice
*/
export async function select(
options: { message: string; choices: Array<{ name: string; value: string }>; initial?: string },
Expand All @@ -77,4 +91,4 @@ export async function select(
]);

return response.value;
}
}
10 changes: 9 additions & 1 deletion betterbase/templates/base/src/middleware/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { HTTPException } from 'hono/http-exception';
import type { ZodType } from 'zod';
import { z } from 'zod';

/**
* Validate and parse an input value against a Zod schema.
*
* @param schema - Zod schema to validate and parse the input into type `T`
* @param body - The value to validate
* @returns The validated and parsed value as type `T`
* @throws HTTPException with status 400 when validation fails. The exception payload contains `message: "Validation failed"` and `cause.errors`, an array of objects each with `path` (dot-joined string), `message`, and `code`
*/
export function parseBody<T>(schema: ZodType<T>, body: unknown): T {
const result = schema.safeParse(body);

Expand All @@ -25,4 +33,4 @@ export function parseBody<T>(schema: ZodType<T>, body: unknown): T {
export const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
});
});