#!/usr/bin/env node const fs = require("fs"); const os = require("os"); const path = require("path"); const { spawnSync } = require("child_process"); const packageRoot = path.resolve(__dirname, ".."); const requiredDigitalTables = [ "digital_plans", "digital_plan_steps", "digital_schedules", "digital_schedule_runs", "digital_tasks", "digital_runs", "digital_events", "digital_artifacts", "digital_approvals", "digital_sync_outbox", "digital_sync_attempts", ]; const testFiles = [ "src/main/services/digitalEmployee/stateService.test.ts", "src/main/services/digitalEmployee/schedulerService.test.ts", "src/main/ipc/eventForwarders.test.ts", "src/main/services/digitalEmployee/syncService.test.ts", "src/main/services/digitalEmployee/skillCatalogService.test.ts", "src/main/services/digitalEmployee/planTemplateService.test.ts", ]; const selfHealingDigitalSchemaSql = ` CREATE TABLE IF NOT EXISTS digital_plan_steps ( id TEXT PRIMARY KEY, remote_id TEXT, plan_id TEXT NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL, seq INTEGER NOT NULL, depends_on TEXT NOT NULL DEFAULT '[]', preferred_skill_ids TEXT NOT NULL DEFAULT '[]', payload TEXT NOT NULL DEFAULT '{}', sync_status TEXT NOT NULL DEFAULT 'pending', last_synced_at TEXT, sync_error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(plan_id) REFERENCES digital_plans(id) ); CREATE INDEX IF NOT EXISTS idx_digital_plan_steps_plan ON digital_plan_steps(plan_id, seq); CREATE INDEX IF NOT EXISTS idx_digital_plan_steps_sync ON digital_plan_steps(sync_status); CREATE TABLE IF NOT EXISTS digital_schedules ( id TEXT PRIMARY KEY, remote_id TEXT, plan_id TEXT, name TEXT NOT NULL, description TEXT, cron_expr TEXT NOT NULL, timezone TEXT NOT NULL DEFAULT 'system', status TEXT NOT NULL DEFAULT 'enabled', enabled INTEGER NOT NULL DEFAULT 1, prompt TEXT, command TEXT, payload TEXT NOT NULL DEFAULT '{}', next_run_at TEXT, last_run_at TEXT, catch_up_on_startup INTEGER NOT NULL DEFAULT 1, max_catch_up_runs INTEGER NOT NULL DEFAULT 5, max_run_history INTEGER NOT NULL DEFAULT 100, failure_count INTEGER NOT NULL DEFAULT 0, last_error TEXT, sync_status TEXT NOT NULL DEFAULT 'pending', last_synced_at TEXT, sync_error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, deleted_at TEXT, FOREIGN KEY(plan_id) REFERENCES digital_plans(id) ); CREATE TABLE IF NOT EXISTS digital_schedule_runs ( id TEXT PRIMARY KEY, remote_id TEXT, schedule_id TEXT NOT NULL, plan_id TEXT, run_id TEXT, due_at TEXT NOT NULL, started_at TEXT, finished_at TEXT, status TEXT NOT NULL, attempt_no INTEGER NOT NULL DEFAULT 1, trigger_kind TEXT NOT NULL DEFAULT 'scheduled', error TEXT, payload TEXT NOT NULL DEFAULT '{}', sync_status TEXT NOT NULL DEFAULT 'pending', last_synced_at TEXT, sync_error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(schedule_id) REFERENCES digital_schedules(id), FOREIGN KEY(plan_id) REFERENCES digital_plans(id), FOREIGN KEY(run_id) REFERENCES digital_runs(id) ); CREATE INDEX IF NOT EXISTS idx_digital_schedules_due ON digital_schedules(enabled, status, next_run_at); CREATE INDEX IF NOT EXISTS idx_digital_schedules_plan ON digital_schedules(plan_id); CREATE INDEX IF NOT EXISTS idx_digital_schedules_sync ON digital_schedules(sync_status); CREATE INDEX IF NOT EXISTS idx_digital_schedule_runs_schedule ON digital_schedule_runs(schedule_id, due_at); CREATE UNIQUE INDEX IF NOT EXISTS idx_digital_schedule_runs_attempt ON digital_schedule_runs(schedule_id, due_at, attempt_no); CREATE INDEX IF NOT EXISTS idx_digital_schedule_runs_sync ON digital_schedule_runs(sync_status); `; function runStep(title, command, args, options = {}) { console.log(`\n[DigitalEmployeeCheck] ${title}`); const result = spawnSync(command, args, { cwd: packageRoot, stdio: "inherit", shell: process.platform === "win32", ...options, }); if (result.status !== 0) { process.exitCode = result.status || 1; throw new Error(`${title} failed`); } } function checkSgrobotDigitalAssets() { console.log("\n[DigitalEmployeeCheck] Verify embedded digital assets"); const indexPath = path.join(packageRoot, "public", "sgrobot-digital", "index.html"); const html = fs.readFileSync(indexPath, "utf8"); const assetRefs = [...html.matchAll(/(?:src|href)="\.\/(assets\/[^"]+)"/g)] .map((match) => match[1]); if (assetRefs.length === 0) { throw new Error("No embedded digital assets referenced by index.html"); } for (const assetRef of assetRefs) { const assetPath = path.join(packageRoot, "public", "sgrobot-digital", assetRef); if (!fs.existsSync(assetPath)) { throw new Error(`Missing embedded digital asset: ${assetRef}`); } } console.log(`[DigitalEmployeeCheck] ${assetRefs.length} asset references OK`); } function checkLocalDatabaseSchema() { console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema"); const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db"); if (!fs.existsSync(dbPath)) { console.log(`[DigitalEmployeeCheck] Skip DB schema check; database not found: ${dbPath}`); return; } const healResult = spawnSync("sqlite3", [dbPath, selfHealingDigitalSchemaSql], { encoding: "utf8", shell: process.platform === "win32", }); if (healResult.error) { console.log(`[DigitalEmployeeCheck] Skip DB schema check; sqlite3 unavailable: ${healResult.error.message}`); return; } if (healResult.status !== 0) { throw new Error(healResult.stderr || "sqlite3 digital schema self-heal failed"); } const result = spawnSync("sqlite3", [dbPath, ".tables"], { encoding: "utf8", shell: process.platform === "win32", }); if (result.status !== 0) { throw new Error(result.stderr || "sqlite3 .tables failed"); } const tables = new Set(result.stdout.split(/\s+/).filter(Boolean)); const missing = requiredDigitalTables.filter((table) => !tables.has(table)); if (missing.length > 0) { throw new Error(`Missing digital employee tables: ${missing.join(", ")}`); } console.log(`[DigitalEmployeeCheck] ${requiredDigitalTables.length} digital tables OK`); } function main() { try { runStep("Run digital employee unit tests", "npm", ["run", "test:run", "--", ...testFiles]); runStep("Build Electron main process", "npm", ["run", "build:main:dev"]); runStep("Build embedded digital employee", "npm", ["run", "build:sgrobot-digital"]); checkSgrobotDigitalAssets(); checkLocalDatabaseSchema(); console.log("\n[DigitalEmployeeCheck] All checks passed"); } catch (error) { console.error(`\n[DigitalEmployeeCheck] ${error instanceof Error ? error.message : String(error)}`); process.exit(process.exitCode || 1); } } main();