@hasna/todos 0.11.6 → 0.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -416
- package/dist/cli/index.js +422 -50
- package/dist/mcp/index.js +456 -84
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -10538,6 +10538,8 @@ import {
|
|
|
10538
10538
|
import { homedir as homedir3 } from "os";
|
|
10539
10539
|
import { join as join9, relative as relative2 } from "path";
|
|
10540
10540
|
import { hostname } from "os";
|
|
10541
|
+
import { homedir as homedir32 } from "os";
|
|
10542
|
+
import { join as join32 } from "path";
|
|
10541
10543
|
function __accessProp2(key) {
|
|
10542
10544
|
return this[key];
|
|
10543
10545
|
}
|
|
@@ -10605,6 +10607,9 @@ class SqliteAdapter {
|
|
|
10605
10607
|
exec(sql) {
|
|
10606
10608
|
this.db.exec(sql);
|
|
10607
10609
|
}
|
|
10610
|
+
query(sql) {
|
|
10611
|
+
return this.db.query(sql);
|
|
10612
|
+
}
|
|
10608
10613
|
prepare(sql) {
|
|
10609
10614
|
const stmt = this.db.prepare(sql);
|
|
10610
10615
|
return {
|
|
@@ -10762,6 +10767,62 @@ class PgAdapter {
|
|
|
10762
10767
|
return this.pool;
|
|
10763
10768
|
}
|
|
10764
10769
|
}
|
|
10770
|
+
|
|
10771
|
+
class PgAdapterAsync {
|
|
10772
|
+
pool;
|
|
10773
|
+
constructor(arg) {
|
|
10774
|
+
if (typeof arg === "string") {
|
|
10775
|
+
this.pool = new esm_default.Pool({ connectionString: arg });
|
|
10776
|
+
} else {
|
|
10777
|
+
this.pool = arg;
|
|
10778
|
+
}
|
|
10779
|
+
}
|
|
10780
|
+
async run(sql, ...params) {
|
|
10781
|
+
const pgSql = translateSql(sql, "pg");
|
|
10782
|
+
const pgParams = translateParams(params);
|
|
10783
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
10784
|
+
return {
|
|
10785
|
+
changes: res.rowCount ?? 0,
|
|
10786
|
+
lastInsertRowid: res.rows?.[0]?.id ?? 0
|
|
10787
|
+
};
|
|
10788
|
+
}
|
|
10789
|
+
async get(sql, ...params) {
|
|
10790
|
+
const pgSql = translateSql(sql, "pg");
|
|
10791
|
+
const pgParams = translateParams(params);
|
|
10792
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
10793
|
+
return res.rows[0] ?? null;
|
|
10794
|
+
}
|
|
10795
|
+
async all(sql, ...params) {
|
|
10796
|
+
const pgSql = translateSql(sql, "pg");
|
|
10797
|
+
const pgParams = translateParams(params);
|
|
10798
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
10799
|
+
return res.rows;
|
|
10800
|
+
}
|
|
10801
|
+
async exec(sql) {
|
|
10802
|
+
const pgSql = translateSql(sql, "pg");
|
|
10803
|
+
await this.pool.query(pgSql);
|
|
10804
|
+
}
|
|
10805
|
+
async close() {
|
|
10806
|
+
await this.pool.end();
|
|
10807
|
+
}
|
|
10808
|
+
async transaction(fn) {
|
|
10809
|
+
const client = await this.pool.connect();
|
|
10810
|
+
try {
|
|
10811
|
+
await client.query("BEGIN");
|
|
10812
|
+
const result = await fn(client);
|
|
10813
|
+
await client.query("COMMIT");
|
|
10814
|
+
return result;
|
|
10815
|
+
} catch (err) {
|
|
10816
|
+
await client.query("ROLLBACK");
|
|
10817
|
+
throw err;
|
|
10818
|
+
} finally {
|
|
10819
|
+
client.release();
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
get raw() {
|
|
10823
|
+
return this.pool;
|
|
10824
|
+
}
|
|
10825
|
+
}
|
|
10765
10826
|
function setErrorMap2(map) {
|
|
10766
10827
|
overrideErrorMap2 = map;
|
|
10767
10828
|
}
|
|
@@ -11373,19 +11434,133 @@ function createDatabase(options) {
|
|
|
11373
11434
|
const dbPath = options.sqlitePath ?? getDbPath2(options.service);
|
|
11374
11435
|
return new SqliteAdapter(dbPath);
|
|
11375
11436
|
}
|
|
11376
|
-
function syncPush(local,
|
|
11377
|
-
|
|
11437
|
+
async function syncPush(local, remote, options) {
|
|
11438
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
11439
|
+
return syncTransfer(local, remote, { ...options, tables: orderedTables }, "push");
|
|
11440
|
+
}
|
|
11441
|
+
async function syncPull(remote, local, options) {
|
|
11442
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
11443
|
+
return syncTransfer(remote, local, { ...options, tables: orderedTables }, "pull");
|
|
11444
|
+
}
|
|
11445
|
+
async function getTableOrder(remote, tables) {
|
|
11446
|
+
if (tables.length <= 1)
|
|
11447
|
+
return tables;
|
|
11448
|
+
try {
|
|
11449
|
+
const fks = await remote.all(`
|
|
11450
|
+
SELECT DISTINCT
|
|
11451
|
+
tc.table_name AS source_table,
|
|
11452
|
+
ccu.table_name AS referenced_table
|
|
11453
|
+
FROM information_schema.table_constraints tc
|
|
11454
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
11455
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
11456
|
+
AND tc.table_schema = ccu.table_schema
|
|
11457
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
11458
|
+
AND tc.table_schema = 'public'
|
|
11459
|
+
`);
|
|
11460
|
+
if (fks.length > 0) {
|
|
11461
|
+
return topoSort(tables, fks);
|
|
11462
|
+
}
|
|
11463
|
+
} catch {}
|
|
11464
|
+
return heuristicOrder(tables);
|
|
11465
|
+
}
|
|
11466
|
+
function topoSort(tables, fks) {
|
|
11467
|
+
const tableSet = new Set(tables);
|
|
11468
|
+
const deps = new Map;
|
|
11469
|
+
for (const t of tables) {
|
|
11470
|
+
deps.set(t, new Set);
|
|
11471
|
+
}
|
|
11472
|
+
for (const fk of fks) {
|
|
11473
|
+
if (tableSet.has(fk.source_table) && tableSet.has(fk.referenced_table)) {
|
|
11474
|
+
deps.get(fk.source_table).add(fk.referenced_table);
|
|
11475
|
+
}
|
|
11476
|
+
}
|
|
11477
|
+
const sorted = [];
|
|
11478
|
+
const visited = new Set;
|
|
11479
|
+
const visiting = new Set;
|
|
11480
|
+
function visit(table) {
|
|
11481
|
+
if (visited.has(table))
|
|
11482
|
+
return;
|
|
11483
|
+
if (visiting.has(table)) {
|
|
11484
|
+
sorted.push(table);
|
|
11485
|
+
visited.add(table);
|
|
11486
|
+
return;
|
|
11487
|
+
}
|
|
11488
|
+
visiting.add(table);
|
|
11489
|
+
const tableDeps = deps.get(table) ?? new Set;
|
|
11490
|
+
for (const dep of tableDeps) {
|
|
11491
|
+
visit(dep);
|
|
11492
|
+
}
|
|
11493
|
+
visiting.delete(table);
|
|
11494
|
+
visited.add(table);
|
|
11495
|
+
sorted.push(table);
|
|
11496
|
+
}
|
|
11497
|
+
for (const t of tables) {
|
|
11498
|
+
visit(t);
|
|
11499
|
+
}
|
|
11500
|
+
return sorted;
|
|
11501
|
+
}
|
|
11502
|
+
function heuristicOrder(tables) {
|
|
11503
|
+
const sorted = [...tables].sort((a, b) => {
|
|
11504
|
+
const aIsChild = a.includes("_") && tables.some((t) => a.startsWith(t + "_") || a.endsWith("_" + t));
|
|
11505
|
+
const bIsChild = b.includes("_") && tables.some((t) => b.startsWith(t + "_") || b.endsWith("_" + t));
|
|
11506
|
+
if (aIsChild && !bIsChild)
|
|
11507
|
+
return 1;
|
|
11508
|
+
if (!aIsChild && bIsChild)
|
|
11509
|
+
return -1;
|
|
11510
|
+
return a.localeCompare(b);
|
|
11511
|
+
});
|
|
11512
|
+
return sorted;
|
|
11513
|
+
}
|
|
11514
|
+
function getSqlitePrimaryKeys(adapter, table) {
|
|
11515
|
+
try {
|
|
11516
|
+
const cols = adapter.all(`PRAGMA table_info("${table}")`);
|
|
11517
|
+
const pkCols = cols.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
11518
|
+
return pkCols;
|
|
11519
|
+
} catch {
|
|
11520
|
+
return [];
|
|
11521
|
+
}
|
|
11378
11522
|
}
|
|
11379
|
-
function
|
|
11380
|
-
|
|
11523
|
+
async function getPgPrimaryKeys(adapter, table) {
|
|
11524
|
+
try {
|
|
11525
|
+
const rows = await adapter.all(`
|
|
11526
|
+
SELECT kcu.column_name, kcu.ordinal_position
|
|
11527
|
+
FROM information_schema.table_constraints tc
|
|
11528
|
+
JOIN information_schema.key_column_usage kcu
|
|
11529
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
11530
|
+
AND tc.table_schema = kcu.table_schema
|
|
11531
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
11532
|
+
AND tc.table_schema = 'public'
|
|
11533
|
+
AND tc.table_name = '${table}'
|
|
11534
|
+
ORDER BY kcu.ordinal_position
|
|
11535
|
+
`);
|
|
11536
|
+
return rows.map((r) => r.column_name);
|
|
11537
|
+
} catch {
|
|
11538
|
+
return [];
|
|
11539
|
+
}
|
|
11540
|
+
}
|
|
11541
|
+
async function detectPrimaryKeys(adapter, table) {
|
|
11542
|
+
if (isAsyncAdapter(adapter)) {
|
|
11543
|
+
return getPgPrimaryKeys(adapter, table);
|
|
11544
|
+
}
|
|
11545
|
+
return getSqlitePrimaryKeys(adapter, table);
|
|
11546
|
+
}
|
|
11547
|
+
async function resolvePrimaryKeys(source, target, table, pkOption) {
|
|
11548
|
+
if (pkOption) {
|
|
11549
|
+
return Array.isArray(pkOption) ? pkOption : [pkOption];
|
|
11550
|
+
}
|
|
11551
|
+
let pks = await detectPrimaryKeys(source, table);
|
|
11552
|
+
if (pks.length === 0) {
|
|
11553
|
+
pks = await detectPrimaryKeys(target, table);
|
|
11554
|
+
}
|
|
11555
|
+
return pks;
|
|
11381
11556
|
}
|
|
11382
|
-
function syncTransfer(source, target, options, _direction) {
|
|
11557
|
+
async function syncTransfer(source, target, options, _direction) {
|
|
11383
11558
|
const {
|
|
11384
11559
|
tables,
|
|
11385
11560
|
onProgress,
|
|
11386
|
-
batchSize =
|
|
11561
|
+
batchSize = 100,
|
|
11387
11562
|
conflictColumn = "updated_at",
|
|
11388
|
-
primaryKey
|
|
11563
|
+
primaryKey: pkOption
|
|
11389
11564
|
} = options;
|
|
11390
11565
|
const results = [];
|
|
11391
11566
|
for (let i = 0;i < tables.length; i++) {
|
|
@@ -11406,7 +11581,7 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
11406
11581
|
totalTables: tables.length,
|
|
11407
11582
|
currentTableIndex: i
|
|
11408
11583
|
});
|
|
11409
|
-
const rows = source
|
|
11584
|
+
const rows = await readAll(source, `SELECT * FROM "${table}"`);
|
|
11410
11585
|
result.rowsRead = rows.length;
|
|
11411
11586
|
if (rows.length === 0) {
|
|
11412
11587
|
onProgress?.({
|
|
@@ -11420,11 +11595,45 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
11420
11595
|
results.push(result);
|
|
11421
11596
|
continue;
|
|
11422
11597
|
}
|
|
11598
|
+
const pkColumns = await resolvePrimaryKeys(source, target, table, pkOption);
|
|
11423
11599
|
const columns = Object.keys(rows[0]);
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11600
|
+
if (pkColumns.length === 0) {
|
|
11601
|
+
result.errors.push(`Table "${table}" has no primary key \u2014 inserting without conflict handling`);
|
|
11602
|
+
onProgress?.({
|
|
11603
|
+
table,
|
|
11604
|
+
phase: "writing",
|
|
11605
|
+
rowsRead: result.rowsRead,
|
|
11606
|
+
rowsWritten: 0,
|
|
11607
|
+
totalTables: tables.length,
|
|
11608
|
+
currentTableIndex: i
|
|
11609
|
+
});
|
|
11610
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
11611
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
11612
|
+
try {
|
|
11613
|
+
if (isAsyncAdapter(target)) {
|
|
11614
|
+
await batchInsertPg(target, table, columns, batch);
|
|
11615
|
+
} else {
|
|
11616
|
+
batchInsertSqlite(target, table, columns, batch);
|
|
11617
|
+
}
|
|
11618
|
+
result.rowsWritten += batch.length;
|
|
11619
|
+
} catch (err) {
|
|
11620
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
11621
|
+
}
|
|
11622
|
+
}
|
|
11623
|
+
onProgress?.({
|
|
11624
|
+
table,
|
|
11625
|
+
phase: "done",
|
|
11626
|
+
rowsRead: result.rowsRead,
|
|
11627
|
+
rowsWritten: result.rowsWritten,
|
|
11628
|
+
totalTables: tables.length,
|
|
11629
|
+
currentTableIndex: i
|
|
11630
|
+
});
|
|
11631
|
+
results.push(result);
|
|
11632
|
+
continue;
|
|
11633
|
+
}
|
|
11634
|
+
const missingPks = pkColumns.filter((pk) => !columns.includes(pk));
|
|
11635
|
+
if (missingPks.length > 0) {
|
|
11636
|
+
result.errors.push(`Table "${table}" missing PK columns in data: ${missingPks.join(", ")} \u2014 skipping`);
|
|
11428
11637
|
results.push(result);
|
|
11429
11638
|
continue;
|
|
11430
11639
|
}
|
|
@@ -11436,34 +11645,18 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
11436
11645
|
totalTables: tables.length,
|
|
11437
11646
|
currentTableIndex: i
|
|
11438
11647
|
});
|
|
11648
|
+
const updateCols = columns.filter((c) => !pkColumns.includes(c));
|
|
11439
11649
|
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
11440
11650
|
const batch = rows.slice(offset, offset + batchSize);
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
const existingTime = new Date(existing[conflictColumn]).getTime();
|
|
11447
|
-
const incomingTime = new Date(row[conflictColumn]).getTime();
|
|
11448
|
-
if (existingTime >= incomingTime) {
|
|
11449
|
-
result.rowsSkipped++;
|
|
11450
|
-
continue;
|
|
11451
|
-
}
|
|
11452
|
-
}
|
|
11453
|
-
const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
|
|
11454
|
-
const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
|
|
11455
|
-
values.push(row[primaryKey]);
|
|
11456
|
-
target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
|
|
11457
|
-
} else {
|
|
11458
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
11459
|
-
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
11460
|
-
const values = columns.map((c) => row[c]);
|
|
11461
|
-
target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
|
|
11462
|
-
}
|
|
11463
|
-
result.rowsWritten++;
|
|
11464
|
-
} catch (err) {
|
|
11465
|
-
result.errors.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
|
|
11651
|
+
try {
|
|
11652
|
+
if (isAsyncAdapter(target)) {
|
|
11653
|
+
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
|
|
11654
|
+
} else {
|
|
11655
|
+
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
|
|
11466
11656
|
}
|
|
11657
|
+
result.rowsWritten += batch.length;
|
|
11658
|
+
} catch (err) {
|
|
11659
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
11467
11660
|
}
|
|
11468
11661
|
onProgress?.({
|
|
11469
11662
|
table,
|
|
@@ -11489,10 +11682,69 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
11489
11682
|
}
|
|
11490
11683
|
return results;
|
|
11491
11684
|
}
|
|
11685
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
|
|
11686
|
+
if (batch.length === 0)
|
|
11687
|
+
return;
|
|
11688
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
11689
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
11690
|
+
const offset = rowIdx * columns.length;
|
|
11691
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
11692
|
+
}).join(", ");
|
|
11693
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
11694
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
11695
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
11696
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
11697
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
11698
|
+
await target.run(sql, ...params);
|
|
11699
|
+
}
|
|
11700
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch) {
|
|
11701
|
+
if (batch.length === 0)
|
|
11702
|
+
return;
|
|
11703
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
11704
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
11705
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
11706
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
11707
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
11708
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
11709
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
11710
|
+
target.run(sql, ...params);
|
|
11711
|
+
}
|
|
11712
|
+
async function batchInsertPg(target, table, columns, batch) {
|
|
11713
|
+
if (batch.length === 0)
|
|
11714
|
+
return;
|
|
11715
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
11716
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
11717
|
+
const offset = rowIdx * columns.length;
|
|
11718
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
11719
|
+
}).join(", ");
|
|
11720
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
11721
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
11722
|
+
await target.run(sql, ...params);
|
|
11723
|
+
}
|
|
11724
|
+
function batchInsertSqlite(target, table, columns, batch) {
|
|
11725
|
+
if (batch.length === 0)
|
|
11726
|
+
return;
|
|
11727
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
11728
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
11729
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
11730
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
11731
|
+
target.run(sql, ...params);
|
|
11732
|
+
}
|
|
11733
|
+
function isAsyncAdapter(adapter) {
|
|
11734
|
+
return adapter.constructor.name === "PgAdapterAsync" || typeof adapter.raw?.connect === "function";
|
|
11735
|
+
}
|
|
11736
|
+
async function readAll(adapter, sql) {
|
|
11737
|
+
const result = adapter.all(sql);
|
|
11738
|
+
return result instanceof Promise ? await result : result;
|
|
11739
|
+
}
|
|
11492
11740
|
function listSqliteTables(db) {
|
|
11493
11741
|
const rows = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
11494
11742
|
return rows.map((r) => r.name);
|
|
11495
11743
|
}
|
|
11744
|
+
async function listPgTables(db) {
|
|
11745
|
+
const rows = await db.all(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename`);
|
|
11746
|
+
return rows.map((r) => r.tablename);
|
|
11747
|
+
}
|
|
11496
11748
|
function ensureFeedbackTable(db) {
|
|
11497
11749
|
db.exec(FEEDBACK_TABLE_SQL);
|
|
11498
11750
|
}
|
|
@@ -11545,6 +11797,126 @@ async function sendFeedback(feedback, db) {
|
|
|
11545
11797
|
return { sent: false, id, error: errorMsg };
|
|
11546
11798
|
}
|
|
11547
11799
|
}
|
|
11800
|
+
|
|
11801
|
+
class SyncProgressTracker {
|
|
11802
|
+
db;
|
|
11803
|
+
progress = new Map;
|
|
11804
|
+
startTimes = new Map;
|
|
11805
|
+
callback;
|
|
11806
|
+
constructor(db, callback) {
|
|
11807
|
+
this.db = db;
|
|
11808
|
+
this.callback = callback;
|
|
11809
|
+
this.ensureResumeTable();
|
|
11810
|
+
}
|
|
11811
|
+
ensureResumeTable() {
|
|
11812
|
+
this.db.exec(`
|
|
11813
|
+
CREATE TABLE IF NOT EXISTS _sync_resume (
|
|
11814
|
+
table_name TEXT PRIMARY KEY,
|
|
11815
|
+
last_row_id TEXT,
|
|
11816
|
+
direction TEXT,
|
|
11817
|
+
started_at TEXT,
|
|
11818
|
+
status TEXT DEFAULT 'in_progress'
|
|
11819
|
+
)
|
|
11820
|
+
`);
|
|
11821
|
+
}
|
|
11822
|
+
start(table, total, direction) {
|
|
11823
|
+
const resumed = this.canResume(table);
|
|
11824
|
+
const now2 = Date.now();
|
|
11825
|
+
this.startTimes.set(table, now2);
|
|
11826
|
+
const status = resumed ? "resumed" : "in_progress";
|
|
11827
|
+
const info = {
|
|
11828
|
+
table,
|
|
11829
|
+
total,
|
|
11830
|
+
done: 0,
|
|
11831
|
+
percent: 0,
|
|
11832
|
+
elapsed_ms: 0,
|
|
11833
|
+
eta_ms: 0,
|
|
11834
|
+
status
|
|
11835
|
+
};
|
|
11836
|
+
this.progress.set(table, info);
|
|
11837
|
+
this.db.run(`INSERT INTO _sync_resume (table_name, last_row_id, direction, started_at, status)
|
|
11838
|
+
VALUES (?, ?, ?, datetime('now'), ?)
|
|
11839
|
+
ON CONFLICT (table_name) DO UPDATE SET
|
|
11840
|
+
direction = excluded.direction,
|
|
11841
|
+
started_at = datetime('now'),
|
|
11842
|
+
status = excluded.status`, table, "", direction, status);
|
|
11843
|
+
this.notify(table);
|
|
11844
|
+
}
|
|
11845
|
+
update(table, done, lastRowId) {
|
|
11846
|
+
const info = this.progress.get(table);
|
|
11847
|
+
if (!info)
|
|
11848
|
+
return;
|
|
11849
|
+
const startTime = this.startTimes.get(table) ?? Date.now();
|
|
11850
|
+
const elapsed = Date.now() - startTime;
|
|
11851
|
+
const rate = done > 0 ? elapsed / done : 0;
|
|
11852
|
+
const remaining = info.total - done;
|
|
11853
|
+
const eta = remaining > 0 ? Math.round(rate * remaining) : 0;
|
|
11854
|
+
info.done = done;
|
|
11855
|
+
info.percent = info.total > 0 ? Math.round(done / info.total * 100) : 0;
|
|
11856
|
+
info.elapsed_ms = elapsed;
|
|
11857
|
+
info.eta_ms = eta;
|
|
11858
|
+
info.status = "in_progress";
|
|
11859
|
+
this.db.run(`UPDATE _sync_resume SET last_row_id = ?, status = 'in_progress' WHERE table_name = ?`, lastRowId, table);
|
|
11860
|
+
this.notify(table);
|
|
11861
|
+
}
|
|
11862
|
+
markComplete(table) {
|
|
11863
|
+
const info = this.progress.get(table);
|
|
11864
|
+
if (info) {
|
|
11865
|
+
const startTime = this.startTimes.get(table) ?? Date.now();
|
|
11866
|
+
info.elapsed_ms = Date.now() - startTime;
|
|
11867
|
+
info.done = info.total;
|
|
11868
|
+
info.percent = 100;
|
|
11869
|
+
info.eta_ms = 0;
|
|
11870
|
+
info.status = "completed";
|
|
11871
|
+
this.notify(table);
|
|
11872
|
+
}
|
|
11873
|
+
this.db.run(`UPDATE _sync_resume SET status = 'completed' WHERE table_name = ?`, table);
|
|
11874
|
+
}
|
|
11875
|
+
markFailed(table, _error) {
|
|
11876
|
+
const info = this.progress.get(table);
|
|
11877
|
+
if (info) {
|
|
11878
|
+
const startTime = this.startTimes.get(table) ?? Date.now();
|
|
11879
|
+
info.elapsed_ms = Date.now() - startTime;
|
|
11880
|
+
info.status = "failed";
|
|
11881
|
+
this.notify(table);
|
|
11882
|
+
}
|
|
11883
|
+
this.db.run(`UPDATE _sync_resume SET status = 'failed' WHERE table_name = ?`, table);
|
|
11884
|
+
}
|
|
11885
|
+
canResume(table) {
|
|
11886
|
+
const row = this.db.get(`SELECT status FROM _sync_resume WHERE table_name = ?`, table);
|
|
11887
|
+
if (!row)
|
|
11888
|
+
return false;
|
|
11889
|
+
return row.status === "in_progress" || row.status === "resumed";
|
|
11890
|
+
}
|
|
11891
|
+
getResumePoint(table) {
|
|
11892
|
+
const row = this.db.get(`SELECT table_name, last_row_id, direction, started_at, status FROM _sync_resume WHERE table_name = ?`, table);
|
|
11893
|
+
if (!row)
|
|
11894
|
+
return null;
|
|
11895
|
+
if (row.status !== "in_progress" && row.status !== "resumed")
|
|
11896
|
+
return null;
|
|
11897
|
+
return row;
|
|
11898
|
+
}
|
|
11899
|
+
clearResume(table) {
|
|
11900
|
+
this.db.run(`DELETE FROM _sync_resume WHERE table_name = ?`, table);
|
|
11901
|
+
this.progress.delete(table);
|
|
11902
|
+
this.startTimes.delete(table);
|
|
11903
|
+
}
|
|
11904
|
+
getProgress(table) {
|
|
11905
|
+
return this.progress.get(table) ?? null;
|
|
11906
|
+
}
|
|
11907
|
+
getAllProgress() {
|
|
11908
|
+
return Array.from(this.progress.values());
|
|
11909
|
+
}
|
|
11910
|
+
listResumeRecords() {
|
|
11911
|
+
return this.db.all(`SELECT table_name, last_row_id, direction, started_at, status FROM _sync_resume ORDER BY started_at DESC`);
|
|
11912
|
+
}
|
|
11913
|
+
notify(table) {
|
|
11914
|
+
const info = this.progress.get(table);
|
|
11915
|
+
if (info && this.callback) {
|
|
11916
|
+
this.callback({ ...info });
|
|
11917
|
+
}
|
|
11918
|
+
}
|
|
11919
|
+
}
|
|
11548
11920
|
function registerCloudTools(server, serviceName) {
|
|
11549
11921
|
server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
|
|
11550
11922
|
const config = getCloudConfig();
|
|
@@ -11555,10 +11927,10 @@ function registerCloudTools(server, serviceName) {
|
|
|
11555
11927
|
];
|
|
11556
11928
|
if (config.rds.host && config.rds.username) {
|
|
11557
11929
|
try {
|
|
11558
|
-
const pg2 = new
|
|
11559
|
-
pg2.get("SELECT 1 as ok");
|
|
11930
|
+
const pg2 = new PgAdapterAsync(getConnectionString("postgres"));
|
|
11931
|
+
await pg2.get("SELECT 1 as ok");
|
|
11560
11932
|
lines.push("PostgreSQL: connected");
|
|
11561
|
-
pg2.close();
|
|
11933
|
+
await pg2.close();
|
|
11562
11934
|
} catch (err) {
|
|
11563
11935
|
lines.push(`PostgreSQL: failed \u2014 ${err?.message}`);
|
|
11564
11936
|
}
|
|
@@ -11579,11 +11951,11 @@ function registerCloudTools(server, serviceName) {
|
|
|
11579
11951
|
};
|
|
11580
11952
|
}
|
|
11581
11953
|
const local = new SqliteAdapter(getDbPath2(serviceName));
|
|
11582
|
-
const cloud = new
|
|
11954
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
11583
11955
|
const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
|
|
11584
|
-
const results = syncPush(local, cloud, { tables: tableList });
|
|
11956
|
+
const results = await syncPush(local, cloud, { tables: tableList });
|
|
11585
11957
|
local.close();
|
|
11586
|
-
cloud.close();
|
|
11958
|
+
await cloud.close();
|
|
11587
11959
|
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
11588
11960
|
return {
|
|
11589
11961
|
content: [{ type: "text", text: `Pushed ${total} rows across ${tableList.length} table(s).` }]
|
|
@@ -11602,17 +11974,16 @@ function registerCloudTools(server, serviceName) {
|
|
|
11602
11974
|
};
|
|
11603
11975
|
}
|
|
11604
11976
|
const local = new SqliteAdapter(getDbPath2(serviceName));
|
|
11605
|
-
const cloud = new
|
|
11977
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
11606
11978
|
let tableList;
|
|
11607
11979
|
if (tablesStr) {
|
|
11608
11980
|
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
11609
11981
|
} else {
|
|
11610
11982
|
try {
|
|
11611
|
-
|
|
11612
|
-
tableList = rows.map((r) => r.tablename);
|
|
11983
|
+
tableList = await listPgTables(cloud);
|
|
11613
11984
|
} catch {
|
|
11614
11985
|
local.close();
|
|
11615
|
-
cloud.close();
|
|
11986
|
+
await cloud.close();
|
|
11616
11987
|
return {
|
|
11617
11988
|
content: [
|
|
11618
11989
|
{ type: "text", text: "Error: failed to list cloud tables." }
|
|
@@ -11621,9 +11992,9 @@ function registerCloudTools(server, serviceName) {
|
|
|
11621
11992
|
};
|
|
11622
11993
|
}
|
|
11623
11994
|
}
|
|
11624
|
-
const results = syncPull(
|
|
11995
|
+
const results = await syncPull(cloud, local, { tables: tableList });
|
|
11625
11996
|
local.close();
|
|
11626
|
-
cloud.close();
|
|
11997
|
+
await cloud.close();
|
|
11627
11998
|
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
11628
11999
|
return {
|
|
11629
12000
|
content: [{ type: "text", text: `Pulled ${total} rows across ${tableList.length} table(s).` }]
|
|
@@ -11898,7 +12269,7 @@ CREATE TABLE IF NOT EXISTS feedback (
|
|
|
11898
12269
|
email TEXT DEFAULT '',
|
|
11899
12270
|
machine_id TEXT DEFAULT '',
|
|
11900
12271
|
created_at TEXT DEFAULT (datetime('now'))
|
|
11901
|
-
)
|
|
12272
|
+
)`, AUTO_SYNC_CONFIG_PATH;
|
|
11902
12273
|
var init_dist = __esm(() => {
|
|
11903
12274
|
__create2 = Object.create;
|
|
11904
12275
|
__getProtoOf2 = Object.getPrototypeOf;
|
|
@@ -19841,6 +20212,7 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
19841
20212
|
});
|
|
19842
20213
|
CONFIG_DIR2 = join22(homedir22(), ".hasna", "cloud");
|
|
19843
20214
|
CONFIG_PATH2 = join22(CONFIG_DIR2, "config.json");
|
|
20215
|
+
AUTO_SYNC_CONFIG_PATH = join32(homedir32(), ".hasna", "cloud", "config.json");
|
|
19844
20216
|
});
|
|
19845
20217
|
|
|
19846
20218
|
// src/db/traces.ts
|