@hasna/cloud 0.1.7 → 0.1.8
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 +445 -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 +166 -1
- package/dist/mcp/index.js +8 -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/package.json +3 -3
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");
|
|
@@ -11589,7 +11652,10 @@ var CloudConfigSchema2 = exports_external.object({
|
|
|
11589
11652
|
}).default({}),
|
|
11590
11653
|
mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
|
|
11591
11654
|
auto_sync_interval_minutes: exports_external.number().default(0),
|
|
11592
|
-
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
|
|
11655
|
+
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
|
|
11656
|
+
sync: exports_external.object({
|
|
11657
|
+
schedule_minutes: exports_external.number().default(0)
|
|
11658
|
+
}).default({})
|
|
11593
11659
|
});
|
|
11594
11660
|
var CONFIG_DIR2 = join3(homedir3(), ".hasna", "cloud");
|
|
11595
11661
|
var CONFIG_PATH2 = join3(CONFIG_DIR2, "config.json");
|
|
@@ -11604,6 +11670,21 @@ function getCloudConfig2() {
|
|
|
11604
11670
|
return CloudConfigSchema2.parse({});
|
|
11605
11671
|
}
|
|
11606
11672
|
}
|
|
11673
|
+
function saveCloudConfig2(config) {
|
|
11674
|
+
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
11675
|
+
writeFileSync2(CONFIG_PATH2, JSON.stringify(config, null, 2) + `
|
|
11676
|
+
`, "utf-8");
|
|
11677
|
+
}
|
|
11678
|
+
function getConnectionString2(dbName) {
|
|
11679
|
+
const config = getCloudConfig2();
|
|
11680
|
+
const { host, port, username, password_env, ssl } = config.rds;
|
|
11681
|
+
if (!host || !username) {
|
|
11682
|
+
throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
|
|
11683
|
+
}
|
|
11684
|
+
const password = process.env[password_env] ?? "";
|
|
11685
|
+
const sslParam = ssl ? "?sslmode=require" : "";
|
|
11686
|
+
return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
|
|
11687
|
+
}
|
|
11607
11688
|
|
|
11608
11689
|
// src/feedback.ts
|
|
11609
11690
|
var FEEDBACK_TABLE_SQL = `
|
|
@@ -11775,7 +11856,7 @@ class SqliteAdapter2 {
|
|
|
11775
11856
|
return this.db;
|
|
11776
11857
|
}
|
|
11777
11858
|
}
|
|
11778
|
-
class
|
|
11859
|
+
class PgAdapterAsync2 {
|
|
11779
11860
|
pool;
|
|
11780
11861
|
constructor(arg) {
|
|
11781
11862
|
if (typeof arg === "string") {
|
|
@@ -11831,9 +11912,295 @@ class PgAdapterAsync {
|
|
|
11831
11912
|
}
|
|
11832
11913
|
}
|
|
11833
11914
|
|
|
11915
|
+
// src/sync-schedule.ts
|
|
11916
|
+
import { join as join5, dirname } from "path";
|
|
11917
|
+
var CRON_TITLE = "hasna-cloud-sync";
|
|
11918
|
+
function getWorkerPath() {
|
|
11919
|
+
const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
|
|
11920
|
+
const tsPath = join5(dir, "scheduled-sync.ts");
|
|
11921
|
+
const jsPath = join5(dir, "scheduled-sync.js");
|
|
11922
|
+
try {
|
|
11923
|
+
const { existsSync: existsSync5 } = __require("fs");
|
|
11924
|
+
if (existsSync5(tsPath))
|
|
11925
|
+
return tsPath;
|
|
11926
|
+
} catch {}
|
|
11927
|
+
return jsPath;
|
|
11928
|
+
}
|
|
11929
|
+
function parseInterval(input) {
|
|
11930
|
+
const trimmed = input.trim().toLowerCase();
|
|
11931
|
+
const hourMatch = trimmed.match(/^(\d+)\s*h$/);
|
|
11932
|
+
if (hourMatch) {
|
|
11933
|
+
const hours = parseInt(hourMatch[1], 10);
|
|
11934
|
+
if (hours <= 0) {
|
|
11935
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
11936
|
+
}
|
|
11937
|
+
return hours * 60;
|
|
11938
|
+
}
|
|
11939
|
+
const minMatch = trimmed.match(/^(\d+)\s*m$/);
|
|
11940
|
+
if (minMatch) {
|
|
11941
|
+
const mins = parseInt(minMatch[1], 10);
|
|
11942
|
+
if (mins <= 0) {
|
|
11943
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
11944
|
+
}
|
|
11945
|
+
return mins;
|
|
11946
|
+
}
|
|
11947
|
+
const plain = parseInt(trimmed, 10);
|
|
11948
|
+
if (!isNaN(plain) && plain > 0) {
|
|
11949
|
+
return plain;
|
|
11950
|
+
}
|
|
11951
|
+
throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
|
|
11952
|
+
}
|
|
11953
|
+
function minutesToCron(minutes) {
|
|
11954
|
+
if (minutes <= 0) {
|
|
11955
|
+
throw new Error("Interval must be greater than 0 minutes.");
|
|
11956
|
+
}
|
|
11957
|
+
if (minutes < 60) {
|
|
11958
|
+
return `*/${minutes} * * * *`;
|
|
11959
|
+
}
|
|
11960
|
+
const hours = Math.floor(minutes / 60);
|
|
11961
|
+
const remainderMins = minutes % 60;
|
|
11962
|
+
if (remainderMins === 0 && hours <= 24) {
|
|
11963
|
+
return `0 */${hours} * * *`;
|
|
11964
|
+
}
|
|
11965
|
+
return `*/${minutes} * * * *`;
|
|
11966
|
+
}
|
|
11967
|
+
async function registerSyncSchedule(intervalMinutes) {
|
|
11968
|
+
if (intervalMinutes <= 0) {
|
|
11969
|
+
throw new Error("Interval must be a positive number of minutes.");
|
|
11970
|
+
}
|
|
11971
|
+
const cronExpr = minutesToCron(intervalMinutes);
|
|
11972
|
+
const workerPath = getWorkerPath();
|
|
11973
|
+
await Bun.cron(workerPath, cronExpr, CRON_TITLE);
|
|
11974
|
+
const config = getCloudConfig2();
|
|
11975
|
+
config.sync.schedule_minutes = intervalMinutes;
|
|
11976
|
+
saveCloudConfig2(config);
|
|
11977
|
+
}
|
|
11978
|
+
async function removeSyncSchedule() {
|
|
11979
|
+
await Bun.cron.remove(CRON_TITLE);
|
|
11980
|
+
const config = getCloudConfig2();
|
|
11981
|
+
config.sync.schedule_minutes = 0;
|
|
11982
|
+
saveCloudConfig2(config);
|
|
11983
|
+
}
|
|
11984
|
+
function getSyncScheduleStatus() {
|
|
11985
|
+
const config = getCloudConfig2();
|
|
11986
|
+
const minutes = config.sync.schedule_minutes;
|
|
11987
|
+
const registered = minutes > 0;
|
|
11988
|
+
return {
|
|
11989
|
+
registered,
|
|
11990
|
+
schedule_minutes: minutes,
|
|
11991
|
+
cron_expression: registered ? minutesToCron(minutes) : null
|
|
11992
|
+
};
|
|
11993
|
+
}
|
|
11994
|
+
|
|
11995
|
+
// src/scheduled-sync.ts
|
|
11996
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
|
|
11997
|
+
import { join as join6 } from "path";
|
|
11998
|
+
|
|
11999
|
+
// src/sync-incremental.ts
|
|
12000
|
+
var SYNC_META_TABLE_SQL = `
|
|
12001
|
+
CREATE TABLE IF NOT EXISTS _sync_meta (
|
|
12002
|
+
table_name TEXT PRIMARY KEY,
|
|
12003
|
+
last_synced_at TEXT,
|
|
12004
|
+
last_synced_row_count INTEGER DEFAULT 0,
|
|
12005
|
+
direction TEXT DEFAULT 'push'
|
|
12006
|
+
)`;
|
|
12007
|
+
function ensureSyncMetaTable(db) {
|
|
12008
|
+
db.exec(SYNC_META_TABLE_SQL);
|
|
12009
|
+
}
|
|
12010
|
+
function getSyncMeta(db, table) {
|
|
12011
|
+
ensureSyncMetaTable(db);
|
|
12012
|
+
return db.get(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta WHERE table_name = ?`, table) ?? null;
|
|
12013
|
+
}
|
|
12014
|
+
function upsertSyncMeta(db, meta) {
|
|
12015
|
+
ensureSyncMetaTable(db);
|
|
12016
|
+
const existing = db.get(`SELECT table_name FROM _sync_meta WHERE table_name = ?`, meta.table_name);
|
|
12017
|
+
if (existing) {
|
|
12018
|
+
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);
|
|
12019
|
+
} else {
|
|
12020
|
+
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);
|
|
12021
|
+
}
|
|
12022
|
+
}
|
|
12023
|
+
function transferRows(source, target, table, rows, options) {
|
|
12024
|
+
const { primaryKey = "id", conflictColumn = "updated_at" } = options;
|
|
12025
|
+
let written = 0;
|
|
12026
|
+
let skipped = 0;
|
|
12027
|
+
const errors2 = [];
|
|
12028
|
+
if (rows.length === 0)
|
|
12029
|
+
return { written, skipped, errors: errors2 };
|
|
12030
|
+
const columns = Object.keys(rows[0]);
|
|
12031
|
+
const hasConflictCol = columns.includes(conflictColumn);
|
|
12032
|
+
const hasPrimaryKey = columns.includes(primaryKey);
|
|
12033
|
+
if (!hasPrimaryKey) {
|
|
12034
|
+
errors2.push(`Table "${table}" has no "${primaryKey}" column -- skipping`);
|
|
12035
|
+
return { written, skipped, errors: errors2 };
|
|
12036
|
+
}
|
|
12037
|
+
for (const row of rows) {
|
|
12038
|
+
try {
|
|
12039
|
+
const existing = target.get(`SELECT "${primaryKey}"${hasConflictCol ? `, "${conflictColumn}"` : ""} FROM "${table}" WHERE "${primaryKey}" = ?`, row[primaryKey]);
|
|
12040
|
+
if (existing) {
|
|
12041
|
+
if (hasConflictCol && existing[conflictColumn] && row[conflictColumn]) {
|
|
12042
|
+
const existingTime = new Date(existing[conflictColumn]).getTime();
|
|
12043
|
+
const incomingTime = new Date(row[conflictColumn]).getTime();
|
|
12044
|
+
if (existingTime >= incomingTime) {
|
|
12045
|
+
skipped++;
|
|
12046
|
+
continue;
|
|
12047
|
+
}
|
|
12048
|
+
}
|
|
12049
|
+
const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
|
|
12050
|
+
const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
|
|
12051
|
+
values.push(row[primaryKey]);
|
|
12052
|
+
target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
|
|
12053
|
+
} else {
|
|
12054
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
12055
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
12056
|
+
const values = columns.map((c) => row[c]);
|
|
12057
|
+
target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
|
|
12058
|
+
}
|
|
12059
|
+
written++;
|
|
12060
|
+
} catch (err) {
|
|
12061
|
+
errors2.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
|
|
12062
|
+
}
|
|
12063
|
+
}
|
|
12064
|
+
return { written, skipped, errors: errors2 };
|
|
12065
|
+
}
|
|
12066
|
+
function incrementalSyncPush(local, remote, tables, options = {}) {
|
|
12067
|
+
const { conflictColumn = "updated_at", batchSize = 500 } = options;
|
|
12068
|
+
const results = [];
|
|
12069
|
+
ensureSyncMetaTable(local);
|
|
12070
|
+
for (const table of tables) {
|
|
12071
|
+
const stat = {
|
|
12072
|
+
table,
|
|
12073
|
+
total_rows: 0,
|
|
12074
|
+
synced_rows: 0,
|
|
12075
|
+
skipped_rows: 0,
|
|
12076
|
+
errors: [],
|
|
12077
|
+
first_sync: false
|
|
12078
|
+
};
|
|
12079
|
+
try {
|
|
12080
|
+
const countResult = local.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
|
|
12081
|
+
stat.total_rows = countResult?.cnt ?? 0;
|
|
12082
|
+
const meta = getSyncMeta(local, table);
|
|
12083
|
+
let rows;
|
|
12084
|
+
if (meta?.last_synced_at) {
|
|
12085
|
+
try {
|
|
12086
|
+
rows = local.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
|
|
12087
|
+
} catch {
|
|
12088
|
+
rows = local.all(`SELECT * FROM "${table}"`);
|
|
12089
|
+
stat.first_sync = true;
|
|
12090
|
+
}
|
|
12091
|
+
} else {
|
|
12092
|
+
rows = local.all(`SELECT * FROM "${table}"`);
|
|
12093
|
+
stat.first_sync = true;
|
|
12094
|
+
}
|
|
12095
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
12096
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
12097
|
+
const result = transferRows(local, remote, table, batch, options);
|
|
12098
|
+
stat.synced_rows += result.written;
|
|
12099
|
+
stat.skipped_rows += result.skipped;
|
|
12100
|
+
stat.errors.push(...result.errors);
|
|
12101
|
+
}
|
|
12102
|
+
if (rows.length === 0) {
|
|
12103
|
+
stat.skipped_rows = stat.total_rows;
|
|
12104
|
+
}
|
|
12105
|
+
const now = new Date().toISOString();
|
|
12106
|
+
upsertSyncMeta(local, {
|
|
12107
|
+
table_name: table,
|
|
12108
|
+
last_synced_at: now,
|
|
12109
|
+
last_synced_row_count: stat.synced_rows,
|
|
12110
|
+
direction: "push"
|
|
12111
|
+
});
|
|
12112
|
+
} catch (err) {
|
|
12113
|
+
stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
|
|
12114
|
+
}
|
|
12115
|
+
results.push(stat);
|
|
12116
|
+
}
|
|
12117
|
+
return results;
|
|
12118
|
+
}
|
|
12119
|
+
|
|
12120
|
+
// src/sync.ts
|
|
12121
|
+
function listSqliteTables2(db) {
|
|
12122
|
+
const rows = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
12123
|
+
return rows.map((r) => r.name);
|
|
12124
|
+
}
|
|
12125
|
+
|
|
12126
|
+
// src/scheduled-sync.ts
|
|
12127
|
+
function discoverSyncableServices() {
|
|
12128
|
+
const hasnaDir = getHasnaDir();
|
|
12129
|
+
const services = [];
|
|
12130
|
+
try {
|
|
12131
|
+
const entries = readdirSync3(hasnaDir, { withFileTypes: true });
|
|
12132
|
+
for (const entry of entries) {
|
|
12133
|
+
if (!entry.isDirectory())
|
|
12134
|
+
continue;
|
|
12135
|
+
const dbPath = join6(hasnaDir, entry.name, `${entry.name}.db`);
|
|
12136
|
+
if (existsSync5(dbPath)) {
|
|
12137
|
+
services.push(entry.name);
|
|
12138
|
+
}
|
|
12139
|
+
}
|
|
12140
|
+
} catch {}
|
|
12141
|
+
return services;
|
|
12142
|
+
}
|
|
12143
|
+
async function runScheduledSync() {
|
|
12144
|
+
const config = getCloudConfig2();
|
|
12145
|
+
if (config.mode === "local")
|
|
12146
|
+
return [];
|
|
12147
|
+
const services = discoverSyncableServices();
|
|
12148
|
+
const results = [];
|
|
12149
|
+
let remote = null;
|
|
12150
|
+
for (const service of services) {
|
|
12151
|
+
const result = {
|
|
12152
|
+
service,
|
|
12153
|
+
tables_synced: 0,
|
|
12154
|
+
total_rows_synced: 0,
|
|
12155
|
+
errors: []
|
|
12156
|
+
};
|
|
12157
|
+
try {
|
|
12158
|
+
const dbPath = join6(getDataDir(service), `${service}.db`);
|
|
12159
|
+
if (!existsSync5(dbPath)) {
|
|
12160
|
+
continue;
|
|
12161
|
+
}
|
|
12162
|
+
const local = new SqliteAdapter(dbPath);
|
|
12163
|
+
const tables = listSqliteTables2(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
|
|
12164
|
+
if (tables.length === 0) {
|
|
12165
|
+
local.close();
|
|
12166
|
+
continue;
|
|
12167
|
+
}
|
|
12168
|
+
try {
|
|
12169
|
+
const connStr = getConnectionString2(service);
|
|
12170
|
+
remote = new PgAdapterAsync(connStr);
|
|
12171
|
+
} catch (err) {
|
|
12172
|
+
result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
|
|
12173
|
+
local.close();
|
|
12174
|
+
results.push(result);
|
|
12175
|
+
continue;
|
|
12176
|
+
}
|
|
12177
|
+
const stats = incrementalSyncPush(local, remote, tables);
|
|
12178
|
+
for (const s of stats) {
|
|
12179
|
+
if (s.errors.length === 0) {
|
|
12180
|
+
result.tables_synced++;
|
|
12181
|
+
}
|
|
12182
|
+
result.total_rows_synced += s.synced_rows;
|
|
12183
|
+
result.errors.push(...s.errors);
|
|
12184
|
+
}
|
|
12185
|
+
local.close();
|
|
12186
|
+
await remote.close();
|
|
12187
|
+
remote = null;
|
|
12188
|
+
} catch (err) {
|
|
12189
|
+
result.errors.push(err?.message ?? String(err));
|
|
12190
|
+
}
|
|
12191
|
+
results.push(result);
|
|
12192
|
+
}
|
|
12193
|
+
if (remote) {
|
|
12194
|
+
try {
|
|
12195
|
+
await remote.close();
|
|
12196
|
+
} catch {}
|
|
12197
|
+
}
|
|
12198
|
+
return results;
|
|
12199
|
+
}
|
|
12200
|
+
|
|
11834
12201
|
// src/cli/index.ts
|
|
11835
12202
|
var program2 = new Command;
|
|
11836
|
-
program2.name("cloud").description("Shared cloud infrastructure \u2014 database adapter, sync engine, feedback, dotfile migration").version("0.1.
|
|
12203
|
+
program2.name("cloud").description("Shared cloud infrastructure \u2014 database adapter, sync engine, feedback, dotfile migration").version("0.1.8");
|
|
11837
12204
|
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
12205
|
const config = getCloudConfig();
|
|
11839
12206
|
if (opts.host)
|
|
@@ -11866,7 +12233,7 @@ program2.command("status").description("Show current cloud configuration and con
|
|
|
11866
12233
|
Checking PostgreSQL connection...`);
|
|
11867
12234
|
try {
|
|
11868
12235
|
const connStr = getConnectionString("postgres");
|
|
11869
|
-
const pg2 = new
|
|
12236
|
+
const pg2 = new PgAdapterAsync2(connStr);
|
|
11870
12237
|
const row = await pg2.get("SELECT 1 as ok");
|
|
11871
12238
|
if (row?.ok === 1) {
|
|
11872
12239
|
console.log("PostgreSQL: connected");
|
|
@@ -11899,7 +12266,7 @@ syncCmd.command("push").description("Push local data to cloud").requiredOption("
|
|
|
11899
12266
|
}
|
|
11900
12267
|
console.log(`Pushing ${tables.length} table(s) to cloud...`);
|
|
11901
12268
|
const connStr = getConnectionString(opts.service);
|
|
11902
|
-
const cloud = new
|
|
12269
|
+
const cloud = new PgAdapterAsync2(connStr);
|
|
11903
12270
|
const results = await syncPush(local, cloud, {
|
|
11904
12271
|
tables,
|
|
11905
12272
|
onProgress: (p) => {
|
|
@@ -11931,7 +12298,7 @@ syncCmd.command("pull").description("Pull cloud data to local").requiredOption("
|
|
|
11931
12298
|
const dbPath = getDbPath2(opts.service);
|
|
11932
12299
|
const local = new SqliteAdapter2(dbPath);
|
|
11933
12300
|
const connStr = getConnectionString(opts.service);
|
|
11934
|
-
const cloud = new
|
|
12301
|
+
const cloud = new PgAdapterAsync2(connStr);
|
|
11935
12302
|
let tables;
|
|
11936
12303
|
if (opts.tables) {
|
|
11937
12304
|
tables = opts.tables.split(",").map((t) => t.trim());
|
|
@@ -11975,6 +12342,77 @@ Done. ${totalWritten} rows pulled, ${totalErrors} errors.`);
|
|
|
11975
12342
|
}
|
|
11976
12343
|
}
|
|
11977
12344
|
});
|
|
12345
|
+
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) => {
|
|
12346
|
+
if (opts.off) {
|
|
12347
|
+
try {
|
|
12348
|
+
await removeSyncSchedule();
|
|
12349
|
+
console.log("Scheduled sync disabled.");
|
|
12350
|
+
} catch (err) {
|
|
12351
|
+
console.error("Failed to remove schedule:", err?.message);
|
|
12352
|
+
process.exit(1);
|
|
12353
|
+
}
|
|
12354
|
+
return;
|
|
12355
|
+
}
|
|
12356
|
+
if (opts.now) {
|
|
12357
|
+
const config = getCloudConfig();
|
|
12358
|
+
if (config.mode === "local") {
|
|
12359
|
+
console.error("Error: mode is 'local'. Run `cloud setup --mode hybrid` or `--mode cloud` first.");
|
|
12360
|
+
process.exit(1);
|
|
12361
|
+
}
|
|
12362
|
+
console.log("Running sync now...");
|
|
12363
|
+
const services2 = discoverSyncableServices();
|
|
12364
|
+
console.log(`Discovered ${services2.length} service(s): ${services2.join(", ") || "(none)"}`);
|
|
12365
|
+
const results = await runScheduledSync();
|
|
12366
|
+
for (const r of results) {
|
|
12367
|
+
const status2 = r.errors.length === 0 ? "ok" : "errors";
|
|
12368
|
+
console.log(` ${r.service}: ${r.tables_synced} table(s), ${r.total_rows_synced} row(s) [${status2}]`);
|
|
12369
|
+
for (const e of r.errors) {
|
|
12370
|
+
console.error(` ${e}`);
|
|
12371
|
+
}
|
|
12372
|
+
}
|
|
12373
|
+
if (results.length === 0) {
|
|
12374
|
+
console.log("No services synced (mode may be local or no databases found).");
|
|
12375
|
+
} else {
|
|
12376
|
+
const totalRows = results.reduce((s, r) => s + r.total_rows_synced, 0);
|
|
12377
|
+
const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
|
|
12378
|
+
console.log(`
|
|
12379
|
+
Done. ${totalRows} rows synced, ${totalErrors} errors.`);
|
|
12380
|
+
}
|
|
12381
|
+
return;
|
|
12382
|
+
}
|
|
12383
|
+
if (opts.every) {
|
|
12384
|
+
try {
|
|
12385
|
+
const minutes = parseInterval(opts.every);
|
|
12386
|
+
await registerSyncSchedule(minutes);
|
|
12387
|
+
console.log(`Scheduled sync registered: every ${minutes} minute(s).`);
|
|
12388
|
+
} catch (err) {
|
|
12389
|
+
console.error("Failed to register schedule:", err?.message);
|
|
12390
|
+
process.exit(1);
|
|
12391
|
+
}
|
|
12392
|
+
return;
|
|
12393
|
+
}
|
|
12394
|
+
const status = getSyncScheduleStatus();
|
|
12395
|
+
if (status.registered) {
|
|
12396
|
+
console.log("Scheduled sync: enabled");
|
|
12397
|
+
console.log(` Interval: ${status.schedule_minutes} minute(s)`);
|
|
12398
|
+
console.log(` Cron expression: ${status.cron_expression}`);
|
|
12399
|
+
} else {
|
|
12400
|
+
console.log("Scheduled sync: disabled");
|
|
12401
|
+
console.log(`
|
|
12402
|
+
To enable, run: cloud sync schedule --every 5m`);
|
|
12403
|
+
}
|
|
12404
|
+
const services = discoverSyncableServices();
|
|
12405
|
+
if (services.length > 0) {
|
|
12406
|
+
console.log(`
|
|
12407
|
+
Syncable services (${services.length}):`);
|
|
12408
|
+
for (const s of services) {
|
|
12409
|
+
console.log(` - ${s}`);
|
|
12410
|
+
}
|
|
12411
|
+
} else {
|
|
12412
|
+
console.log(`
|
|
12413
|
+
No syncable services found (no .db files in ~/.hasna/).`);
|
|
12414
|
+
}
|
|
12415
|
+
});
|
|
11978
12416
|
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
12417
|
const db = createDatabase({ service: "cloud" });
|
|
11980
12418
|
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"}
|