@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 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 PgAdapterAsync {
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.7");
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 PgAdapterAsync(connStr);
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 PgAdapterAsync(connStr);
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 PgAdapterAsync(connStr);
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;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAe5B,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"}
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
@@ -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"}