/** * emdash seed * * Apply a seed file to the database */ import { readFile, access, mkdir } 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"; import { applySeed } from "../../seed/apply.js"; import type { SeedFile, SeedApplyOptions } from "../../seed/types.js"; import { validateSeed } from "../../seed/validate.js"; import { LocalStorage } from "../../storage/local.js"; interface PackageJson { name?: string; emdash?: { seed?: string; }; } async function fileExists(path: string): Promise { try { await access(path); return true; } catch { return false; } } 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; } } /** * Resolve seed file path from: * 1. Positional argument (if provided) * 2. .emdash/seed.json (convention) * 3. package.json emdash.seed (config) */ async function resolveSeedPath(cwd: string, positional?: string): Promise { // 1. Positional argument if (positional) { const resolved = resolve(cwd, positional); if (await fileExists(resolved)) { return resolved; } consola.error(`Seed file not found: ${positional}`); return null; } // 2. Convention: .emdash/seed.json const conventionPath = resolve(cwd, ".emdash", "seed.json"); if (await fileExists(conventionPath)) { return conventionPath; } // 3. package.json emdash.seed const pkg = await readPackageJson(cwd); if (pkg?.emdash?.seed) { const pkgSeedPath = resolve(cwd, pkg.emdash.seed); if (await fileExists(pkgSeedPath)) { return pkgSeedPath; } consola.warn(`Seed file from package.json not found: ${pkg.emdash.seed}`); } return null; } export const seedCommand = defineCommand({ meta: { name: "seed", description: "Apply a seed file to the database", }, args: { path: { type: "positional", description: "Path to seed file (default: .emdash/seed.json)", required: false, }, database: { type: "string", alias: "d", description: "Database path", default: "./data.db", }, cwd: { type: "string", description: "Working directory", default: process.cwd(), }, validate: { type: "boolean", description: "Validate only, don't apply", default: false, }, "no-content": { type: "boolean", description: "Skip sample data (content entries, bylines, taxonomy terms)", default: false, }, "on-conflict": { type: "string", description: "Conflict handling: skip, update, error", default: "skip", }, "uploads-dir": { type: "string", description: "Directory for media uploads", default: "./uploads", }, "media-base-url": { type: "string", description: "Base URL for media files", default: "/_emdash/api/media/file", }, }, async run({ args }) { const cwd = resolve(args.cwd); consola.start("Loading seed file..."); // Resolve seed file path const seedPath = await resolveSeedPath(cwd, args.path); if (!seedPath) { consola.error("No seed file found"); consola.info("Provide a path, create .emdash/seed.json, or set emdash.seed in package.json"); process.exit(1); } consola.info(`Seed file: ${seedPath}`); // Load and parse seed file let seed: SeedFile; try { const content = await readFile(seedPath, "utf-8"); seed = JSON.parse(content); } catch (error) { consola.error("Failed to parse seed file:", error); process.exit(1); } // Validate seed consola.start("Validating seed file..."); const validation = validateSeed(seed); if (validation.warnings.length > 0) { for (const warning of validation.warnings) { consola.warn(warning); } } if (!validation.valid) { consola.error("Seed validation failed:"); for (const error of validation.errors) { consola.error(` - ${error}`); } process.exit(1); } consola.success("Seed file is valid"); // If validate-only mode, exit here if (args.validate) { consola.success("Validation complete"); return; } // Connect to database const dbPath = resolve(cwd, args.database); consola.info(`Database: ${dbPath}`); const db = createDatabase({ url: `file:${dbPath}` }); // Run migrations consola.start("Running migrations..."); try { 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); } // Set up storage for $media resolution const uploadsDir = resolve(cwd, args["uploads-dir"]); await mkdir(uploadsDir, { recursive: true }); const storage = new LocalStorage({ directory: uploadsDir, baseUrl: args["media-base-url"], }); // Prepare apply options const onConflictRaw = args["on-conflict"]; if (onConflictRaw !== "skip" && onConflictRaw !== "update" && onConflictRaw !== "error") { consola.error(`Invalid --on-conflict value: ${onConflictRaw}`); consola.info("Use: skip, update, or error"); await db.destroy(); process.exit(1); } const options: SeedApplyOptions = { includeContent: !args["no-content"], onConflict: onConflictRaw, storage, }; // Apply seed consola.start("Applying seed..."); try { const result = await applySeed(db, seed, options); consola.success("Seed applied successfully!"); consola.log(""); // Print summary if (result.settings.applied > 0) { consola.info(`Settings: ${result.settings.applied} applied`); } if ( result.collections.created > 0 || result.collections.skipped > 0 || result.collections.updated > 0 ) { consola.info( `Collections: ${result.collections.created} created, ${result.collections.skipped} skipped, ${result.collections.updated} updated`, ); } if (result.fields.created > 0 || result.fields.skipped > 0 || result.fields.updated > 0) { consola.info( `Fields: ${result.fields.created} created, ${result.fields.skipped} skipped, ${result.fields.updated} updated`, ); } if (result.taxonomies.created > 0 || result.taxonomies.terms > 0) { consola.info( `Taxonomies: ${result.taxonomies.created} created, ${result.taxonomies.terms} terms`, ); } if (result.bylines.created > 0 || result.bylines.skipped > 0 || result.bylines.updated > 0) { consola.info( `Bylines: ${result.bylines.created} created, ${result.bylines.skipped} skipped, ${result.bylines.updated} updated`, ); } if (result.menus.created > 0 || result.menus.items > 0) { consola.info(`Menus: ${result.menus.created} created, ${result.menus.items} items`); } if (result.widgetAreas.created > 0 || result.widgetAreas.widgets > 0) { consola.info( `Widget Areas: ${result.widgetAreas.created} created, ${result.widgetAreas.widgets} widgets`, ); } if (result.content.created > 0 || result.content.skipped > 0 || result.content.updated > 0) { consola.info( `Content: ${result.content.created} created, ${result.content.skipped} skipped, ${result.content.updated} updated`, ); } if (result.media.created > 0 || result.media.skipped > 0) { consola.info(`Media: ${result.media.created} created, ${result.media.skipped} skipped`); } } catch (error) { consola.error("Seed failed:", error instanceof Error ? error.message : error); await db.destroy(); process.exit(1); } await db.destroy(); consola.success("Done!"); }, });