bbdb68a6de
Esta commit conteudo a estrutura atomica completa:
- types: Result<T,E>, AsyncState<T>, Paginated<T>, SortConfig<T>
- utils: date, str, num, cn, debounce, throttle, storage, arr, obj
- validators: Zod schemas — email, password, uuid, url, phone, CPF/CNPJ, sanitizedStr, safeParse
- hooks: useToggle, useAsync, useDebounce, useLocalStorage, useMedia, useInterval, useOnClickOutside, useClipboard, useFetch
- components: Button, Input, Alert, Card, Spinner (atomic design pattern)
- build: tsup v8 ESM+CJS + DTS + sourcemaps — 0 erros
- tests: 57 testes 100% usuarios
- docker: multi-stage Dockerfile (node 20-alpine)
- config: vitest, tsup, tsconfig strict, .npmignore
Filosofia atomica:/utils ← /types ← /validators ← /hooks ← /components
Build: npm run build | Test: npm test | Publish: npm publish
🤖 Generated with Pulse (openclaw + nova-self-improver)
199 lines
5.5 KiB
JavaScript
199 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* init-db.mjs — TaskFlow SQLite bootstrap (idempotent)
|
|
*
|
|
* Reads schema/taskflow.sql and executes it against the TaskFlow SQLite DB.
|
|
* Safe to run multiple times — all DDL uses IF NOT EXISTS.
|
|
*
|
|
* DB path resolution (in order):
|
|
* 1. $OPENCLAW_WORKSPACE/memory/taskflow.sqlite
|
|
* 2. process.cwd()/memory/taskflow.sqlite
|
|
*
|
|
* Usage:
|
|
* node scripts/init-db.mjs
|
|
* npm run init-db
|
|
*
|
|
* Requires: Node >=22.5 (node:sqlite / DatabaseSync)
|
|
*/
|
|
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
import { resolve, dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Resolve paths
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const schemaPath = resolve(__dirname, '..', 'schema', 'taskflow.sql');
|
|
|
|
const workspaceRoot = process.env.OPENCLAW_WORKSPACE
|
|
? process.env.OPENCLAW_WORKSPACE
|
|
: process.cwd();
|
|
|
|
const memoryDir = join(workspaceRoot, 'memory');
|
|
const dbPath = join(memoryDir, 'taskflow.sqlite');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function log(msg) {
|
|
const ts = new Date().toISOString();
|
|
console.log(`[${ts}] ${msg}`);
|
|
}
|
|
|
|
function err(msg) {
|
|
const ts = new Date().toISOString();
|
|
console.error(`[${ts}] ERROR: ${msg}`);
|
|
}
|
|
|
|
/**
|
|
* Split a SQL file into individual statements, stripping comments and blanks.
|
|
* Handles multi-line statements and PRAGMA directives.
|
|
*/
|
|
function splitStatements(sql) {
|
|
// Remove -- line comments
|
|
const stripped = sql.replace(/--[^\n]*/g, '');
|
|
// Split on semicolons, filter blank/whitespace-only entries
|
|
return stripped
|
|
.split(';')
|
|
.map(s => s.trim())
|
|
.filter(s => s.length > 0);
|
|
}
|
|
|
|
/**
|
|
* Detect the statement type for human-readable logging.
|
|
*/
|
|
function describeStatement(stmt) {
|
|
const upper = stmt.trimStart().toUpperCase();
|
|
if (upper.startsWith('CREATE TABLE IF NOT EXISTS')) {
|
|
const m = stmt.match(/CREATE TABLE IF NOT EXISTS\s+(\S+)/i);
|
|
return m ? `CREATE TABLE IF NOT EXISTS ${m[1]}` : 'CREATE TABLE (IF NOT EXISTS)';
|
|
}
|
|
if (upper.startsWith('CREATE INDEX IF NOT EXISTS')) {
|
|
const m = stmt.match(/CREATE INDEX IF NOT EXISTS\s+(\S+)/i);
|
|
return m ? `CREATE INDEX IF NOT EXISTS ${m[1]}` : 'CREATE INDEX (IF NOT EXISTS)';
|
|
}
|
|
if (upper.startsWith('CREATE')) {
|
|
return stmt.trimStart().split(/\s+/).slice(0, 4).join(' ');
|
|
}
|
|
if (upper.startsWith('INSERT OR IGNORE')) {
|
|
const m = stmt.match(/INTO\s+(\S+)/i);
|
|
return m ? `INSERT OR IGNORE INTO ${m[1]}` : 'INSERT OR IGNORE';
|
|
}
|
|
if (upper.startsWith('PRAGMA')) {
|
|
return stmt.trimStart().split('\n')[0].trim();
|
|
}
|
|
return stmt.trimStart().split(/\s+/).slice(0, 3).join(' ');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
log('TaskFlow init-db starting');
|
|
log(`Schema: ${schemaPath}`);
|
|
log(`DB: ${dbPath}`);
|
|
|
|
// Ensure memory directory exists
|
|
if (!existsSync(memoryDir)) {
|
|
mkdirSync(memoryDir, { recursive: true });
|
|
log(`Created directory: ${memoryDir}`);
|
|
} else {
|
|
log(`Directory exists: ${memoryDir}`);
|
|
}
|
|
|
|
// Read schema
|
|
let schemaSQL;
|
|
try {
|
|
schemaSQL = readFileSync(schemaPath, 'utf8');
|
|
} catch (e) {
|
|
err(`Cannot read schema file: ${schemaPath}\n${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Open (or create) DB
|
|
let db;
|
|
try {
|
|
db = new DatabaseSync(dbPath);
|
|
log(`Opened DB (created if new): ${dbPath}`);
|
|
} catch (e) {
|
|
err(`Cannot open SQLite DB at ${dbPath}\n${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Execute each statement
|
|
const statements = splitStatements(schemaSQL);
|
|
log(`Executing ${statements.length} SQL statement(s)...`);
|
|
console.log('');
|
|
|
|
let ok = 0;
|
|
let skipped = 0;
|
|
|
|
for (const stmt of statements) {
|
|
const label = describeStatement(stmt);
|
|
try {
|
|
db.exec(stmt);
|
|
log(` ✓ ${label}`);
|
|
ok++;
|
|
} catch (e) {
|
|
// PRAGMA errors on read-only pragmas are non-fatal; everything else is fatal.
|
|
if (stmt.trim().toUpperCase().startsWith('PRAGMA')) {
|
|
log(` ~ ${label} (skipped — ${e.message})`);
|
|
skipped++;
|
|
} else {
|
|
err(`Failed: ${label}\n ${e.message}`);
|
|
db.close?.();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify expected tables exist
|
|
const EXPECTED_TABLES = [
|
|
'projects',
|
|
'tasks_v2',
|
|
'task_transitions_v2',
|
|
'sync_state',
|
|
'legacy_key_map',
|
|
];
|
|
|
|
console.log('');
|
|
log('Verifying tables...');
|
|
|
|
const tableRows = db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
|
.all();
|
|
const existingTables = new Set(tableRows.map(r => r.name));
|
|
|
|
let allPresent = true;
|
|
for (const t of EXPECTED_TABLES) {
|
|
if (existingTables.has(t)) {
|
|
log(` ✓ table: ${t}`);
|
|
} else {
|
|
log(` ✗ MISSING table: ${t}`);
|
|
allPresent = false;
|
|
}
|
|
}
|
|
|
|
// Verify singleton sync_state row
|
|
const syncRow = db.prepare('SELECT id FROM sync_state WHERE id = 1').get();
|
|
if (syncRow) {
|
|
log(` ✓ sync_state singleton row present`);
|
|
} else {
|
|
log(` ✗ sync_state singleton row MISSING`);
|
|
allPresent = false;
|
|
}
|
|
|
|
// Summary
|
|
console.log('');
|
|
if (allPresent) {
|
|
log(`init-db complete — ${ok} statement(s) executed, ${skipped} skipped`);
|
|
log(`TaskFlow DB is ready at: ${dbPath}`);
|
|
} else {
|
|
err('init-db completed with warnings — some expected tables are missing');
|
|
process.exit(1);
|
|
}
|