@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/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");
|
|
@@ -10247,6 +10250,161 @@ function setupAutoSync(serviceName, server, local, remote, tables) {
|
|
|
10247
10250
|
function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
|
|
10248
10251
|
setupAutoSync(serviceName, mcpServer, local, remote, tables);
|
|
10249
10252
|
}
|
|
10253
|
+
// src/scheduled-sync.ts
|
|
10254
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
|
|
10255
|
+
import { join as join4 } from "path";
|
|
10256
|
+
function discoverSyncableServices() {
|
|
10257
|
+
const hasnaDir = getHasnaDir();
|
|
10258
|
+
const services = [];
|
|
10259
|
+
try {
|
|
10260
|
+
const entries = readdirSync2(hasnaDir, { withFileTypes: true });
|
|
10261
|
+
for (const entry of entries) {
|
|
10262
|
+
if (!entry.isDirectory())
|
|
10263
|
+
continue;
|
|
10264
|
+
const dbPath = join4(hasnaDir, entry.name, `${entry.name}.db`);
|
|
10265
|
+
if (existsSync4(dbPath)) {
|
|
10266
|
+
services.push(entry.name);
|
|
10267
|
+
}
|
|
10268
|
+
}
|
|
10269
|
+
} catch {}
|
|
10270
|
+
return services;
|
|
10271
|
+
}
|
|
10272
|
+
async function runScheduledSync() {
|
|
10273
|
+
const config = getCloudConfig();
|
|
10274
|
+
if (config.mode === "local")
|
|
10275
|
+
return [];
|
|
10276
|
+
const services = discoverSyncableServices();
|
|
10277
|
+
const results = [];
|
|
10278
|
+
let remote = null;
|
|
10279
|
+
for (const service of services) {
|
|
10280
|
+
const result = {
|
|
10281
|
+
service,
|
|
10282
|
+
tables_synced: 0,
|
|
10283
|
+
total_rows_synced: 0,
|
|
10284
|
+
errors: []
|
|
10285
|
+
};
|
|
10286
|
+
try {
|
|
10287
|
+
const dbPath = join4(getDataDir(service), `${service}.db`);
|
|
10288
|
+
if (!existsSync4(dbPath)) {
|
|
10289
|
+
continue;
|
|
10290
|
+
}
|
|
10291
|
+
const local = new SqliteAdapter(dbPath);
|
|
10292
|
+
const tables = listSqliteTables(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
|
|
10293
|
+
if (tables.length === 0) {
|
|
10294
|
+
local.close();
|
|
10295
|
+
continue;
|
|
10296
|
+
}
|
|
10297
|
+
try {
|
|
10298
|
+
const connStr = getConnectionString(service);
|
|
10299
|
+
remote = new PgAdapterAsync(connStr);
|
|
10300
|
+
} catch (err) {
|
|
10301
|
+
result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
|
|
10302
|
+
local.close();
|
|
10303
|
+
results.push(result);
|
|
10304
|
+
continue;
|
|
10305
|
+
}
|
|
10306
|
+
const stats = incrementalSyncPush(local, remote, tables);
|
|
10307
|
+
for (const s of stats) {
|
|
10308
|
+
if (s.errors.length === 0) {
|
|
10309
|
+
result.tables_synced++;
|
|
10310
|
+
}
|
|
10311
|
+
result.total_rows_synced += s.synced_rows;
|
|
10312
|
+
result.errors.push(...s.errors);
|
|
10313
|
+
}
|
|
10314
|
+
local.close();
|
|
10315
|
+
await remote.close();
|
|
10316
|
+
remote = null;
|
|
10317
|
+
} catch (err) {
|
|
10318
|
+
result.errors.push(err?.message ?? String(err));
|
|
10319
|
+
}
|
|
10320
|
+
results.push(result);
|
|
10321
|
+
}
|
|
10322
|
+
if (remote) {
|
|
10323
|
+
try {
|
|
10324
|
+
await remote.close();
|
|
10325
|
+
} catch {}
|
|
10326
|
+
}
|
|
10327
|
+
return results;
|
|
10328
|
+
}
|
|
10329
|
+
// src/sync-schedule.ts
|
|
10330
|
+
import { join as join5, dirname } from "path";
|
|
10331
|
+
var CRON_TITLE = "hasna-cloud-sync";
|
|
10332
|
+
function getWorkerPath() {
|
|
10333
|
+
const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
|
|
10334
|
+
const tsPath = join5(dir, "scheduled-sync.ts");
|
|
10335
|
+
const jsPath = join5(dir, "scheduled-sync.js");
|
|
10336
|
+
try {
|
|
10337
|
+
const { existsSync: existsSync5 } = __require("fs");
|
|
10338
|
+
if (existsSync5(tsPath))
|
|
10339
|
+
return tsPath;
|
|
10340
|
+
} catch {}
|
|
10341
|
+
return jsPath;
|
|
10342
|
+
}
|
|
10343
|
+
function parseInterval(input) {
|
|
10344
|
+
const trimmed = input.trim().toLowerCase();
|
|
10345
|
+
const hourMatch = trimmed.match(/^(\d+)\s*h$/);
|
|
10346
|
+
if (hourMatch) {
|
|
10347
|
+
const hours = parseInt(hourMatch[1], 10);
|
|
10348
|
+
if (hours <= 0) {
|
|
10349
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
10350
|
+
}
|
|
10351
|
+
return hours * 60;
|
|
10352
|
+
}
|
|
10353
|
+
const minMatch = trimmed.match(/^(\d+)\s*m$/);
|
|
10354
|
+
if (minMatch) {
|
|
10355
|
+
const mins = parseInt(minMatch[1], 10);
|
|
10356
|
+
if (mins <= 0) {
|
|
10357
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
10358
|
+
}
|
|
10359
|
+
return mins;
|
|
10360
|
+
}
|
|
10361
|
+
const plain = parseInt(trimmed, 10);
|
|
10362
|
+
if (!isNaN(plain) && plain > 0) {
|
|
10363
|
+
return plain;
|
|
10364
|
+
}
|
|
10365
|
+
throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
|
|
10366
|
+
}
|
|
10367
|
+
function minutesToCron(minutes) {
|
|
10368
|
+
if (minutes <= 0) {
|
|
10369
|
+
throw new Error("Interval must be greater than 0 minutes.");
|
|
10370
|
+
}
|
|
10371
|
+
if (minutes < 60) {
|
|
10372
|
+
return `*/${minutes} * * * *`;
|
|
10373
|
+
}
|
|
10374
|
+
const hours = Math.floor(minutes / 60);
|
|
10375
|
+
const remainderMins = minutes % 60;
|
|
10376
|
+
if (remainderMins === 0 && hours <= 24) {
|
|
10377
|
+
return `0 */${hours} * * *`;
|
|
10378
|
+
}
|
|
10379
|
+
return `*/${minutes} * * * *`;
|
|
10380
|
+
}
|
|
10381
|
+
async function registerSyncSchedule(intervalMinutes) {
|
|
10382
|
+
if (intervalMinutes <= 0) {
|
|
10383
|
+
throw new Error("Interval must be a positive number of minutes.");
|
|
10384
|
+
}
|
|
10385
|
+
const cronExpr = minutesToCron(intervalMinutes);
|
|
10386
|
+
const workerPath = getWorkerPath();
|
|
10387
|
+
await Bun.cron(workerPath, cronExpr, CRON_TITLE);
|
|
10388
|
+
const config = getCloudConfig();
|
|
10389
|
+
config.sync.schedule_minutes = intervalMinutes;
|
|
10390
|
+
saveCloudConfig(config);
|
|
10391
|
+
}
|
|
10392
|
+
async function removeSyncSchedule() {
|
|
10393
|
+
await Bun.cron.remove(CRON_TITLE);
|
|
10394
|
+
const config = getCloudConfig();
|
|
10395
|
+
config.sync.schedule_minutes = 0;
|
|
10396
|
+
saveCloudConfig(config);
|
|
10397
|
+
}
|
|
10398
|
+
function getSyncScheduleStatus() {
|
|
10399
|
+
const config = getCloudConfig();
|
|
10400
|
+
const minutes = config.sync.schedule_minutes;
|
|
10401
|
+
const registered = minutes > 0;
|
|
10402
|
+
return {
|
|
10403
|
+
registered,
|
|
10404
|
+
schedule_minutes: minutes,
|
|
10405
|
+
cron_expression: registered ? minutesToCron(minutes) : null
|
|
10406
|
+
};
|
|
10407
|
+
}
|
|
10250
10408
|
// src/mcp-helpers.ts
|
|
10251
10409
|
function registerCloudTools(server, serviceName) {
|
|
10252
10410
|
server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
|
|
@@ -10439,13 +10597,18 @@ export {
|
|
|
10439
10597
|
sendFeedback,
|
|
10440
10598
|
saveFeedback,
|
|
10441
10599
|
saveCloudConfig,
|
|
10600
|
+
runScheduledSync,
|
|
10442
10601
|
resolveConflicts,
|
|
10443
10602
|
resolveConflict,
|
|
10444
10603
|
resetSyncMeta,
|
|
10445
10604
|
resetAllSyncMeta,
|
|
10605
|
+
removeSyncSchedule,
|
|
10606
|
+
registerSyncSchedule,
|
|
10446
10607
|
registerCloudTools,
|
|
10447
10608
|
registerCloudCommands,
|
|
10448
10609
|
purgeResolvedConflicts,
|
|
10610
|
+
parseInterval,
|
|
10611
|
+
minutesToCron,
|
|
10449
10612
|
migrateDotfile,
|
|
10450
10613
|
listSqliteTables,
|
|
10451
10614
|
listPgTables,
|
|
@@ -10455,6 +10618,7 @@ export {
|
|
|
10455
10618
|
incrementalSyncPull,
|
|
10456
10619
|
hasLegacyDotfile,
|
|
10457
10620
|
getWinningData,
|
|
10621
|
+
getSyncScheduleStatus,
|
|
10458
10622
|
getSyncMetaForTable,
|
|
10459
10623
|
getSyncMetaAll,
|
|
10460
10624
|
getHasnaDir,
|
|
@@ -10470,6 +10634,7 @@ export {
|
|
|
10470
10634
|
ensureFeedbackTable,
|
|
10471
10635
|
ensureConflictsTable,
|
|
10472
10636
|
enableAutoSync,
|
|
10637
|
+
discoverSyncableServices,
|
|
10473
10638
|
detectConflicts,
|
|
10474
10639
|
createDatabase,
|
|
10475
10640
|
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");
|
|
@@ -24988,7 +24991,10 @@ var CloudConfigSchema2 = exports_external.object({
|
|
|
24988
24991
|
}).default({}),
|
|
24989
24992
|
mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
|
|
24990
24993
|
auto_sync_interval_minutes: exports_external.number().default(0),
|
|
24991
|
-
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
|
|
24994
|
+
feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
|
|
24995
|
+
sync: exports_external.object({
|
|
24996
|
+
schedule_minutes: exports_external.number().default(0)
|
|
24997
|
+
}).default({})
|
|
24992
24998
|
});
|
|
24993
24999
|
var CONFIG_DIR2 = join3(homedir3(), ".hasna", "cloud");
|
|
24994
25000
|
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"}
|