/** * emdash dev * * Start development server with optional schema sync from remote */ import { spawn } from "node:child_process"; import { readFile, access } from "node:fs/promises"; import { resolve } from "node:path"; import { defineCommand } from "citty"; import consola from "consola"; import { createDatabase } from "../../database/connection.js"; import { runMigrations } from "../../database/migrations/runner.js"; interface PackageJson { name?: string; scripts?: Record; emdash?: { url?: string; database?: string; }; } async function readPackageJson(cwd: string): Promise { const pkgPath = resolve(cwd, "package.json"); try { const content = await readFile(pkgPath, "utf-8"); return JSON.parse(content); } catch { return null; } } async function fileExists(path: string): Promise { try { await access(path); return true; } catch { return false; } } export const devCommand = defineCommand({ meta: { name: "dev", description: "Start dev server with local database", }, args: { database: { type: "string", alias: "d", description: "Database path (default: ./data.db)", default: "./data.db", }, types: { type: "boolean", alias: "t", description: "Generate types from remote before starting", default: false, }, port: { type: "string", alias: "p", description: "Port for dev server", default: "4321", }, cwd: { type: "string", description: "Working directory", default: process.cwd(), }, }, async run({ args }) { const cwd = resolve(args.cwd); const pkg = await readPackageJson(cwd); if (!pkg) { consola.error("No package.json found"); process.exit(1); } const dbPath = resolve(cwd, args.database); // Run migrations if database doesn't exist const dbExists = await fileExists(dbPath); if (!dbExists) { consola.start("Database not found, initializing..."); } // Always run migrations (they're idempotent) const db = createDatabase({ url: `file:${dbPath}` }); try { consola.start("Checking database migrations..."); const { applied } = await runMigrations(db); if (applied.length > 0) { consola.success(`Applied ${applied.length} migrations`); } else { consola.info("Database up to date"); } } catch (error) { consola.error("Migration failed:", error); await db.destroy(); process.exit(1); } await db.destroy(); // Generate types from remote if requested if (args.types) { const remoteUrl = pkg.emdash?.url || process.env.EMDASH_URL; if (!remoteUrl) { consola.warn("No remote URL configured. Set EMDASH_URL or emdash.url in package.json"); } else { try { const { createClientFromArgs } = await import("../client-factory.js"); const client = createClientFromArgs({ url: remoteUrl }); const schema = await client.schemaExport(); const types = await client.schemaTypes(); const { writeFile, mkdir } = await import("node:fs/promises"); const { resolve: resolvePath, dirname } = await import("node:path"); const outputPath = resolvePath(cwd, ".emdash/types.ts"); await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, types, "utf-8"); await writeFile( resolvePath(dirname(outputPath), "schema.json"), JSON.stringify(schema, null, 2), "utf-8", ); consola.success(`Generated types for ${schema.collections.length} collections`); } catch (error) { consola.warn("Type generation failed:", error instanceof Error ? error.message : error); } } } // Start Astro dev server consola.start("Starting Astro dev server..."); const astroArgs = ["astro", "dev", "--port", args.port]; // Check if using pnpm, npm, or yarn const pnpmLockExists = await fileExists(resolve(cwd, "pnpm-lock.yaml")); const yarnLockExists = await fileExists(resolve(cwd, "yarn.lock")); let cmd: string; let cmdArgs: string[]; if (pnpmLockExists) { cmd = "pnpm"; cmdArgs = astroArgs; } else if (yarnLockExists) { cmd = "yarn"; cmdArgs = astroArgs; } else { cmd = "npx"; cmdArgs = astroArgs; } consola.info(`Running: ${cmd} ${cmdArgs.join(" ")}`); const child = spawn(cmd, cmdArgs, { cwd, stdio: "inherit", env: { ...process.env, // Pass database path to Astro EMDASH_DATABASE_URL: `file:${dbPath}`, }, }); child.on("error", (error) => { consola.error("Failed to start dev server:", error); process.exit(1); }); child.on("exit", (code) => { process.exit(code ?? 0); }); // Handle termination signals const cleanup = () => { child.kill("SIGTERM"); }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); }, });