#!/usr/bin/env node import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import process from "node:process"; const SCHEMAS = [ { name: "openclaw-state", schema: "src/state/openclaw-state-schema.sql", outFile: "src/state/openclaw-state-db.generated.d.ts", schemaOutFile: "src/state/openclaw-state-schema.generated.ts", schemaExport: "OPENCLAW_STATE_SCHEMA_SQL", }, { name: "openclaw-agent", schema: "src/state/openclaw-agent-schema.sql", outFile: "src/state/openclaw-agent-db.generated.d.ts", schemaOutFile: "src/state/openclaw-agent-schema.generated.ts", schemaExport: "OPENCLAW_AGENT_SCHEMA_SQL", }, ]; const verify = process.argv.includes("--verify") || process.argv.includes("--check"); function run(command, args, options = {}) { const result = spawnSync(command, args, { stdio: options.input ? ["pipe", "inherit", "inherit"] : "inherit", input: options.input, encoding: "utf8", env: { ...process.env, ...options.env }, cwd: options.cwd, }); if (result.error) { throw result.error; } if (result.status !== 0) { process.exit(result.status ?? 1); } } function runCapture(command, args, options = {}) { const result = spawnSync(command, args, { stdio: ["pipe", "pipe", "inherit"], input: options.input, encoding: "utf8", env: { ...process.env, ...options.env }, cwd: options.cwd, }); if (result.error) { throw result.error; } if (result.status !== 0) { process.exit(result.status ?? 1); } return result.stdout; } function sqliteJson(dbPath, sql) { const raw = runCapture("sqlite3", ["-json", dbPath, sql]); return raw.trim() ? JSON.parse(raw) : []; } function toInterfaceName(tableName) { return tableName .split("_") .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`) .join(""); } function columnBaseType(columnType) { const normalized = columnType.toUpperCase(); if (normalized.includes("BLOB")) { return "Uint8Array"; } if ( normalized.includes("INT") || normalized.includes("REAL") || normalized.includes("FLOA") || normalized.includes("DOUB") || normalized.includes("NUM") || normalized.includes("DEC") ) { return "number"; } return "string"; } function columnType(column, primaryKeyColumnCount) { const baseType = columnBaseType(String(column.type ?? "")); const generated = column.dflt_value != null || (primaryKeyColumnCount === 1 && Number(column.pk) > 0 && String(column.type ?? "") .toUpperCase() .includes("INT")); const nullable = Number(column.notnull) !== 1 && !generated; const valueType = nullable ? `${baseType} | null` : baseType; return generated ? `Generated<${valueType}>` : valueType; } function generateTypes(dbPath) { const tables = sqliteJson( dbPath, "SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name;", ).map((row) => String(row.name)); const lines = [ "/**", " * This file was generated by kysely-codegen.", " * Please do not edit it manually.", " */", "", 'import type { ColumnType } from "kysely";', "", "export type Generated = T extends ColumnType", " ? ColumnType", " : ColumnType;", "", ]; const interfaces = []; for (const table of tables) { const interfaceName = toInterfaceName(table); interfaces.push({ interfaceName, table }); lines.push(`export interface ${interfaceName} {`); const columns = sqliteJson(dbPath, `PRAGMA table_xinfo(${JSON.stringify(table)});`) .filter((column) => Number(column.hidden) === 0) .toSorted((left, right) => String(left.name).localeCompare(String(right.name))); const primaryKeyColumnCount = columns.filter((column) => Number(column.pk) > 0).length; for (const column of columns) { lines.push(` ${column.name}: ${columnType(column, primaryKeyColumnCount)};`); } lines.push("}", ""); } lines.push("export interface DB {"); for (const { interfaceName, table } of interfaces) { lines.push(` ${table}: ${interfaceName};`); } lines.push("}", ""); return lines.join("\n"); } function readUtf8(file) { return fs.readFileSync(file, "utf8"); } function generatedSchemaModule(schema) { const source = readUtf8(schema.schema).trimEnd(); const literal = source.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${"); return [ "/**", " * This file was generated from the SQLite schema source.", " * Please do not edit it manually.", " */", "", `export const ${schema.schemaExport} = \`${literal}\\n\`;`, "", ].join("\n"); } function generate(schema) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `openclaw-kysely-${schema.name}-`)); const tmpDb = path.join(tmpDir, "schema.sqlite"); const tmpOut = verify ? path.join(tmpDir, "db.generated.d.ts") : schema.outFile; const tmpSchemaOut = verify ? path.join(tmpDir, path.basename(schema.schemaOutFile)) : schema.schemaOutFile; try { run("sqlite3", [tmpDb], { input: readUtf8(schema.schema) }); fs.writeFileSync(tmpOut, generateTypes(tmpDb)); if (verify && readUtf8(tmpOut) !== readUtf8(schema.outFile)) { console.error(`${schema.outFile} is out of date. Run pnpm db:kysely:gen.`); process.exitCode = 1; } fs.writeFileSync(tmpSchemaOut, generatedSchemaModule(schema)); if (verify && readUtf8(tmpSchemaOut) !== readUtf8(schema.schemaOutFile)) { console.error(`${schema.schemaOutFile} is out of date. Run pnpm db:kysely:gen.`); process.exitCode = 1; } } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } } for (const schema of SCHEMAS) { generate(schema); }