@hasna/cloud 0.1.7 → 0.1.9
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/dist/cli/index.js +455 -7
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +176 -1
- package/dist/mcp/index.js +18 -2
- package/dist/scheduled-sync.d.ts +24 -0
- package/dist/scheduled-sync.d.ts.map +1 -0
- package/dist/scheduled-sync.js +9339 -0
- package/dist/sync-schedule.d.ts +42 -0
- package/dist/sync-schedule.d.ts.map +1 -0
- package/dist/sync.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/cli/index.js
CHANGED
|
@@ -11187,6 +11187,62 @@ class PgAdapter {
|
|
|
11187
11187
|
}
|
|
11188
11188
|
}
|
|
11189
11189
|
|
|
11190
|
+
class PgAdapterAsync {
|
|
11191
|
+
pool;
|
|
11192
|
+
constructor(arg) {
|
|
11193
|
+
if (typeof arg === "string") {
|
|
11194
|
+
this.pool = new esm_default.Pool({ connectionString: arg });
|
|
11195
|
+
} else {
|
|
11196
|
+
this.pool = arg;
|
|
11197
|
+
}
|
|
11198
|
+
}
|
|
11199
|
+
async run(sql, ...params) {
|
|
11200
|
+
const pgSql = translateSql(sql, "pg");
|
|
11201
|
+
const pgParams = translateParams(params);
|
|
11202
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
11203
|
+
return {
|
|
11204
|
+
changes: res.rowCount ?? 0,
|
|
11205
|
+
lastInsertRowid: res.rows?.[0]?.id ?? 0
|
|
11206
|
+
};
|
|
11207
|
+
}
|
|
11208
|
+
async get(sql, ...params) {
|
|
11209
|
+
const pgSql = translateSql(sql, "pg");
|
|
11210
|
+
const pgParams = translateParams(params);
|
|
11211
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
11212
|
+
return res.rows[0] ?? null;
|
|
11213
|
+
}
|
|
11214
|
+
async all(sql, ...params) {
|
|
11215
|
+
const pgSql = translateSql(sql, "pg");
|
|
11216
|
+
const pgParams = translateParams(params);
|
|
11217
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
11218
|
+
return res.rows;
|
|
11219
|
+
}
|
|
11220
|
+
async exec(sql) {
|
|
11221
|
+
const pgSql = translateSql(sql, "pg");
|
|
11222
|
+
await this.pool.query(pgSql);
|
|
11223
|
+
}
|
|
11224
|
+
async close() {
|
|
11225
|
+
await this.pool.end();
|
|
11226
|
+
}
|
|
11227
|
+
async transaction(fn) {
|
|
11228
|
+
const client = await this.pool.connect();
|
|
11229
|
+
try {
|
|
11230
|
+
await client.query("BEGIN");
|
|
11231
|
+
const result = await fn(client);
|
|
11232
|
+
await client.query("COMMIT");
|
|
11233
|
+
return result;
|
|
11234
|
+
} catch (err) {
|
|
11235
|
+
await client.query("ROLLBACK");
|
|
11236
|
+
throw err;
|
|
11237
|
+
} finally {
|
|
11238
|
+
client.release();
|
|
11239
|
+
}
|
|
11240
|
+
}
|
|
11241
|
+
get raw() {
|
|
11242
|
+
return this.pool;
|
|
11243
|
+
}
|
|
11244
|
+
}
|
|
11245
|
+
|
|
11190
11246
|
// src/dotfile.ts
|
|
11191
11247
|
import {
|
|
11192
11248
|
existsSync,
|
|
@@ -11206,6 +11262,10 @@ function getDbPath(serviceName) {
|
|
|
11206
11262
|
const dir = getDataDir(serviceName);
|
|
11207
11263
|
return join(dir, `${serviceName}.db`);
|
|
11208
11264
|
}
|
|
11265
|
+
function getHasnaDir() {
|
|
11266
|
+
mkdirSync(HASNA_DIR, { recursive: true });
|
|
11267
|
+
return HASNA_DIR;
|
|
11268
|
+
}
|
|
11209
11269
|
|
|
11210
11270
|
// src/config.ts
|
|
11211
11271
|
var CloudConfigSchema = exports_external.object({
|
|
@@ -11218,7 +11278,10 @@ var CloudConfigSchema = exports_external.object({
|
|
|
11218
11278
|
}).default({}),
|
|
11219
11279
|
mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
|
|
11220
11280
|
auto_sync_interval_minutes: exports_external.number().default(0),
|
|
11221
|
-
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
|
|
11281
|
+
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
|
|
11282
|
+
sync: exports_external.object({
|
|
11283
|
+
schedule_minutes: exports_external.number().default(0)
|
|
11284
|
+
}).default({})
|
|
11222
11285
|
});
|
|
11223
11286
|
var CONFIG_DIR = join2(homedir2(), ".hasna", "cloud");
|
|
11224
11287
|
var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
|
|
@@ -11389,6 +11452,11 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
11389
11452
|
primaryKey: pkOption
|
|
11390
11453
|
} = options;
|
|
11391
11454
|
const results = [];
|
|
11455
|
+
if (!isAsyncAdapter(target)) {
|
|
11456
|
+
try {
|
|
11457
|
+
target.run("PRAGMA foreign_keys = OFF");
|
|
11458
|
+
} catch {}
|
|
11459
|
+
}
|
|
11392
11460
|
for (let i = 0;i < tables.length; i++) {
|
|
11393
11461
|
const table = tables[i];
|
|
11394
11462
|
const result = {
|
|
@@ -11506,6 +11574,11 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
11506
11574
|
}
|
|
11507
11575
|
results.push(result);
|
|
11508
11576
|
}
|
|
11577
|
+
if (!isAsyncAdapter(target)) {
|
|
11578
|
+
try {
|
|
11579
|
+
target.run("PRAGMA foreign_keys = ON");
|
|
11580
|
+
} catch {}
|
|
11581
|
+
}
|
|
11509
11582
|
return results;
|
|
11510
11583
|
}
|
|
11511
11584
|
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
|
|
@@ -11589,7 +11662,10 @@ var CloudConfigSchema2 = exports_external.object({
|
|
|
11589
11662
|
}).default({}),
|
|
11590
11663
|
mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
|
|
11591
11664
|
auto_sync_interval_minutes: exports_external.number().default(0),
|
|
11592
|
-
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
|
|
11665
|
+
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
|
|
11666
|
+
sync: exports_external.object({
|
|
11667
|
+
schedule_minutes: exports_external.number().default(0)
|
|
11668
|
+
}).default({})
|
|
11593
11669
|
});
|
|
11594
11670
|
var CONFIG_DIR2 = join3(homedir3(), ".hasna", "cloud");
|
|
11595
11671
|
var CONFIG_PATH2 = join3(CONFIG_DIR2, "config.json");
|
|
@@ -11604,6 +11680,21 @@ function getCloudConfig2() {
|
|
|
11604
11680
|
return CloudConfigSchema2.parse({});
|
|
11605
11681
|
}
|
|
11606
11682
|
}
|
|
11683
|
+
function saveCloudConfig2(config) {
|
|
11684
|
+
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
11685
|
+
writeFileSync2(CONFIG_PATH2, JSON.stringify(config, null, 2) + `
|
|
11686
|
+
`, "utf-8");
|
|
11687
|
+
}
|
|
11688
|
+
function getConnectionString2(dbName) {
|
|
11689
|
+
const config = getCloudConfig2();
|
|
11690
|
+
const { host, port, username, password_env, ssl } = config.rds;
|
|
11691
|
+
if (!host || !username) {
|
|
11692
|
+
throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
|
|
11693
|
+
}
|
|
11694
|
+
const password = process.env[password_env] ?? "";
|
|
11695
|
+
const sslParam = ssl ? "?sslmode=require" : "";
|
|
11696
|
+
return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
|
|
11697
|
+
}
|
|
11607
11698
|
|
|
11608
11699
|
// src/feedback.ts
|
|
11609
11700
|
var FEEDBACK_TABLE_SQL = `
|
|
@@ -11775,7 +11866,7 @@ class SqliteAdapter2 {
|
|
|
11775
11866
|
return this.db;
|
|
11776
11867
|
}
|
|
11777
11868
|
}
|
|
11778
|
-
class
|
|
11869
|
+
class PgAdapterAsync2 {
|
|
11779
11870
|
pool;
|
|
11780
11871
|
constructor(arg) {
|
|
11781
11872
|
if (typeof arg === "string") {
|
|
@@ -11831,9 +11922,295 @@ class PgAdapterAsync {
|
|
|
11831
11922
|
}
|
|
11832
11923
|
}
|
|
11833
11924
|
|
|
11925
|
+
// src/sync-schedule.ts
|
|
11926
|
+
import { join as join5, dirname } from "path";
|
|
11927
|
+
var CRON_TITLE = "hasna-cloud-sync";
|
|
11928
|
+
function getWorkerPath() {
|
|
11929
|
+
const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
|
|
11930
|
+
const tsPath = join5(dir, "scheduled-sync.ts");
|
|
11931
|
+
const jsPath = join5(dir, "scheduled-sync.js");
|
|
11932
|
+
try {
|
|
11933
|
+
const { existsSync: existsSync5 } = __require("fs");
|
|
11934
|
+
if (existsSync5(tsPath))
|
|
11935
|
+
return tsPath;
|
|
11936
|
+
} catch {}
|
|
11937
|
+
return jsPath;
|
|
11938
|
+
}
|
|
11939
|
+
function parseInterval(input) {
|
|
11940
|
+
const trimmed = input.trim().toLowerCase();
|
|
11941
|
+
const hourMatch = trimmed.match(/^(\d+)\s*h$/);
|
|
11942
|
+
if (hourMatch) {
|
|
11943
|
+
const hours = parseInt(hourMatch[1], 10);
|
|
11944
|
+
if (hours <= 0) {
|
|
11945
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
11946
|
+
}
|
|
11947
|
+
return hours * 60;
|
|
11948
|
+
}
|
|
11949
|
+
const minMatch = trimmed.match(/^(\d+)\s*m$/);
|
|
11950
|
+
if (minMatch) {
|
|
11951
|
+
const mins = parseInt(minMatch[1], 10);
|
|
11952
|
+
if (mins <= 0) {
|
|
11953
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
11954
|
+
}
|
|
11955
|
+
return mins;
|
|
11956
|
+
}
|
|
11957
|
+
const plain = parseInt(trimmed, 10);
|
|
11958
|
+
if (!isNaN(plain) && plain > 0) {
|
|
11959
|
+
return plain;
|
|
11960
|
+
}
|
|
11961
|
+
throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
|
|
11962
|
+
}
|
|
11963
|
+
function minutesToCron(minutes) {
|
|
11964
|
+
if (minutes <= 0) {
|
|
11965
|
+
throw new Error("Interval must be greater than 0 minutes.");
|
|
11966
|
+
}
|
|
11967
|
+
if (minutes < 60) {
|
|
11968
|
+
return `*/${minutes} * * * *`;
|
|
11969
|
+
}
|
|
11970
|
+
const hours = Math.floor(minutes / 60);
|
|
11971
|
+
const remainderMins = minutes % 60;
|
|
11972
|
+
if (remainderMins === 0 && hours <= 24) {
|
|
11973
|
+
return `0 */${hours} * * *`;
|
|
11974
|
+
}
|
|
11975
|
+
return `*/${minutes} * * * *`;
|
|
11976
|
+
}
|
|
11977
|
+
async function registerSyncSchedule(intervalMinutes) {
|
|
11978
|
+
if (intervalMinutes <= 0) {
|
|
11979
|
+
throw new Error("Interval must be a positive number of minutes.");
|
|
11980
|
+
}
|
|
11981
|
+
const cronExpr = minutesToCron(intervalMinutes);
|
|
11982
|
+
const workerPath = getWorkerPath();
|
|
11983
|
+
await Bun.cron(workerPath, cronExpr, CRON_TITLE);
|
|
11984
|
+
const config = getCloudConfig2();
|
|
11985
|
+
config.sync.schedule_minutes = intervalMinutes;
|
|
11986
|
+
saveCloudConfig2(config);
|
|
11987
|
+
}
|
|
11988
|
+
async function removeSyncSchedule() {
|
|
11989
|
+
await Bun.cron.remove(CRON_TITLE);
|
|
11990
|
+
const config = getCloudConfig2();
|
|
11991
|
+
config.sync.schedule_minutes = 0;
|
|
11992
|
+
saveCloudConfig2(config);
|
|
11993
|
+
}
|
|
11994
|
+
function getSyncScheduleStatus() {
|
|
11995
|
+
const config = getCloudConfig2();
|
|
11996
|
+
const minutes = config.sync.schedule_minutes;
|
|
11997
|
+
const registered = minutes > 0;
|
|
11998
|
+
return {
|
|
11999
|
+
registered,
|
|
12000
|
+
schedule_minutes: minutes,
|
|
12001
|
+
cron_expression: registered ? minutesToCron(minutes) : null
|
|
12002
|
+
};
|
|
12003
|
+
}
|
|
12004
|
+
|
|
12005
|
+
// src/scheduled-sync.ts
|
|
12006
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
|
|
12007
|
+
import { join as join6 } from "path";
|
|
12008
|
+
|
|
12009
|
+
// src/sync-incremental.ts
|
|
12010
|
+
var SYNC_META_TABLE_SQL = `
|
|
12011
|
+
CREATE TABLE IF NOT EXISTS _sync_meta (
|
|
12012
|
+
table_name TEXT PRIMARY KEY,
|
|
12013
|
+
last_synced_at TEXT,
|
|
12014
|
+
last_synced_row_count INTEGER DEFAULT 0,
|
|
12015
|
+
direction TEXT DEFAULT 'push'
|
|
12016
|
+
)`;
|
|
12017
|
+
function ensureSyncMetaTable(db) {
|
|
12018
|
+
db.exec(SYNC_META_TABLE_SQL);
|
|
12019
|
+
}
|
|
12020
|
+
function getSyncMeta(db, table) {
|
|
12021
|
+
ensureSyncMetaTable(db);
|
|
12022
|
+
return db.get(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta WHERE table_name = ?`, table) ?? null;
|
|
12023
|
+
}
|
|
12024
|
+
function upsertSyncMeta(db, meta) {
|
|
12025
|
+
ensureSyncMetaTable(db);
|
|
12026
|
+
const existing = db.get(`SELECT table_name FROM _sync_meta WHERE table_name = ?`, meta.table_name);
|
|
12027
|
+
if (existing) {
|
|
12028
|
+
db.run(`UPDATE _sync_meta SET last_synced_at = ?, last_synced_row_count = ?, direction = ? WHERE table_name = ?`, meta.last_synced_at, meta.last_synced_row_count, meta.direction, meta.table_name);
|
|
12029
|
+
} else {
|
|
12030
|
+
db.run(`INSERT INTO _sync_meta (table_name, last_synced_at, last_synced_row_count, direction) VALUES (?, ?, ?, ?)`, meta.table_name, meta.last_synced_at, meta.last_synced_row_count, meta.direction);
|
|
12031
|
+
}
|
|
12032
|
+
}
|
|
12033
|
+
function transferRows(source, target, table, rows, options) {
|
|
12034
|
+
const { primaryKey = "id", conflictColumn = "updated_at" } = options;
|
|
12035
|
+
let written = 0;
|
|
12036
|
+
let skipped = 0;
|
|
12037
|
+
const errors2 = [];
|
|
12038
|
+
if (rows.length === 0)
|
|
12039
|
+
return { written, skipped, errors: errors2 };
|
|
12040
|
+
const columns = Object.keys(rows[0]);
|
|
12041
|
+
const hasConflictCol = columns.includes(conflictColumn);
|
|
12042
|
+
const hasPrimaryKey = columns.includes(primaryKey);
|
|
12043
|
+
if (!hasPrimaryKey) {
|
|
12044
|
+
errors2.push(`Table "${table}" has no "${primaryKey}" column -- skipping`);
|
|
12045
|
+
return { written, skipped, errors: errors2 };
|
|
12046
|
+
}
|
|
12047
|
+
for (const row of rows) {
|
|
12048
|
+
try {
|
|
12049
|
+
const existing = target.get(`SELECT "${primaryKey}"${hasConflictCol ? `, "${conflictColumn}"` : ""} FROM "${table}" WHERE "${primaryKey}" = ?`, row[primaryKey]);
|
|
12050
|
+
if (existing) {
|
|
12051
|
+
if (hasConflictCol && existing[conflictColumn] && row[conflictColumn]) {
|
|
12052
|
+
const existingTime = new Date(existing[conflictColumn]).getTime();
|
|
12053
|
+
const incomingTime = new Date(row[conflictColumn]).getTime();
|
|
12054
|
+
if (existingTime >= incomingTime) {
|
|
12055
|
+
skipped++;
|
|
12056
|
+
continue;
|
|
12057
|
+
}
|
|
12058
|
+
}
|
|
12059
|
+
const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
|
|
12060
|
+
const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
|
|
12061
|
+
values.push(row[primaryKey]);
|
|
12062
|
+
target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
|
|
12063
|
+
} else {
|
|
12064
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
12065
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
12066
|
+
const values = columns.map((c) => row[c]);
|
|
12067
|
+
target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
|
|
12068
|
+
}
|
|
12069
|
+
written++;
|
|
12070
|
+
} catch (err) {
|
|
12071
|
+
errors2.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
|
|
12072
|
+
}
|
|
12073
|
+
}
|
|
12074
|
+
return { written, skipped, errors: errors2 };
|
|
12075
|
+
}
|
|
12076
|
+
function incrementalSyncPush(local, remote, tables, options = {}) {
|
|
12077
|
+
const { conflictColumn = "updated_at", batchSize = 500 } = options;
|
|
12078
|
+
const results = [];
|
|
12079
|
+
ensureSyncMetaTable(local);
|
|
12080
|
+
for (const table of tables) {
|
|
12081
|
+
const stat = {
|
|
12082
|
+
table,
|
|
12083
|
+
total_rows: 0,
|
|
12084
|
+
synced_rows: 0,
|
|
12085
|
+
skipped_rows: 0,
|
|
12086
|
+
errors: [],
|
|
12087
|
+
first_sync: false
|
|
12088
|
+
};
|
|
12089
|
+
try {
|
|
12090
|
+
const countResult = local.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
|
|
12091
|
+
stat.total_rows = countResult?.cnt ?? 0;
|
|
12092
|
+
const meta = getSyncMeta(local, table);
|
|
12093
|
+
let rows;
|
|
12094
|
+
if (meta?.last_synced_at) {
|
|
12095
|
+
try {
|
|
12096
|
+
rows = local.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
|
|
12097
|
+
} catch {
|
|
12098
|
+
rows = local.all(`SELECT * FROM "${table}"`);
|
|
12099
|
+
stat.first_sync = true;
|
|
12100
|
+
}
|
|
12101
|
+
} else {
|
|
12102
|
+
rows = local.all(`SELECT * FROM "${table}"`);
|
|
12103
|
+
stat.first_sync = true;
|
|
12104
|
+
}
|
|
12105
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
12106
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
12107
|
+
const result = transferRows(local, remote, table, batch, options);
|
|
12108
|
+
stat.synced_rows += result.written;
|
|
12109
|
+
stat.skipped_rows += result.skipped;
|
|
12110
|
+
stat.errors.push(...result.errors);
|
|
12111
|
+
}
|
|
12112
|
+
if (rows.length === 0) {
|
|
12113
|
+
stat.skipped_rows = stat.total_rows;
|
|
12114
|
+
}
|
|
12115
|
+
const now = new Date().toISOString();
|
|
12116
|
+
upsertSyncMeta(local, {
|
|
12117
|
+
table_name: table,
|
|
12118
|
+
last_synced_at: now,
|
|
12119
|
+
last_synced_row_count: stat.synced_rows,
|
|
12120
|
+
direction: "push"
|
|
12121
|
+
});
|
|
12122
|
+
} catch (err) {
|
|
12123
|
+
stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
|
|
12124
|
+
}
|
|
12125
|
+
results.push(stat);
|
|
12126
|
+
}
|
|
12127
|
+
return results;
|
|
12128
|
+
}
|
|
12129
|
+
|
|
12130
|
+
// src/sync.ts
|
|
12131
|
+
function listSqliteTables2(db) {
|
|
12132
|
+
const rows = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
12133
|
+
return rows.map((r) => r.name);
|
|
12134
|
+
}
|
|
12135
|
+
|
|
12136
|
+
// src/scheduled-sync.ts
|
|
12137
|
+
function discoverSyncableServices() {
|
|
12138
|
+
const hasnaDir = getHasnaDir();
|
|
12139
|
+
const services = [];
|
|
12140
|
+
try {
|
|
12141
|
+
const entries = readdirSync3(hasnaDir, { withFileTypes: true });
|
|
12142
|
+
for (const entry of entries) {
|
|
12143
|
+
if (!entry.isDirectory())
|
|
12144
|
+
continue;
|
|
12145
|
+
const dbPath = join6(hasnaDir, entry.name, `${entry.name}.db`);
|
|
12146
|
+
if (existsSync5(dbPath)) {
|
|
12147
|
+
services.push(entry.name);
|
|
12148
|
+
}
|
|
12149
|
+
}
|
|
12150
|
+
} catch {}
|
|
12151
|
+
return services;
|
|
12152
|
+
}
|
|
12153
|
+
async function runScheduledSync() {
|
|
12154
|
+
const config = getCloudConfig2();
|
|
12155
|
+
if (config.mode === "local")
|
|
12156
|
+
return [];
|
|
12157
|
+
const services = discoverSyncableServices();
|
|
12158
|
+
const results = [];
|
|
12159
|
+
let remote = null;
|
|
12160
|
+
for (const service of services) {
|
|
12161
|
+
const result = {
|
|
12162
|
+
service,
|
|
12163
|
+
tables_synced: 0,
|
|
12164
|
+
total_rows_synced: 0,
|
|
12165
|
+
errors: []
|
|
12166
|
+
};
|
|
12167
|
+
try {
|
|
12168
|
+
const dbPath = join6(getDataDir(service), `${service}.db`);
|
|
12169
|
+
if (!existsSync5(dbPath)) {
|
|
12170
|
+
continue;
|
|
12171
|
+
}
|
|
12172
|
+
const local = new SqliteAdapter(dbPath);
|
|
12173
|
+
const tables = listSqliteTables2(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
|
|
12174
|
+
if (tables.length === 0) {
|
|
12175
|
+
local.close();
|
|
12176
|
+
continue;
|
|
12177
|
+
}
|
|
12178
|
+
try {
|
|
12179
|
+
const connStr = getConnectionString2(service);
|
|
12180
|
+
remote = new PgAdapterAsync(connStr);
|
|
12181
|
+
} catch (err) {
|
|
12182
|
+
result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
|
|
12183
|
+
local.close();
|
|
12184
|
+
results.push(result);
|
|
12185
|
+
continue;
|
|
12186
|
+
}
|
|
12187
|
+
const stats = incrementalSyncPush(local, remote, tables);
|
|
12188
|
+
for (const s of stats) {
|
|
12189
|
+
if (s.errors.length === 0) {
|
|
12190
|
+
result.tables_synced++;
|
|
12191
|
+
}
|
|
12192
|
+
result.total_rows_synced += s.synced_rows;
|
|
12193
|
+
result.errors.push(...s.errors);
|
|
12194
|
+
}
|
|
12195
|
+
local.close();
|
|
12196
|
+
await remote.close();
|
|
12197
|
+
remote = null;
|
|
12198
|
+
} catch (err) {
|
|
12199
|
+
result.errors.push(err?.message ?? String(err));
|
|
12200
|
+
}
|
|
12201
|
+
results.push(result);
|
|
12202
|
+
}
|
|
12203
|
+
if (remote) {
|
|
12204
|
+
try {
|
|
12205
|
+
await remote.close();
|
|
12206
|
+
} catch {}
|
|
12207
|
+
}
|
|
12208
|
+
return results;
|
|
12209
|
+
}
|
|
12210
|
+
|
|
11834
12211
|
// src/cli/index.ts
|
|
11835
12212
|
var program2 = new Command;
|
|
11836
|
-
program2.name("cloud").description("Shared cloud infrastructure \u2014 database adapter, sync engine, feedback, dotfile migration").version("0.1.
|
|
12213
|
+
program2.name("cloud").description("Shared cloud infrastructure \u2014 database adapter, sync engine, feedback, dotfile migration").version("0.1.8");
|
|
11837
12214
|
program2.command("setup").description("Configure cloud settings").option("--host <host>", "RDS hostname").option("--port <port>", "RDS port", "5432").option("--username <user>", "RDS username").option("--password-env <env>", "Env var for RDS password", "HASNA_RDS_PASSWORD").option("--ssl", "Enable SSL", true).option("--no-ssl", "Disable SSL").option("--mode <mode>", "Mode: local, cloud, or hybrid", "local").option("--sync-interval <minutes>", "Auto-sync interval in minutes", "0").action((opts) => {
|
|
11838
12215
|
const config = getCloudConfig();
|
|
11839
12216
|
if (opts.host)
|
|
@@ -11866,7 +12243,7 @@ program2.command("status").description("Show current cloud configuration and con
|
|
|
11866
12243
|
Checking PostgreSQL connection...`);
|
|
11867
12244
|
try {
|
|
11868
12245
|
const connStr = getConnectionString("postgres");
|
|
11869
|
-
const pg2 = new
|
|
12246
|
+
const pg2 = new PgAdapterAsync2(connStr);
|
|
11870
12247
|
const row = await pg2.get("SELECT 1 as ok");
|
|
11871
12248
|
if (row?.ok === 1) {
|
|
11872
12249
|
console.log("PostgreSQL: connected");
|
|
@@ -11899,7 +12276,7 @@ syncCmd.command("push").description("Push local data to cloud").requiredOption("
|
|
|
11899
12276
|
}
|
|
11900
12277
|
console.log(`Pushing ${tables.length} table(s) to cloud...`);
|
|
11901
12278
|
const connStr = getConnectionString(opts.service);
|
|
11902
|
-
const cloud = new
|
|
12279
|
+
const cloud = new PgAdapterAsync2(connStr);
|
|
11903
12280
|
const results = await syncPush(local, cloud, {
|
|
11904
12281
|
tables,
|
|
11905
12282
|
onProgress: (p) => {
|
|
@@ -11931,7 +12308,7 @@ syncCmd.command("pull").description("Pull cloud data to local").requiredOption("
|
|
|
11931
12308
|
const dbPath = getDbPath2(opts.service);
|
|
11932
12309
|
const local = new SqliteAdapter2(dbPath);
|
|
11933
12310
|
const connStr = getConnectionString(opts.service);
|
|
11934
|
-
const cloud = new
|
|
12311
|
+
const cloud = new PgAdapterAsync2(connStr);
|
|
11935
12312
|
let tables;
|
|
11936
12313
|
if (opts.tables) {
|
|
11937
12314
|
tables = opts.tables.split(",").map((t) => t.trim());
|
|
@@ -11975,6 +12352,77 @@ Done. ${totalWritten} rows pulled, ${totalErrors} errors.`);
|
|
|
11975
12352
|
}
|
|
11976
12353
|
}
|
|
11977
12354
|
});
|
|
12355
|
+
syncCmd.command("schedule").description("Manage scheduled background sync").option("--every <interval>", "Set sync interval (e.g. 5m, 10m, 1h)").option("--off", "Disable scheduled sync").option("--now", "Run a one-off sync immediately").action(async (opts) => {
|
|
12356
|
+
if (opts.off) {
|
|
12357
|
+
try {
|
|
12358
|
+
await removeSyncSchedule();
|
|
12359
|
+
console.log("Scheduled sync disabled.");
|
|
12360
|
+
} catch (err) {
|
|
12361
|
+
console.error("Failed to remove schedule:", err?.message);
|
|
12362
|
+
process.exit(1);
|
|
12363
|
+
}
|
|
12364
|
+
return;
|
|
12365
|
+
}
|
|
12366
|
+
if (opts.now) {
|
|
12367
|
+
const config = getCloudConfig();
|
|
12368
|
+
if (config.mode === "local") {
|
|
12369
|
+
console.error("Error: mode is 'local'. Run `cloud setup --mode hybrid` or `--mode cloud` first.");
|
|
12370
|
+
process.exit(1);
|
|
12371
|
+
}
|
|
12372
|
+
console.log("Running sync now...");
|
|
12373
|
+
const services2 = discoverSyncableServices();
|
|
12374
|
+
console.log(`Discovered ${services2.length} service(s): ${services2.join(", ") || "(none)"}`);
|
|
12375
|
+
const results = await runScheduledSync();
|
|
12376
|
+
for (const r of results) {
|
|
12377
|
+
const status2 = r.errors.length === 0 ? "ok" : "errors";
|
|
12378
|
+
console.log(` ${r.service}: ${r.tables_synced} table(s), ${r.total_rows_synced} row(s) [${status2}]`);
|
|
12379
|
+
for (const e of r.errors) {
|
|
12380
|
+
console.error(` ${e}`);
|
|
12381
|
+
}
|
|
12382
|
+
}
|
|
12383
|
+
if (results.length === 0) {
|
|
12384
|
+
console.log("No services synced (mode may be local or no databases found).");
|
|
12385
|
+
} else {
|
|
12386
|
+
const totalRows = results.reduce((s, r) => s + r.total_rows_synced, 0);
|
|
12387
|
+
const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
|
|
12388
|
+
console.log(`
|
|
12389
|
+
Done. ${totalRows} rows synced, ${totalErrors} errors.`);
|
|
12390
|
+
}
|
|
12391
|
+
return;
|
|
12392
|
+
}
|
|
12393
|
+
if (opts.every) {
|
|
12394
|
+
try {
|
|
12395
|
+
const minutes = parseInterval(opts.every);
|
|
12396
|
+
await registerSyncSchedule(minutes);
|
|
12397
|
+
console.log(`Scheduled sync registered: every ${minutes} minute(s).`);
|
|
12398
|
+
} catch (err) {
|
|
12399
|
+
console.error("Failed to register schedule:", err?.message);
|
|
12400
|
+
process.exit(1);
|
|
12401
|
+
}
|
|
12402
|
+
return;
|
|
12403
|
+
}
|
|
12404
|
+
const status = getSyncScheduleStatus();
|
|
12405
|
+
if (status.registered) {
|
|
12406
|
+
console.log("Scheduled sync: enabled");
|
|
12407
|
+
console.log(` Interval: ${status.schedule_minutes} minute(s)`);
|
|
12408
|
+
console.log(` Cron expression: ${status.cron_expression}`);
|
|
12409
|
+
} else {
|
|
12410
|
+
console.log("Scheduled sync: disabled");
|
|
12411
|
+
console.log(`
|
|
12412
|
+
To enable, run: cloud sync schedule --every 5m`);
|
|
12413
|
+
}
|
|
12414
|
+
const services = discoverSyncableServices();
|
|
12415
|
+
if (services.length > 0) {
|
|
12416
|
+
console.log(`
|
|
12417
|
+
Syncable services (${services.length}):`);
|
|
12418
|
+
for (const s of services) {
|
|
12419
|
+
console.log(` - ${s}`);
|
|
12420
|
+
}
|
|
12421
|
+
} else {
|
|
12422
|
+
console.log(`
|
|
12423
|
+
No syncable services found (no .db files in ~/.hasna/).`);
|
|
12424
|
+
}
|
|
12425
|
+
});
|
|
11978
12426
|
program2.command("feedback").description("Send feedback").requiredOption("--service <name>", "Service name").requiredOption("--message <msg>", "Feedback message").option("--email <email>", "Contact email").option("--version <ver>", "Service version").action(async (opts) => {
|
|
11979
12427
|
const db = createDatabase({ service: "cloud" });
|
|
11980
12428
|
const result = await sendFeedback({
|
package/dist/config.d.ts
CHANGED
|
@@ -22,6 +22,13 @@ export declare const CloudConfigSchema: z.ZodObject<{
|
|
|
22
22
|
mode: z.ZodDefault<z.ZodEnum<["local", "cloud", "hybrid"]>>;
|
|
23
23
|
auto_sync_interval_minutes: z.ZodDefault<z.ZodNumber>;
|
|
24
24
|
feedback_endpoint: z.ZodDefault<z.ZodString>;
|
|
25
|
+
sync: z.ZodDefault<z.ZodObject<{
|
|
26
|
+
schedule_minutes: z.ZodDefault<z.ZodNumber>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
schedule_minutes: number;
|
|
29
|
+
}, {
|
|
30
|
+
schedule_minutes?: number | undefined;
|
|
31
|
+
}>>;
|
|
25
32
|
}, "strip", z.ZodTypeAny, {
|
|
26
33
|
rds: {
|
|
27
34
|
host: string;
|
|
@@ -33,6 +40,9 @@ export declare const CloudConfigSchema: z.ZodObject<{
|
|
|
33
40
|
mode: "local" | "cloud" | "hybrid";
|
|
34
41
|
auto_sync_interval_minutes: number;
|
|
35
42
|
feedback_endpoint: string;
|
|
43
|
+
sync: {
|
|
44
|
+
schedule_minutes: number;
|
|
45
|
+
};
|
|
36
46
|
}, {
|
|
37
47
|
rds?: {
|
|
38
48
|
host?: string | undefined;
|
|
@@ -44,6 +54,9 @@ export declare const CloudConfigSchema: z.ZodObject<{
|
|
|
44
54
|
mode?: "local" | "cloud" | "hybrid" | undefined;
|
|
45
55
|
auto_sync_interval_minutes?: number | undefined;
|
|
46
56
|
feedback_endpoint?: string | undefined;
|
|
57
|
+
sync?: {
|
|
58
|
+
schedule_minutes?: number | undefined;
|
|
59
|
+
} | undefined;
|
|
47
60
|
}>;
|
|
48
61
|
export type CloudConfig = z.infer<typeof CloudConfigSchema>;
|
|
49
62
|
export declare function getConfigDir(): string;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,iBAAiB
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoB5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAS5D,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAMD,wBAAgB,cAAc,IAAI,WAAW,CAU5C;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAGzD;AAMD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAa1D;AAMD,OAAO,EAA4B,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAGxE,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpC,qCAAqC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,SAAS,CAaxE"}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { SyncProgressTracker, type SyncProgressInfo, type ProgressCallback, type
|
|
|
8
8
|
export { detectConflicts, resolveConflicts, getWinningData, ensureConflictsTable, storeConflicts, listConflicts, resolveConflict, getConflict, purgeResolvedConflicts, type SyncConflict, type ConflictStrategy, type StoredConflict, } from "./sync-conflicts.js";
|
|
9
9
|
export { incrementalSyncPush, incrementalSyncPull, ensureSyncMetaTable, getSyncMetaAll, getSyncMetaForTable, resetSyncMeta, resetAllSyncMeta, type IncrementalSyncStats, type IncrementalSyncOptions, type SyncMeta, } from "./sync-incremental.js";
|
|
10
10
|
export { setupAutoSync, enableAutoSync, getAutoSyncConfig, type AutoSyncConfig, type AutoSyncContext, type AutoSyncResult, } from "./auto-sync.js";
|
|
11
|
+
export { runScheduledSync, discoverSyncableServices, type ScheduledSyncResult, } from "./scheduled-sync.js";
|
|
12
|
+
export { registerSyncSchedule, removeSyncSchedule, getSyncScheduleStatus, parseInterval, minutesToCron, type SyncScheduleStatus, } from "./sync-schedule.js";
|
|
11
13
|
export { registerCloudTools } from "./mcp-helpers.js";
|
|
12
14
|
export { registerCloudCommands } from "./cli-helpers.js";
|
|
13
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,aAAa,EACb,SAAS,EACT,cAAc,EACd,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,SAAS,GACf,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,KAAK,OAAO,GACb,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,KAAK,QAAQ,GACd,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,cAAc,EACd,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,WAAW,GACZ,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,WAAW,GACjB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,eAAe,EACf,WAAW,EACX,sBAAsB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,QAAQ,GACd,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,aAAa,EACb,SAAS,EACT,cAAc,EACd,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,SAAS,GACf,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,KAAK,OAAO,GACb,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,KAAK,QAAQ,GACd,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,cAAc,EACd,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,WAAW,GACZ,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,WAAW,GACjB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,eAAe,EACf,WAAW,EACX,sBAAsB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,QAAQ,GACd,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,KAAK,mBAAmB,GACzB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,aAAa,EACb,KAAK,kBAAkB,GACxB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC"}
|