feat(lib-core): biblioteca atomica @pulse-libs/core v1.0.0-beta.1
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)
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
#!/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);
|
||||
}
|
||||
Reference in New Issue
Block a user