Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .kilo/agent-manager.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"worktrees": {},
"sessions": {},
"tabOrder": {
"local": []
}
}
Comment on lines +1 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not version-control local agent state in this file.

worktrees, sessions, and tabOrder.local represent mutable local runtime state. Keeping this tracked will cause noisy diffs/merge conflicts and can leak local session metadata once populated. Move this to ignored local storage (or commit only a static template and gitignore the live state file).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.kilo/agent-manager.json around lines 1 - 9, The .kilo/agent-manager.json
currently contains mutable runtime keys ("worktrees", "sessions",
"tabOrder.local") that must not be version-controlled; stop tracking live state
by either replacing the committed file with a static template (e.g., a minimal
skeleton showing allowed keys) and adding the real file path to .gitignore, or
remove the file from git history and add it to .gitignore so runtime-populated
"worktrees", "sessions", and "tabOrder.local" are stored only locally; ensure
the repository retains a safe template if consumers need an example.

1 change: 1 addition & 0 deletions drizzle/migrations/0001_initial_down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS users;
4 changes: 4 additions & 0 deletions drizzle/migrations/0001_initial_up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Initial migration: creates the base users table.
-- Additional tables (sessions, tokens, domain tables, etc.) are generated by
-- subsequent migrations produced by `drizzle-kit generate`.
CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY);
12 changes: 10 additions & 2 deletions packages/cli/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,19 @@ export async function runDevCommand(projectRoot: string) {

// --- Graceful shutdown ---
process.on("SIGINT", async () => {
await shutdown();
try {
await shutdown();
} catch (e) {
warn(`Shutdown error: ${e instanceof Error ? e.message : String(e)}`);
}
process.exit(0);
});
process.on("SIGTERM", async () => {
await shutdown();
try {
await shutdown();
} catch (e) {
warn(`Shutdown error: ${e instanceof Error ? e.message : String(e)}`);
}
process.exit(0);
});

Expand Down
15 changes: 8 additions & 7 deletions packages/cli/src/commands/iac/analyze.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { extname, join } from "node:path";
import * as logger from "../../utils/logger";

Expand All @@ -16,7 +16,7 @@ export async function runIacAnalyze(
): Promise<void> {
const betterbaseDir = join(projectRoot, "betterbase");

if (!statSync(betterbaseDir).isDirectory()) {
if (!existsSync(betterbaseDir) || !statSync(betterbaseDir).isDirectory()) {
logger.error("No betterbase/ directory found. Run this from a BetterBase project.");
return;
}
Expand All @@ -27,7 +27,7 @@ export async function runIacAnalyze(
const results: QueryAnalysis[] = [];

for (const q of queries) {
const analysis = analyzeQuery(q);
const analysis = analyzeQuery(q, betterbaseDir);
results.push(analysis);
}

Expand All @@ -48,10 +48,11 @@ interface QueryAnalysis {

function scanQueries(betterbaseDir: string): string[] {
const queriesDir = join(betterbaseDir, "queries");
const files: string[] = [];

if (!existsSync(queriesDir)) return [];
if (!statSync(queriesDir).isDirectory()) return [];

const files: string[] = [];

function walk(dir: string) {
for (const entry of readdirSync(dir)) {
const fullPath = join(dir, entry);
Expand All @@ -67,9 +68,9 @@ function scanQueries(betterbaseDir: string): string[] {
return files;
}

function analyzeQuery(filePath: string): QueryAnalysis {
function analyzeQuery(filePath: string, betterbaseDir: string): QueryAnalysis {
const content = readFileSync(filePath, "utf-8");
const path = filePath.replace(join(process.cwd(), "betterbase/"), "");
const path = relative(betterbaseDir, filePath);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const issues: string[] = [];
const suggestions: string[] = [];
Expand Down
106 changes: 97 additions & 9 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { existsSync } from "node:fs";
import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { generateDrizzleConfig } from "@betterbase/core/config";
Expand All @@ -10,8 +11,32 @@ import { generateEnvContent, promptForProvider } from "../utils/provider-prompts
/**
* Copy the IaC template to the target directory
*/
async function copyIaCTemplate(targetDir: string): Promise<void> {
const templateDir = path.join(import.meta.dir, "..", "..", "..", "templates", "iac");
async function copyIaCTemplate(targetDir: string, projectName: string): Promise<void> {
// Try multiple possible template locations to support both development and production scenarios
const possibleTemplatePaths = [
// When installed globally and templates are copied to dist/templates (production)
path.join(import.meta.dir, "..", "..", "..", "..", "templates", "iac"),
// When running from built monorepo (packages/cli/dist -> betterbase/templates)
path.join(import.meta.dir, "..", "..", "..", "..", "..", "betterbase", "templates", "iac"),
// When running from monorepo source with one level of nesting
path.join(import.meta.dir, "..", "..", "..", "..", "..", "..", "betterbase", "templates", "iac"),
// When running from monorepo source with betterbase/ subdirectory
path.join(import.meta.dir, "..", "..", "..", "..", "..", "..", "..", "betterbase", "templates", "iac"),
];

let templateDir: string | null = null;
for (const testPath of possibleTemplatePaths) {
if (existsSync(testPath)) {
templateDir = testPath;
break;
}
}

if (!templateDir) {
throw new Error(
`IaC template not found. Searched:\n${possibleTemplatePaths.map((p) => ` - ${p}`).join("\n")}`
);
}

// Check if template exists
try {
Expand Down Expand Up @@ -63,11 +88,25 @@ async function copyIaCTemplate(targetDir: string): Promise<void> {
try {
const content = await readFile(srcPath);
await writeFile(destPath, content);
} catch {
// Skip if file doesn't exist
} catch (error) {
const code = (error as NodeJS.ErrnoException | undefined)?.code;
if (code === "ENOENT") {
throw new Error(`Missing IaC template file: ${srcPath}`);
}
throw error;
}
}

// Inject the user-supplied project name into the copied package.json
const pkgPath = path.join(targetDir, "package.json");
try {
const pkgJson = JSON.parse(await readFile(pkgPath, "utf-8"));
pkgJson.name = projectName;
await writeFile(pkgPath, `${JSON.stringify(pkgJson, null, 2)}\n`);
} catch {
// package.json absent from template — safe to skip
}

// Create .env file with multi-provider support
await writeFile(
path.join(targetDir, ".env"),
Expand Down Expand Up @@ -183,7 +222,7 @@ function getAuthDialect(provider: ProviderType): "sqlite" | "pg" | "mysql" {
}

async function installDependencies(projectPath: string): Promise<void> {
const installProcess = Bun.spawn(["bun", "install"], {
const installProcess = Bun.spawn([process.execPath, "install"], {
cwd: projectPath,
stdout: "inherit",
stderr: "inherit",
Expand Down Expand Up @@ -471,7 +510,7 @@ try {
const sqlite = new Database(env.DB_PATH, { create: true });
const db = drizzle(sqlite);

migrate(db, { migrationsFolder: './drizzle' });
await migrate(db, { migrationsFolder: './drizzle' });
console.log('Migrations applied successfully.');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Expand Down Expand Up @@ -855,8 +894,57 @@ const s3 = new S3Client({
${endpointLine}
});

const BUCKET = process.env.STORAGE_BUCKET ?? ''
`;
const BUCKET = process.env.STORAGE_BUCKET ?? '';

export const storageRoute = new Hono();

// TODO: Replace with your production auth middleware before deploying
storageRoute.use('*', async (c, next) => {
// Import auth middleware dynamically to avoid circular dependencies
try {
const { requireAuth } = await import('./middleware/auth');
return requireAuth(c, next);
} catch (e) {
// Fallback to simple bearer check for development
const authHeader = c.req.header('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401);
}
// In a real app, you would verify the token here
// For now, we'll just pass it through but log a warning
console.warn('Using fallback auth - replace with proper token verification');
await next();
}
});

storageRoute.put('/:key', async (c) => {
const key = c.req.param('key');
const body = await c.req.arrayBuffer();
await s3.send(new PutObjectCommand({
Bucket: BUCKET,
Key: key,
Body: Buffer.from(body),
}));
return c.json({ ok: true });
});

storageRoute.get('/:key', async (c) => {
const key = c.req.param('key');
const url = await getSignedUrl(s3, new GetObjectCommand({
Bucket: BUCKET,
Key: key,
}), { expiresIn: 3600 });
return c.json({ url });
});

storageRoute.delete('/:key', async (c) => {
const key = c.req.param('key');
await s3.send(new DeleteObjectCommand({
Bucket: BUCKET,
Key: key,
}));
return c.json({ ok: true });
});`;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

async function writeProjectFiles(
Expand Down Expand Up @@ -1347,7 +1435,7 @@ export async function runInitCommand(rawOptions: InitCommandOptions): Promise<vo
}

// Copy templates/iac/ to target directory
await copyIaCTemplate(projectPath);
await copyIaCTemplate(projectPath, projectName);

logger.blank();
console.log(chalk.bold(chalk.white(` ✦ ${projectName}`)) + chalk.dim(" initialized"));
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface WebhookEntry {
enabled: boolean;
}

function generateWebhookId(): string {
export function generateWebhookId(): string {
return `webhook-${Date.now().toString(36)}`;
}

Expand Down
19 changes: 11 additions & 8 deletions packages/cli/src/utils/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { homedir } from "os";
import { join } from "path";
import { z } from "zod";

const CREDENTIALS_DIR = join(homedir(), ".betterbase");
const CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
// Allow override for testing
const getCredentialsDir = () => process.env.BB_CREDENTIALS_DIR || join(homedir(), ".betterbase");
const CREDENTIALS_FILE = () => join(getCredentialsDir(), "credentials.json");

const CredentialsSchema = z.object({
token: z.string(),
Expand All @@ -16,29 +17,31 @@ const CredentialsSchema = z.object({
export type Credentials = z.infer<typeof CredentialsSchema>;

export function saveCredentials(creds: Credentials): void {
const CREDENTIALS_DIR = getCredentialsDir();
if (!existsSync(CREDENTIALS_DIR)) {
mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
}
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
writeFileSync(CREDENTIALS_FILE(), JSON.stringify(creds, null, 2), { mode: 0o600 });
}

export function loadCredentials(): Credentials | null {
if (!existsSync(CREDENTIALS_FILE)) return null;
if (!existsSync(CREDENTIALS_FILE())) return null;
try {
const raw = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
const raw = JSON.parse(readFileSync(CREDENTIALS_FILE(), "utf-8"));
return CredentialsSchema.parse(raw);
} catch {
return null;
}
}

export function clearCredentials(): void {
if (existsSync(CREDENTIALS_FILE)) {
writeFileSync(CREDENTIALS_FILE, JSON.stringify({}));
if (existsSync(CREDENTIALS_FILE())) {
writeFileSync(CREDENTIALS_FILE(), JSON.stringify({}));
}
}

export function getServerUrl(): string {
const creds = loadCredentials();
return creds?.server_url ?? "https://api.betterbase.io"; // Falls back to cloud
let url = creds?.server_url ?? "https://api.betterbase.io";
return url.replace(/\/+$/, ""); // Remove trailing slashes
}
Loading
Loading