@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/index.js
CHANGED
|
@@ -9265,7 +9265,10 @@ var CloudConfigSchema = exports_external.object({
|
|
|
9265
9265
|
}).default({}),
|
|
9266
9266
|
mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
|
|
9267
9267
|
auto_sync_interval_minutes: exports_external.number().default(0),
|
|
9268
|
-
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
|
|
9268
|
+
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
|
|
9269
|
+
sync: exports_external.object({
|
|
9270
|
+
schedule_minutes: exports_external.number().default(0)
|
|
9271
|
+
}).default({})
|
|
9269
9272
|
});
|
|
9270
9273
|
var CONFIG_DIR = join2(homedir2(), ".hasna", "cloud");
|
|
9271
9274
|
var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
|
|
@@ -9441,6 +9444,11 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
9441
9444
|
primaryKey: pkOption
|
|
9442
9445
|
} = options;
|
|
9443
9446
|
const results = [];
|
|
9447
|
+
if (!isAsyncAdapter(target)) {
|
|
9448
|
+
try {
|
|
9449
|
+
target.run("PRAGMA foreign_keys = OFF");
|
|
9450
|
+
} catch {}
|
|
9451
|
+
}
|
|
9444
9452
|
for (let i = 0;i < tables.length; i++) {
|
|
9445
9453
|
const table = tables[i];
|
|
9446
9454
|
const result = {
|
|
@@ -9558,6 +9566,11 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
9558
9566
|
}
|
|
9559
9567
|
results.push(result);
|
|
9560
9568
|
}
|
|
9569
|
+
if (!isAsyncAdapter(target)) {
|
|
9570
|
+
try {
|
|
9571
|
+
target.run("PRAGMA foreign_keys = ON");
|
|
9572
|
+
} catch {}
|
|
9573
|
+
}
|
|
9561
9574
|
return results;
|
|
9562
9575
|
}
|
|
9563
9576
|
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
|
|
@@ -10247,6 +10260,161 @@ function setupAutoSync(serviceName, server, local, remote, tables) {
|
|
|
10247
10260
|
function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
|
|
10248
10261
|
setupAutoSync(serviceName, mcpServer, local, remote, tables);
|
|
10249
10262
|
}
|
|
10263
|
+
// src/scheduled-sync.ts
|
|
10264
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
|
|
10265
|
+
import { join as join4 } from "path";
|
|
10266
|
+
function discoverSyncableServices() {
|
|
10267
|
+
const hasnaDir = getHasnaDir();
|
|
10268
|
+
const services = [];
|
|
10269
|
+
try {
|
|
10270
|
+
const entries = readdirSync2(hasnaDir, { withFileTypes: true });
|
|
10271
|
+
for (const entry of entries) {
|
|
10272
|
+
if (!entry.isDirectory())
|
|
10273
|
+
continue;
|
|
10274
|
+
const dbPath = join4(hasnaDir, entry.name, `${entry.name}.db`);
|
|
10275
|
+
if (existsSync4(dbPath)) {
|
|
10276
|
+
services.push(entry.name);
|
|
10277
|
+
}
|
|
10278
|
+
}
|
|
10279
|
+
} catch {}
|
|
10280
|
+
return services;
|
|
10281
|
+
}
|
|
10282
|
+
async function runScheduledSync() {
|
|
10283
|
+
const config = getCloudConfig();
|
|
10284
|
+
if (config.mode === "local")
|
|
10285
|
+
return [];
|
|
10286
|
+
const services = discoverSyncableServices();
|
|
10287
|
+
const results = [];
|
|
10288
|
+
let remote = null;
|
|
10289
|
+
for (const service of services) {
|
|
10290
|
+
const result = {
|
|
10291
|
+
service,
|
|
10292
|
+
tables_synced: 0,
|
|
10293
|
+
total_rows_synced: 0,
|
|
10294
|
+
errors: []
|
|
10295
|
+
};
|
|
10296
|
+
try {
|
|
10297
|
+
const dbPath = join4(getDataDir(service), `${service}.db`);
|
|
10298
|
+
if (!existsSync4(dbPath)) {
|
|
10299
|
+
continue;
|
|
10300
|
+
}
|
|
10301
|
+
const local = new SqliteAdapter(dbPath);
|
|
10302
|
+
const tables = listSqliteTables(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
|
|
10303
|
+
if (tables.length === 0) {
|
|
10304
|
+
local.close();
|
|
10305
|
+
continue;
|
|
10306
|
+
}
|
|
10307
|
+
try {
|
|
10308
|
+
const connStr = getConnectionString(service);
|
|
10309
|
+
remote = new PgAdapterAsync(connStr);
|
|
10310
|
+
} catch (err) {
|
|
10311
|
+
result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
|
|
10312
|
+
local.close();
|
|
10313
|
+
results.push(result);
|
|
10314
|
+
continue;
|
|
10315
|
+
}
|
|
10316
|
+
const stats = incrementalSyncPush(local, remote, tables);
|
|
10317
|
+
for (const s of stats) {
|
|
10318
|
+
if (s.errors.length === 0) {
|
|
10319
|
+
result.tables_synced++;
|
|
10320
|
+
}
|
|
10321
|
+
result.total_rows_synced += s.synced_rows;
|
|
10322
|
+
result.errors.push(...s.errors);
|
|
10323
|
+
}
|
|
10324
|
+
local.close();
|
|
10325
|
+
await remote.close();
|
|
10326
|
+
remote = null;
|
|
10327
|
+
} catch (err) {
|
|
10328
|
+
result.errors.push(err?.message ?? String(err));
|
|
10329
|
+
}
|
|
10330
|
+
results.push(result);
|
|
10331
|
+
}
|
|
10332
|
+
if (remote) {
|
|
10333
|
+
try {
|
|
10334
|
+
await remote.close();
|
|
10335
|
+
} catch {}
|
|
10336
|
+
}
|
|
10337
|
+
return results;
|
|
10338
|
+
}
|
|
10339
|
+
// src/sync-schedule.ts
|
|
10340
|
+
import { join as join5, dirname } from "path";
|
|
10341
|
+
var CRON_TITLE = "hasna-cloud-sync";
|
|
10342
|
+
function getWorkerPath() {
|
|
10343
|
+
const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
|
|
10344
|
+
const tsPath = join5(dir, "scheduled-sync.ts");
|
|
10345
|
+
const jsPath = join5(dir, "scheduled-sync.js");
|
|
10346
|
+
try {
|
|
10347
|
+
const { existsSync: existsSync5 } = __require("fs");
|
|
10348
|
+
if (existsSync5(tsPath))
|
|
10349
|
+
return tsPath;
|
|
10350
|
+
} catch {}
|
|
10351
|
+
return jsPath;
|
|
10352
|
+
}
|
|
10353
|
+
function parseInterval(input) {
|
|
10354
|
+
const trimmed = input.trim().toLowerCase();
|
|
10355
|
+
const hourMatch = trimmed.match(/^(\d+)\s*h$/);
|
|
10356
|
+
if (hourMatch) {
|
|
10357
|
+
const hours = parseInt(hourMatch[1], 10);
|
|
10358
|
+
if (hours <= 0) {
|
|
10359
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
10360
|
+
}
|
|
10361
|
+
return hours * 60;
|
|
10362
|
+
}
|
|
10363
|
+
const minMatch = trimmed.match(/^(\d+)\s*m$/);
|
|
10364
|
+
if (minMatch) {
|
|
10365
|
+
const mins = parseInt(minMatch[1], 10);
|
|
10366
|
+
if (mins <= 0) {
|
|
10367
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
10368
|
+
}
|
|
10369
|
+
return mins;
|
|
10370
|
+
}
|
|
10371
|
+
const plain = parseInt(trimmed, 10);
|
|
10372
|
+
if (!isNaN(plain) && plain > 0) {
|
|
10373
|
+
return plain;
|
|
10374
|
+
}
|
|
10375
|
+
throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
|
|
10376
|
+
}
|
|
10377
|
+
function minutesToCron(minutes) {
|
|
10378
|
+
if (minutes <= 0) {
|
|
10379
|
+
throw new Error("Interval must be greater than 0 minutes.");
|
|
10380
|
+
}
|
|
10381
|
+
if (minutes < 60) {
|
|
10382
|
+
return `*/${minutes} * * * *`;
|
|
10383
|
+
}
|
|
10384
|
+
const hours = Math.floor(minutes / 60);
|
|
10385
|
+
const remainderMins = minutes % 60;
|
|
10386
|
+
if (remainderMins === 0 && hours <= 24) {
|
|
10387
|
+
return `0 */${hours} * * *`;
|
|
10388
|
+
}
|
|
10389
|
+
return `*/${minutes} * * * *`;
|
|
10390
|
+
}
|
|
10391
|
+
async function registerSyncSchedule(intervalMinutes) {
|
|
10392
|
+
if (intervalMinutes <= 0) {
|
|
10393
|
+
throw new Error("Interval must be a positive number of minutes.");
|
|
10394
|
+
}
|
|
10395
|
+
const cronExpr = minutesToCron(intervalMinutes);
|
|
10396
|
+
const workerPath = getWorkerPath();
|
|
10397
|
+
await Bun.cron(workerPath, cronExpr, CRON_TITLE);
|
|
10398
|
+
const config = getCloudConfig();
|
|
10399
|
+
config.sync.schedule_minutes = intervalMinutes;
|
|
10400
|
+
saveCloudConfig(config);
|
|
10401
|
+
}
|
|
10402
|
+
async function removeSyncSchedule() {
|
|
10403
|
+
await Bun.cron.remove(CRON_TITLE);
|
|
10404
|
+
const config = getCloudConfig();
|
|
10405
|
+
config.sync.schedule_minutes = 0;
|
|
10406
|
+
saveCloudConfig(config);
|
|
10407
|
+
}
|
|
10408
|
+
function getSyncScheduleStatus() {
|
|
10409
|
+
const config = getCloudConfig();
|
|
10410
|
+
const minutes = config.sync.schedule_minutes;
|
|
10411
|
+
const registered = minutes > 0;
|
|
10412
|
+
return {
|
|
10413
|
+
registered,
|
|
10414
|
+
schedule_minutes: minutes,
|
|
10415
|
+
cron_expression: registered ? minutesToCron(minutes) : null
|
|
10416
|
+
};
|
|
10417
|
+
}
|
|
10250
10418
|
// src/mcp-helpers.ts
|
|
10251
10419
|
function registerCloudTools(server, serviceName) {
|
|
10252
10420
|
server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
|
|
@@ -10439,13 +10607,18 @@ export {
|
|
|
10439
10607
|
sendFeedback,
|
|
10440
10608
|
saveFeedback,
|
|
10441
10609
|
saveCloudConfig,
|
|
10610
|
+
runScheduledSync,
|
|
10442
10611
|
resolveConflicts,
|
|
10443
10612
|
resolveConflict,
|
|
10444
10613
|
resetSyncMeta,
|
|
10445
10614
|
resetAllSyncMeta,
|
|
10615
|
+
removeSyncSchedule,
|
|
10616
|
+
registerSyncSchedule,
|
|
10446
10617
|
registerCloudTools,
|
|
10447
10618
|
registerCloudCommands,
|
|
10448
10619
|
purgeResolvedConflicts,
|
|
10620
|
+
parseInterval,
|
|
10621
|
+
minutesToCron,
|
|
10449
10622
|
migrateDotfile,
|
|
10450
10623
|
listSqliteTables,
|
|
10451
10624
|
listPgTables,
|
|
@@ -10455,6 +10628,7 @@ export {
|
|
|
10455
10628
|
incrementalSyncPull,
|
|
10456
10629
|
hasLegacyDotfile,
|
|
10457
10630
|
getWinningData,
|
|
10631
|
+
getSyncScheduleStatus,
|
|
10458
10632
|
getSyncMetaForTable,
|
|
10459
10633
|
getSyncMetaAll,
|
|
10460
10634
|
getHasnaDir,
|
|
@@ -10470,6 +10644,7 @@ export {
|
|
|
10470
10644
|
ensureFeedbackTable,
|
|
10471
10645
|
ensureConflictsTable,
|
|
10472
10646
|
enableAutoSync,
|
|
10647
|
+
discoverSyncableServices,
|
|
10473
10648
|
detectConflicts,
|
|
10474
10649
|
createDatabase,
|
|
10475
10650
|
SyncProgressTracker,
|
package/dist/mcp/index.js
CHANGED
|
@@ -24622,7 +24622,10 @@ var CloudConfigSchema = exports_external.object({
|
|
|
24622
24622
|
}).default({}),
|
|
24623
24623
|
mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
|
|
24624
24624
|
auto_sync_interval_minutes: exports_external.number().default(0),
|
|
24625
|
-
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
|
|
24625
|
+
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
|
|
24626
|
+
sync: exports_external.object({
|
|
24627
|
+
schedule_minutes: exports_external.number().default(0)
|
|
24628
|
+
}).default({})
|
|
24626
24629
|
});
|
|
24627
24630
|
var CONFIG_DIR = join2(homedir2(), ".hasna", "cloud");
|
|
24628
24631
|
var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
|
|
@@ -24788,6 +24791,11 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
24788
24791
|
primaryKey: pkOption
|
|
24789
24792
|
} = options;
|
|
24790
24793
|
const results = [];
|
|
24794
|
+
if (!isAsyncAdapter(target)) {
|
|
24795
|
+
try {
|
|
24796
|
+
target.run("PRAGMA foreign_keys = OFF");
|
|
24797
|
+
} catch {}
|
|
24798
|
+
}
|
|
24791
24799
|
for (let i = 0;i < tables.length; i++) {
|
|
24792
24800
|
const table = tables[i];
|
|
24793
24801
|
const result = {
|
|
@@ -24905,6 +24913,11 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
24905
24913
|
}
|
|
24906
24914
|
results.push(result);
|
|
24907
24915
|
}
|
|
24916
|
+
if (!isAsyncAdapter(target)) {
|
|
24917
|
+
try {
|
|
24918
|
+
target.run("PRAGMA foreign_keys = ON");
|
|
24919
|
+
} catch {}
|
|
24920
|
+
}
|
|
24908
24921
|
return results;
|
|
24909
24922
|
}
|
|
24910
24923
|
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
|
|
@@ -24988,7 +25001,10 @@ var CloudConfigSchema2 = exports_external.object({
|
|
|
24988
25001
|
}).default({}),
|
|
24989
25002
|
mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
|
|
24990
25003
|
auto_sync_interval_minutes: exports_external.number().default(0),
|
|
24991
|
-
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
|
|
25004
|
+
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
|
|
25005
|
+
sync: exports_external.object({
|
|
25006
|
+
schedule_minutes: exports_external.number().default(0)
|
|
25007
|
+
}).default({})
|
|
24992
25008
|
});
|
|
24993
25009
|
var CONFIG_DIR2 = join3(homedir3(), ".hasna", "cloud");
|
|
24994
25010
|
var CONFIG_PATH2 = join3(CONFIG_DIR2, "config.json");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover services under `~/.hasna/` that have a `<service>.db` SQLite file.
|
|
3
|
+
* Returns an array of service names.
|
|
4
|
+
*/
|
|
5
|
+
export declare function discoverSyncableServices(): string[];
|
|
6
|
+
export interface ScheduledSyncResult {
|
|
7
|
+
service: string;
|
|
8
|
+
tables_synced: number;
|
|
9
|
+
total_rows_synced: number;
|
|
10
|
+
errors: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Run a scheduled sync push for all discovered services.
|
|
14
|
+
*
|
|
15
|
+
* - Skips if mode is `local`.
|
|
16
|
+
* - Opens each service's SQLite DB, discovers tables, and pushes to PG.
|
|
17
|
+
* - Returns per-service results.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runScheduledSync(): Promise<ScheduledSyncResult[]>;
|
|
20
|
+
declare const _default: {
|
|
21
|
+
scheduled(): Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
export default _default;
|
|
24
|
+
//# sourceMappingURL=scheduled-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduled-sync.d.ts","sourceRoot":"","sources":["../src/scheduled-sync.ts"],"names":[],"mappings":"AAYA;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,EAAE,CAkBnD;AAMD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA2EvE;;;;AAUD,wBAIE"}
|