@hasna/economy 0.2.29 → 0.2.31
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 +8 -8
- package/dist/cli/commands/tui.d.ts +1 -1
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/index.js +850 -187
- package/dist/db/database.d.ts +4 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/storage-adapter.d.ts +34 -0
- package/dist/db/storage-adapter.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1057 -54
- package/dist/ingest/billing.d.ts +1 -1
- package/dist/ingest/billing.d.ts.map +1 -1
- package/dist/ingest/claude-quota.d.ts +1 -1
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/cursor.d.ts +1 -1
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/otel.d.ts +1 -1
- package/dist/ingest/otel.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/plugin.d.ts +1 -1
- package/dist/ingest/plugin.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +1 -1
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/cloud-sync.d.ts +9 -2
- package/dist/lib/cloud-sync.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +3 -2
- package/dist/lib/open-projects.d.ts.map +1 -1
- package/dist/lib/peer-sync.d.ts +1 -1
- package/dist/lib/peer-sync.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/remote-storage.d.ts +15 -0
- package/dist/lib/remote-storage.d.ts.map +1 -0
- package/dist/lib/savings.d.ts +1 -1
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +1 -1
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/lib/storage-sync.d.ts +27 -0
- package/dist/lib/storage-sync.d.ts.map +1 -0
- package/dist/lib/sync-all.d.ts +1 -1
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +1 -0
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/index.js +518 -40
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +442 -15
- package/dist/server/index.js +510 -51
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CAyvBjC"}
|
package/dist/otel/index.js
CHANGED
|
@@ -15,7 +15,60 @@ var __export = (target, all) => {
|
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
// src/db/storage-adapter.ts
|
|
20
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
21
|
+
|
|
22
|
+
class SqliteAdapter {
|
|
23
|
+
db;
|
|
24
|
+
constructor(path) {
|
|
25
|
+
this.db = new BunDatabase(path, { create: true });
|
|
26
|
+
}
|
|
27
|
+
run(sql, ...params) {
|
|
28
|
+
const result = this.db.prepare(sql).run(...params);
|
|
29
|
+
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
30
|
+
}
|
|
31
|
+
get(sql, ...params) {
|
|
32
|
+
return this.db.prepare(sql).get(...params);
|
|
33
|
+
}
|
|
34
|
+
all(sql, ...params) {
|
|
35
|
+
return this.db.prepare(sql).all(...params);
|
|
36
|
+
}
|
|
37
|
+
exec(sql) {
|
|
38
|
+
this.db.exec(sql);
|
|
39
|
+
}
|
|
40
|
+
query(sql) {
|
|
41
|
+
return this.db.query(sql);
|
|
42
|
+
}
|
|
43
|
+
prepare(sql) {
|
|
44
|
+
const statement = this.db.prepare(sql);
|
|
45
|
+
return {
|
|
46
|
+
run(...params) {
|
|
47
|
+
const result = statement.run(...params);
|
|
48
|
+
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
49
|
+
},
|
|
50
|
+
get(...params) {
|
|
51
|
+
return statement.get(...params);
|
|
52
|
+
},
|
|
53
|
+
all(...params) {
|
|
54
|
+
return statement.all(...params);
|
|
55
|
+
},
|
|
56
|
+
finalize() {
|
|
57
|
+
statement.finalize();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
close() {
|
|
62
|
+
this.db.close();
|
|
63
|
+
}
|
|
64
|
+
transaction(fn) {
|
|
65
|
+
return this.db.transaction(fn)();
|
|
66
|
+
}
|
|
67
|
+
get raw() {
|
|
68
|
+
return this.db;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var init_storage_adapter = () => {};
|
|
19
72
|
|
|
20
73
|
// src/lib/pricing.ts
|
|
21
74
|
var exports_pricing = {};
|
|
@@ -513,7 +566,6 @@ var init_pricing = __esm(() => {
|
|
|
513
566
|
});
|
|
514
567
|
|
|
515
568
|
// src/db/database.ts
|
|
516
|
-
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
517
569
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
518
570
|
import { hostname } from "os";
|
|
519
571
|
import { homedir } from "os";
|
|
@@ -556,7 +608,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
556
608
|
if (dir && !existsSync(dir))
|
|
557
609
|
mkdirSync(dir, { recursive: true });
|
|
558
610
|
}
|
|
559
|
-
const db = new
|
|
611
|
+
const db = new SqliteAdapter(path);
|
|
560
612
|
db.exec("PRAGMA journal_mode = WAL");
|
|
561
613
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
562
614
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -869,7 +921,10 @@ function seedModelPricing(db, defaults) {
|
|
|
869
921
|
});
|
|
870
922
|
}
|
|
871
923
|
}
|
|
872
|
-
var init_database = () => {
|
|
924
|
+
var init_database = __esm(() => {
|
|
925
|
+
init_storage_adapter();
|
|
926
|
+
init_storage_adapter();
|
|
927
|
+
});
|
|
873
928
|
|
|
874
929
|
// src/db/pg-migrations.ts
|
|
875
930
|
var exports_pg_migrations = {};
|
|
@@ -1269,6 +1324,8 @@ async function ingestOtelRows(db, rows) {
|
|
|
1269
1324
|
|
|
1270
1325
|
// src/lib/cloud-sync.ts
|
|
1271
1326
|
init_database();
|
|
1327
|
+
import { homedir as homedir2, platform } from "os";
|
|
1328
|
+
import { dirname, join as join2 } from "path";
|
|
1272
1329
|
|
|
1273
1330
|
// src/lib/package-metadata.ts
|
|
1274
1331
|
import { readFileSync } from "fs";
|
|
@@ -1286,6 +1343,362 @@ function getPackageMetadata() {
|
|
|
1286
1343
|
}
|
|
1287
1344
|
var packageMetadata = getPackageMetadata();
|
|
1288
1345
|
|
|
1346
|
+
// src/lib/remote-storage.ts
|
|
1347
|
+
import pg from "pg";
|
|
1348
|
+
function translatePlaceholders(sql) {
|
|
1349
|
+
let index = 0;
|
|
1350
|
+
return sql.replace(/\?/g, () => `$${++index}`);
|
|
1351
|
+
}
|
|
1352
|
+
function normalizeParams(params) {
|
|
1353
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
1354
|
+
return flat.map((value) => value === undefined ? null : value);
|
|
1355
|
+
}
|
|
1356
|
+
function sslConfigFor(connectionString) {
|
|
1357
|
+
return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
class PgAdapterAsync {
|
|
1361
|
+
pool;
|
|
1362
|
+
constructor(source) {
|
|
1363
|
+
this.pool = typeof source === "string" ? new pg.Pool({ connectionString: source, ssl: sslConfigFor(source) }) : source;
|
|
1364
|
+
}
|
|
1365
|
+
async run(sql, ...params) {
|
|
1366
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1367
|
+
return { changes: result.rowCount ?? 0, lastInsertRowid: 0 };
|
|
1368
|
+
}
|
|
1369
|
+
async get(sql, ...params) {
|
|
1370
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1371
|
+
return result.rows[0] ?? null;
|
|
1372
|
+
}
|
|
1373
|
+
async all(sql, ...params) {
|
|
1374
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1375
|
+
return result.rows;
|
|
1376
|
+
}
|
|
1377
|
+
async exec(sql) {
|
|
1378
|
+
await this.pool.query(translatePlaceholders(sql));
|
|
1379
|
+
}
|
|
1380
|
+
async close() {
|
|
1381
|
+
await this.pool.end();
|
|
1382
|
+
}
|
|
1383
|
+
async transaction(fn) {
|
|
1384
|
+
const client = await this.pool.connect();
|
|
1385
|
+
try {
|
|
1386
|
+
await client.query("BEGIN");
|
|
1387
|
+
const result = await fn(client);
|
|
1388
|
+
await client.query("COMMIT");
|
|
1389
|
+
return result;
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
await client.query("ROLLBACK");
|
|
1392
|
+
throw error;
|
|
1393
|
+
} finally {
|
|
1394
|
+
client.release();
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
get raw() {
|
|
1398
|
+
return this.pool;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// src/lib/storage-sync.ts
|
|
1403
|
+
async function syncPush(local, remote, options) {
|
|
1404
|
+
const tables = await getTableOrder(remote, options.tables);
|
|
1405
|
+
return syncTransfer(local, remote, { ...options, tables }, "push");
|
|
1406
|
+
}
|
|
1407
|
+
function quoteIdent(identifier) {
|
|
1408
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1409
|
+
}
|
|
1410
|
+
async function getTableOrder(remote, tables) {
|
|
1411
|
+
if (tables.length <= 1)
|
|
1412
|
+
return tables;
|
|
1413
|
+
try {
|
|
1414
|
+
const rows = await remote.all(`
|
|
1415
|
+
SELECT DISTINCT
|
|
1416
|
+
tc.table_name AS source_table,
|
|
1417
|
+
ccu.table_name AS referenced_table
|
|
1418
|
+
FROM information_schema.table_constraints tc
|
|
1419
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
1420
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
1421
|
+
AND tc.table_schema = ccu.table_schema
|
|
1422
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
1423
|
+
AND tc.table_schema = 'public'
|
|
1424
|
+
`);
|
|
1425
|
+
if (rows.length > 0)
|
|
1426
|
+
return topoSort(tables, rows);
|
|
1427
|
+
} catch {}
|
|
1428
|
+
return tables;
|
|
1429
|
+
}
|
|
1430
|
+
function topoSort(tables, foreignKeys) {
|
|
1431
|
+
const allowed = new Set(tables);
|
|
1432
|
+
const deps = new Map;
|
|
1433
|
+
for (const table of tables)
|
|
1434
|
+
deps.set(table, new Set);
|
|
1435
|
+
for (const fk of foreignKeys) {
|
|
1436
|
+
if (allowed.has(fk.source_table) && allowed.has(fk.referenced_table)) {
|
|
1437
|
+
deps.get(fk.source_table)?.add(fk.referenced_table);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const sorted = [];
|
|
1441
|
+
const visited = new Set;
|
|
1442
|
+
const visiting = new Set;
|
|
1443
|
+
function visit(table) {
|
|
1444
|
+
if (visited.has(table))
|
|
1445
|
+
return;
|
|
1446
|
+
if (visiting.has(table)) {
|
|
1447
|
+
visited.add(table);
|
|
1448
|
+
sorted.push(table);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
visiting.add(table);
|
|
1452
|
+
for (const dep of deps.get(table) ?? [])
|
|
1453
|
+
visit(dep);
|
|
1454
|
+
visiting.delete(table);
|
|
1455
|
+
visited.add(table);
|
|
1456
|
+
sorted.push(table);
|
|
1457
|
+
}
|
|
1458
|
+
for (const table of tables)
|
|
1459
|
+
visit(table);
|
|
1460
|
+
return sorted;
|
|
1461
|
+
}
|
|
1462
|
+
async function resolvePrimaryKeys(source, target, table, option) {
|
|
1463
|
+
if (option)
|
|
1464
|
+
return Array.isArray(option) ? option : [option];
|
|
1465
|
+
const sourceKeys = await detectPrimaryKeys(source, table);
|
|
1466
|
+
if (sourceKeys.length > 0)
|
|
1467
|
+
return sourceKeys;
|
|
1468
|
+
return detectPrimaryKeys(target, table);
|
|
1469
|
+
}
|
|
1470
|
+
async function detectPrimaryKeys(adapter, table) {
|
|
1471
|
+
if (isAsyncAdapter(adapter)) {
|
|
1472
|
+
try {
|
|
1473
|
+
const rows = await adapter.all(`
|
|
1474
|
+
SELECT kcu.column_name, kcu.ordinal_position
|
|
1475
|
+
FROM information_schema.table_constraints tc
|
|
1476
|
+
JOIN information_schema.key_column_usage kcu
|
|
1477
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1478
|
+
AND tc.table_schema = kcu.table_schema
|
|
1479
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
1480
|
+
AND tc.table_schema = 'public'
|
|
1481
|
+
AND tc.table_name = ?
|
|
1482
|
+
ORDER BY kcu.ordinal_position
|
|
1483
|
+
`, table);
|
|
1484
|
+
return rows.map((row) => row.column_name);
|
|
1485
|
+
} catch {
|
|
1486
|
+
return [];
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
try {
|
|
1490
|
+
const rows = adapter.all(`PRAGMA table_info(${quoteIdent(table)})`);
|
|
1491
|
+
return rows.filter((row) => row.pk > 0).sort((a, b) => a.pk - b.pk).map((row) => row.name);
|
|
1492
|
+
} catch {
|
|
1493
|
+
return [];
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
async function ensureTablesExist(source, target, tables) {
|
|
1497
|
+
if (!isAsyncAdapter(source) || isAsyncAdapter(target))
|
|
1498
|
+
return;
|
|
1499
|
+
for (const table of tables)
|
|
1500
|
+
await ensureTableInSqliteFromPg(target, source, table);
|
|
1501
|
+
}
|
|
1502
|
+
async function ensureTableInSqliteFromPg(target, source, table) {
|
|
1503
|
+
const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
|
|
1504
|
+
if (existing.length > 0)
|
|
1505
|
+
return;
|
|
1506
|
+
const columns = await source.all(`
|
|
1507
|
+
SELECT column_name, data_type, is_nullable
|
|
1508
|
+
FROM information_schema.columns
|
|
1509
|
+
WHERE table_schema = 'public' AND table_name = ?
|
|
1510
|
+
ORDER BY ordinal_position
|
|
1511
|
+
`, table);
|
|
1512
|
+
if (columns.length === 0)
|
|
1513
|
+
return;
|
|
1514
|
+
const primaryKeys = new Set(await detectPrimaryKeys(source, table));
|
|
1515
|
+
const definitions = columns.filter((column) => !["tsvector", "tsquery"].includes(column.data_type.toLowerCase())).map((column) => {
|
|
1516
|
+
const type = pgTypeToSqlite(column.data_type);
|
|
1517
|
+
const notNull = column.is_nullable === "NO" && !primaryKeys.has(column.column_name) ? " NOT NULL" : "";
|
|
1518
|
+
return `${quoteIdent(column.column_name)} ${type}${notNull}`;
|
|
1519
|
+
});
|
|
1520
|
+
if (primaryKeys.size > 0) {
|
|
1521
|
+
definitions.push(`PRIMARY KEY (${[...primaryKeys].map(quoteIdent).join(", ")})`);
|
|
1522
|
+
}
|
|
1523
|
+
target.exec(`CREATE TABLE IF NOT EXISTS ${quoteIdent(table)} (${definitions.join(", ")})`);
|
|
1524
|
+
}
|
|
1525
|
+
function pgTypeToSqlite(pgType) {
|
|
1526
|
+
const type = pgType.toLowerCase();
|
|
1527
|
+
if (type.includes("int") || ["bigint", "smallint", "serial", "bigserial"].includes(type))
|
|
1528
|
+
return "INTEGER";
|
|
1529
|
+
if (type.includes("bool"))
|
|
1530
|
+
return "INTEGER";
|
|
1531
|
+
if (type.includes("float") || type.includes("double") || ["real", "numeric", "decimal"].includes(type))
|
|
1532
|
+
return "REAL";
|
|
1533
|
+
if (type === "bytea")
|
|
1534
|
+
return "BLOB";
|
|
1535
|
+
return "TEXT";
|
|
1536
|
+
}
|
|
1537
|
+
async function filterColumnsForTarget(target, table, columns) {
|
|
1538
|
+
if (columns.includes("machine_id") && table !== "machines")
|
|
1539
|
+
await ensureMachineIdColumnInTarget(target, table);
|
|
1540
|
+
try {
|
|
1541
|
+
if (isAsyncAdapter(target)) {
|
|
1542
|
+
const rows2 = await target.all(`
|
|
1543
|
+
SELECT column_name
|
|
1544
|
+
FROM information_schema.columns
|
|
1545
|
+
WHERE table_schema = 'public' AND table_name = ?
|
|
1546
|
+
`, table);
|
|
1547
|
+
if (rows2.length === 0)
|
|
1548
|
+
return columns;
|
|
1549
|
+
const targetColumns2 = new Set(rows2.map((row) => row.column_name));
|
|
1550
|
+
return columns.filter((column) => targetColumns2.has(column));
|
|
1551
|
+
}
|
|
1552
|
+
const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
|
|
1553
|
+
if (rows.length === 0)
|
|
1554
|
+
return columns;
|
|
1555
|
+
const targetColumns = new Set(rows.map((row) => row.name));
|
|
1556
|
+
return columns.filter((column) => targetColumns.has(column));
|
|
1557
|
+
} catch {
|
|
1558
|
+
return columns;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
async function ensureMachineIdColumnInTarget(target, table) {
|
|
1562
|
+
if (isAsyncAdapter(target)) {
|
|
1563
|
+
const rows2 = await target.all(`
|
|
1564
|
+
SELECT column_name
|
|
1565
|
+
FROM information_schema.columns
|
|
1566
|
+
WHERE table_schema = 'public' AND table_name = ? AND column_name = 'machine_id'
|
|
1567
|
+
`, table);
|
|
1568
|
+
if (rows2.length === 0)
|
|
1569
|
+
await target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
|
|
1573
|
+
if (!rows.some((row) => row.name === "machine_id")) {
|
|
1574
|
+
target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
async function syncTransfer(source, target, options, _direction) {
|
|
1578
|
+
const { tables, onProgress, batchSize = 100, conflictColumn = "updated_at", primaryKey } = options;
|
|
1579
|
+
const results = [];
|
|
1580
|
+
const sqliteTarget = isAsyncAdapter(target) ? null : target;
|
|
1581
|
+
await ensureTablesExist(source, target, tables);
|
|
1582
|
+
if (sqliteTarget) {
|
|
1583
|
+
try {
|
|
1584
|
+
sqliteTarget.exec("PRAGMA foreign_keys = OFF");
|
|
1585
|
+
} catch {}
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
for (let i = 0;i < tables.length; i++) {
|
|
1589
|
+
const table = tables[i];
|
|
1590
|
+
const result = { table, rowsRead: 0, rowsWritten: 0, rowsSkipped: 0, errors: [] };
|
|
1591
|
+
try {
|
|
1592
|
+
onProgress?.({ table, phase: "reading", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
|
|
1593
|
+
const rows = await readAll(source, `SELECT * FROM ${quoteIdent(table)}`);
|
|
1594
|
+
result.rowsRead = rows.length;
|
|
1595
|
+
if (rows.length === 0) {
|
|
1596
|
+
onProgress?.({ table, phase: "done", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
|
|
1597
|
+
results.push(result);
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const sourceColumns = Object.keys(rows[0]);
|
|
1601
|
+
const columns = await filterColumnsForTarget(target, table, sourceColumns);
|
|
1602
|
+
const primaryKeys = await resolvePrimaryKeys(source, target, table, primaryKey);
|
|
1603
|
+
if (primaryKeys.length === 0) {
|
|
1604
|
+
result.errors.push(`Table "${table}" has no primary key; inserted without conflict handling`);
|
|
1605
|
+
for (const batch of batches(rows, batchSize)) {
|
|
1606
|
+
await insertBatch(target, table, columns, batch);
|
|
1607
|
+
result.rowsWritten += batch.length;
|
|
1608
|
+
}
|
|
1609
|
+
results.push(result);
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
const missingKeys = primaryKeys.filter((key) => !columns.includes(key));
|
|
1613
|
+
if (missingKeys.length > 0) {
|
|
1614
|
+
result.errors.push(`Table "${table}" missing primary key column(s): ${missingKeys.join(", ")}`);
|
|
1615
|
+
results.push(result);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
|
|
1619
|
+
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
1620
|
+
const newestWinsColumn = columns.includes(conflictColumn) ? conflictColumn : undefined;
|
|
1621
|
+
for (const batch of batches(rows, batchSize)) {
|
|
1622
|
+
await upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, newestWinsColumn);
|
|
1623
|
+
result.rowsWritten += batch.length;
|
|
1624
|
+
onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
|
|
1625
|
+
}
|
|
1626
|
+
onProgress?.({ table, phase: "done", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
1629
|
+
}
|
|
1630
|
+
results.push(result);
|
|
1631
|
+
}
|
|
1632
|
+
} finally {
|
|
1633
|
+
if (sqliteTarget) {
|
|
1634
|
+
try {
|
|
1635
|
+
sqliteTarget.exec("PRAGMA foreign_keys = ON");
|
|
1636
|
+
} catch {}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return results;
|
|
1640
|
+
}
|
|
1641
|
+
function batches(rows, size) {
|
|
1642
|
+
const result = [];
|
|
1643
|
+
for (let offset = 0;offset < rows.length; offset += size)
|
|
1644
|
+
result.push(rows.slice(offset, offset + size));
|
|
1645
|
+
return result;
|
|
1646
|
+
}
|
|
1647
|
+
async function upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, conflictColumn) {
|
|
1648
|
+
if (batch.length === 0 || columns.length === 0)
|
|
1649
|
+
return;
|
|
1650
|
+
const fallbackKey = primaryKeys[0] ?? columns[0] ?? "id";
|
|
1651
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
1652
|
+
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
1653
|
+
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
|
|
1654
|
+
const whereClause = conflictColumn && updateColumns.includes(conflictColumn) ? ` WHERE ${quoteIdent(table)}.${quoteIdent(conflictColumn)} IS NULL OR EXCLUDED.${quoteIdent(conflictColumn)} >= ${quoteIdent(table)}.${quoteIdent(conflictColumn)}` : "";
|
|
1655
|
+
if (isAsyncAdapter(target)) {
|
|
1656
|
+
const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
|
|
1657
|
+
const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
|
|
1658
|
+
await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}
|
|
1659
|
+
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params2);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
1663
|
+
const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
|
|
1664
|
+
target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}
|
|
1665
|
+
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params);
|
|
1666
|
+
}
|
|
1667
|
+
async function insertBatch(target, table, columns, batch) {
|
|
1668
|
+
if (batch.length === 0 || columns.length === 0)
|
|
1669
|
+
return;
|
|
1670
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
1671
|
+
if (isAsyncAdapter(target)) {
|
|
1672
|
+
const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
|
|
1673
|
+
const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
|
|
1674
|
+
await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}`, ...params2);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
1678
|
+
const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
|
|
1679
|
+
target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}`, ...params);
|
|
1680
|
+
}
|
|
1681
|
+
function coerceForSqlite(value) {
|
|
1682
|
+
if (value === null || value === undefined)
|
|
1683
|
+
return null;
|
|
1684
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
1685
|
+
return value;
|
|
1686
|
+
if (value instanceof Date)
|
|
1687
|
+
return value.toISOString();
|
|
1688
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
1689
|
+
return value;
|
|
1690
|
+
if (typeof value === "object")
|
|
1691
|
+
return JSON.stringify(value);
|
|
1692
|
+
return String(value);
|
|
1693
|
+
}
|
|
1694
|
+
function isAsyncAdapter(adapter) {
|
|
1695
|
+
return adapter instanceof PgAdapterAsync;
|
|
1696
|
+
}
|
|
1697
|
+
async function readAll(adapter, sql) {
|
|
1698
|
+
const rows = adapter.all(sql);
|
|
1699
|
+
return rows instanceof Promise ? await rows : rows;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1289
1702
|
// src/lib/cloud-sync.ts
|
|
1290
1703
|
var CLOUD_TABLES = [
|
|
1291
1704
|
"requests",
|
|
@@ -1312,7 +1725,6 @@ async function getCloudPg() {
|
|
|
1312
1725
|
if (!url) {
|
|
1313
1726
|
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
1314
1727
|
}
|
|
1315
|
-
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
1316
1728
|
return new PgAdapterAsync(url);
|
|
1317
1729
|
}
|
|
1318
1730
|
async function runCloudMigrations(cloud) {
|
|
@@ -1322,17 +1734,19 @@ async function runCloudMigrations(cloud) {
|
|
|
1322
1734
|
}
|
|
1323
1735
|
}
|
|
1324
1736
|
async function cloudPush(opts) {
|
|
1325
|
-
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
1326
1737
|
const cloud = await getCloudPg();
|
|
1327
|
-
const local =
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1738
|
+
const local = openDatabase(getDbPath(), true);
|
|
1739
|
+
try {
|
|
1740
|
+
await runCloudMigrations(cloud);
|
|
1741
|
+
touchMachineRegistry(local, "push");
|
|
1742
|
+
const tables = resolveCloudTables(opts?.tables);
|
|
1743
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
1744
|
+
const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
|
|
1745
|
+
return { rows, machine: getMachineId() };
|
|
1746
|
+
} finally {
|
|
1747
|
+
local.close();
|
|
1748
|
+
await cloud.close();
|
|
1749
|
+
}
|
|
1336
1750
|
}
|
|
1337
1751
|
async function maybePushAfterIngest() {
|
|
1338
1752
|
if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
|
|
@@ -1359,6 +1773,19 @@ function touchMachineRegistry(db, direction) {
|
|
|
1359
1773
|
updated_at = excluded.updated_at
|
|
1360
1774
|
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
1361
1775
|
}
|
|
1776
|
+
function resolveCloudTables(tables) {
|
|
1777
|
+
if (!tables || tables.length === 0)
|
|
1778
|
+
return [...CLOUD_TABLES];
|
|
1779
|
+
const allowed = new Set(CLOUD_TABLES);
|
|
1780
|
+
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
1781
|
+
const invalid = requested.filter((table) => !allowed.has(table));
|
|
1782
|
+
if (invalid.length > 0) {
|
|
1783
|
+
throw new Error(`Unknown economy sync table(s): ${invalid.join(", ")}`);
|
|
1784
|
+
}
|
|
1785
|
+
return requested;
|
|
1786
|
+
}
|
|
1787
|
+
var SCHEDULE_CONFIG_DIR = join2(homedir2(), ".hasna", "economy");
|
|
1788
|
+
var SCHEDULE_CONFIG_PATH = join2(SCHEDULE_CONFIG_DIR, "cloud-sync-schedule.json");
|
|
1362
1789
|
|
|
1363
1790
|
// src/otel/index.ts
|
|
1364
1791
|
function resolvePort(argv) {
|