@hasna/cloud 0.1.17 → 0.1.19

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
@@ -11343,15 +11343,15 @@ __export(exports_discover, {
11343
11343
  SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS2,
11344
11344
  KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
11345
11345
  });
11346
- import { readdirSync as readdirSync5, existsSync as existsSync7 } from "fs";
11346
+ import { readdirSync as readdirSync5, existsSync as existsSync8 } from "fs";
11347
11347
  import { join as join8 } from "path";
11348
- import { homedir as homedir6 } from "os";
11348
+ import { homedir as homedir7 } from "os";
11349
11349
  function isSyncExcludedTable2(table) {
11350
11350
  return SYNC_EXCLUDED_TABLE_PATTERNS2.some((p) => p.test(table));
11351
11351
  }
11352
11352
  function discoverServices2() {
11353
- const dataDir = join8(homedir6(), ".hasna");
11354
- if (!existsSync7(dataDir))
11353
+ const dataDir = join8(homedir7(), ".hasna");
11354
+ if (!existsSync8(dataDir))
11355
11355
  return [];
11356
11356
  try {
11357
11357
  const entries = readdirSync5(dataDir, { withFileTypes: true });
@@ -11372,8 +11372,8 @@ function discoverSyncableServices2() {
11372
11372
  return local.filter((s) => pgSet.has(s));
11373
11373
  }
11374
11374
  function getServiceDbPath(service) {
11375
- const dataDir = join8(homedir6(), ".hasna", service);
11376
- if (!existsSync7(dataDir))
11375
+ const dataDir = join8(homedir7(), ".hasna", service);
11376
+ if (!existsSync8(dataDir))
11377
11377
  return null;
11378
11378
  const candidates = [
11379
11379
  join8(dataDir, `${service}.db`),
@@ -11389,7 +11389,7 @@ function getServiceDbPath(service) {
11389
11389
  }
11390
11390
  } catch {}
11391
11391
  for (const p of candidates) {
11392
- if (existsSync7(p))
11392
+ if (existsSync8(p))
11393
11393
  return p;
11394
11394
  }
11395
11395
  return null;
@@ -11643,6 +11643,59 @@ async function resolvePrimaryKeys(source, target, table, pkOption) {
11643
11643
  }
11644
11644
  return pks;
11645
11645
  }
11646
+ function pgTypeToSqlite(pgType) {
11647
+ const t = pgType.toLowerCase();
11648
+ if (t.includes("int") || t === "bigint" || t === "smallint" || t === "serial" || t === "bigserial")
11649
+ return "INTEGER";
11650
+ if (t.includes("bool"))
11651
+ return "INTEGER";
11652
+ if (t.includes("float") || t.includes("double") || t === "real" || t === "numeric" || t === "decimal")
11653
+ return "REAL";
11654
+ if (t === "bytea")
11655
+ return "BLOB";
11656
+ return "TEXT";
11657
+ }
11658
+ async function ensureTableInSqliteFromPg(target, source, table) {
11659
+ const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
11660
+ if (existing.length > 0)
11661
+ return false;
11662
+ const cols = await source.all(`SELECT column_name, data_type, is_nullable, column_default
11663
+ FROM information_schema.columns
11664
+ WHERE table_schema = 'public' AND table_name = '${table}'
11665
+ ORDER BY ordinal_position`);
11666
+ if (cols.length === 0)
11667
+ return false;
11668
+ const pkCols = await source.all(`SELECT kcu.column_name
11669
+ FROM information_schema.table_constraints tc
11670
+ JOIN information_schema.key_column_usage kcu
11671
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
11672
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' AND tc.table_name = '${table}'
11673
+ ORDER BY kcu.ordinal_position`);
11674
+ const pkSet = new Set(pkCols.map((c) => c.column_name));
11675
+ const skipTypes = new Set(["tsvector", "tsquery", "user-defined"]);
11676
+ const filteredCols = cols.filter((c) => !skipTypes.has(c.data_type));
11677
+ const colDefs = filteredCols.map((c) => {
11678
+ const sqliteType = pgTypeToSqlite(c.data_type);
11679
+ const notNull = c.is_nullable === "NO" && !pkSet.has(c.column_name) ? " NOT NULL" : "";
11680
+ return `"${c.column_name}" ${sqliteType}${notNull}`;
11681
+ });
11682
+ if (pkSet.size > 0) {
11683
+ const pkList = [...pkSet].map((c) => `"${c}"`).join(", ");
11684
+ colDefs.push(`PRIMARY KEY (${pkList})`);
11685
+ }
11686
+ const sql = `CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`;
11687
+ target.exec(sql);
11688
+ process.stderr.write(` [sync] ${table}: auto-created in SQLite from PG schema
11689
+ `);
11690
+ return true;
11691
+ }
11692
+ async function ensureTablesExist(source, target, tables) {
11693
+ for (const table of tables) {
11694
+ if (!isAsyncAdapter(target) && isAsyncAdapter(source)) {
11695
+ await ensureTableInSqliteFromPg(target, source, table);
11696
+ }
11697
+ }
11698
+ }
11646
11699
  async function filterColumnsForTarget(target, table, sourceColumns) {
11647
11700
  try {
11648
11701
  if (!isAsyncAdapter(target)) {
@@ -11677,6 +11730,7 @@ async function syncTransfer(source, target, options, _direction) {
11677
11730
  } = options;
11678
11731
  const results = [];
11679
11732
  const sqliteTarget = !isAsyncAdapter(target) ? target : null;
11733
+ await ensureTablesExist(source, target, tables);
11680
11734
  if (sqliteTarget) {
11681
11735
  try {
11682
11736
  sqliteTarget.exec("PRAGMA foreign_keys = OFF");
@@ -12131,18 +12185,10 @@ class PgAdapterAsync2 {
12131
12185
  // src/sync-schedule.ts
12132
12186
  init_config();
12133
12187
  import { join as join5, dirname } from "path";
12134
- var CRON_TITLE = "hasna-cloud-sync";
12135
- function getWorkerPath() {
12136
- const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
12137
- const tsPath = join5(dir, "scheduled-sync.ts");
12138
- const jsPath = join5(dir, "scheduled-sync.js");
12139
- try {
12140
- const { existsSync: existsSync5 } = __require("fs");
12141
- if (existsSync5(tsPath))
12142
- return tsPath;
12143
- } catch {}
12144
- return jsPath;
12145
- }
12188
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync5 } from "fs";
12189
+ import { homedir as homedir5, platform } from "os";
12190
+ var SERVICE_NAME = "hasna-cloud-sync";
12191
+ var CONFIG_DIR3 = join5(homedir5(), ".hasna", "cloud");
12146
12192
  function parseInterval(input) {
12147
12193
  const trimmed = input.trim().toLowerCase();
12148
12194
  const hourMatch = trimmed.match(/^(\d+)\s*h$/);
@@ -12181,19 +12227,161 @@ function minutesToCron(minutes) {
12181
12227
  }
12182
12228
  return `*/${minutes} * * * *`;
12183
12229
  }
12230
+ function getWorkerPath() {
12231
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
12232
+ const tsPath = join5(dir, "scheduled-sync.ts");
12233
+ const jsPath = join5(dir, "scheduled-sync.js");
12234
+ try {
12235
+ if (existsSync5(tsPath))
12236
+ return tsPath;
12237
+ } catch {}
12238
+ return jsPath;
12239
+ }
12240
+ function getBunPath() {
12241
+ const candidates = [
12242
+ join5(homedir5(), ".bun", "bin", "bun"),
12243
+ "/usr/local/bin/bun",
12244
+ "/usr/bin/bun"
12245
+ ];
12246
+ for (const p of candidates) {
12247
+ if (existsSync5(p))
12248
+ return p;
12249
+ }
12250
+ return "bun";
12251
+ }
12252
+ function getLaunchdPlistPath() {
12253
+ return join5(homedir5(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
12254
+ }
12255
+ function createLaunchdPlist(intervalMinutes) {
12256
+ const workerPath = getWorkerPath();
12257
+ const bunPath = getBunPath();
12258
+ const logPath = join5(CONFIG_DIR3, "sync.log");
12259
+ const errorLogPath = join5(CONFIG_DIR3, "sync-error.log");
12260
+ return `<?xml version="1.0" encoding="UTF-8"?>
12261
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
12262
+ <plist version="1.0">
12263
+ <dict>
12264
+ <key>Label</key>
12265
+ <string>com.hasna.cloud-sync</string>
12266
+ <key>ProgramArguments</key>
12267
+ <array>
12268
+ <string>${bunPath}</string>
12269
+ <string>run</string>
12270
+ <string>${workerPath}</string>
12271
+ </array>
12272
+ <key>StartInterval</key>
12273
+ <integer>${intervalMinutes * 60}</integer>
12274
+ <key>RunAtLoad</key>
12275
+ <true/>
12276
+ <key>StandardOutPath</key>
12277
+ <string>${logPath}</string>
12278
+ <key>StandardErrorPath</key>
12279
+ <string>${errorLogPath}</string>
12280
+ <key>EnvironmentVariables</key>
12281
+ <dict>
12282
+ <key>PATH</key>
12283
+ <string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
12284
+ <key>HOME</key>
12285
+ <string>${homedir5()}</string>
12286
+ </dict>
12287
+ </dict>
12288
+ </plist>`;
12289
+ }
12290
+ async function registerLaunchd(intervalMinutes) {
12291
+ const plistPath = getLaunchdPlistPath();
12292
+ const plistDir = dirname(plistPath);
12293
+ mkdirSync5(plistDir, { recursive: true });
12294
+ try {
12295
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
12296
+ } catch {}
12297
+ writeFileSync3(plistPath, createLaunchdPlist(intervalMinutes));
12298
+ await Bun.spawn(["launchctl", "load", plistPath]).exited;
12299
+ }
12300
+ async function removeLaunchd() {
12301
+ const plistPath = getLaunchdPlistPath();
12302
+ try {
12303
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
12304
+ } catch {}
12305
+ try {
12306
+ unlinkSync(plistPath);
12307
+ } catch {}
12308
+ }
12309
+ function getSystemdDir() {
12310
+ return join5(homedir5(), ".config", "systemd", "user");
12311
+ }
12312
+ function createSystemdService() {
12313
+ const workerPath = getWorkerPath();
12314
+ const bunPath = getBunPath();
12315
+ return `[Unit]
12316
+ Description=Hasna Cloud Sync
12317
+ After=network.target
12318
+
12319
+ [Service]
12320
+ Type=oneshot
12321
+ ExecStart=${bunPath} run ${workerPath}
12322
+ Environment=HOME=${homedir5()}
12323
+ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
12324
+
12325
+ [Install]
12326
+ WantedBy=default.target
12327
+ `;
12328
+ }
12329
+ function createSystemdTimer(intervalMinutes) {
12330
+ return `[Unit]
12331
+ Description=Hasna Cloud Sync Timer
12332
+
12333
+ [Timer]
12334
+ OnBootSec=${intervalMinutes}min
12335
+ OnUnitActiveSec=${intervalMinutes}min
12336
+ Persistent=true
12337
+
12338
+ [Install]
12339
+ WantedBy=timers.target
12340
+ `;
12341
+ }
12342
+ async function registerSystemd(intervalMinutes) {
12343
+ const dir = getSystemdDir();
12344
+ mkdirSync5(dir, { recursive: true });
12345
+ writeFileSync3(join5(dir, `${SERVICE_NAME}.service`), createSystemdService());
12346
+ writeFileSync3(join5(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
12347
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
12348
+ await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
12349
+ }
12350
+ async function removeSystemd() {
12351
+ try {
12352
+ await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SERVICE_NAME}.timer`]).exited;
12353
+ } catch {}
12354
+ const dir = getSystemdDir();
12355
+ try {
12356
+ unlinkSync(join5(dir, `${SERVICE_NAME}.service`));
12357
+ } catch {}
12358
+ try {
12359
+ unlinkSync(join5(dir, `${SERVICE_NAME}.timer`));
12360
+ } catch {}
12361
+ try {
12362
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
12363
+ } catch {}
12364
+ }
12184
12365
  async function registerSyncSchedule(intervalMinutes) {
12185
12366
  if (intervalMinutes <= 0) {
12186
12367
  throw new Error("Interval must be a positive number of minutes.");
12187
12368
  }
12188
- const cronExpr = minutesToCron(intervalMinutes);
12189
- const workerPath = getWorkerPath();
12190
- await Bun.cron(workerPath, cronExpr, CRON_TITLE);
12369
+ mkdirSync5(CONFIG_DIR3, { recursive: true });
12370
+ if (platform() === "darwin") {
12371
+ await registerLaunchd(intervalMinutes);
12372
+ } else {
12373
+ await registerSystemd(intervalMinutes);
12374
+ }
12191
12375
  const config = getCloudConfig2();
12192
12376
  config.sync.schedule_minutes = intervalMinutes;
12193
12377
  saveCloudConfig2(config);
12194
12378
  }
12195
12379
  async function removeSyncSchedule() {
12196
- await Bun.cron.remove(CRON_TITLE);
12380
+ if (platform() === "darwin") {
12381
+ await removeLaunchd();
12382
+ } else {
12383
+ await removeSystemd();
12384
+ }
12197
12385
  const config = getCloudConfig2();
12198
12386
  config.sync.schedule_minutes = 0;
12199
12387
  saveCloudConfig2(config);
@@ -12202,17 +12390,26 @@ function getSyncScheduleStatus() {
12202
12390
  const config = getCloudConfig2();
12203
12391
  const minutes = config.sync.schedule_minutes;
12204
12392
  const registered = minutes > 0;
12393
+ let mechanism = "none";
12394
+ if (registered) {
12395
+ if (platform() === "darwin") {
12396
+ mechanism = existsSync5(getLaunchdPlistPath()) ? "launchd" : "none";
12397
+ } else {
12398
+ mechanism = existsSync5(join5(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
12399
+ }
12400
+ }
12205
12401
  return {
12206
12402
  registered,
12207
12403
  schedule_minutes: minutes,
12208
- cron_expression: registered ? minutesToCron(minutes) : null
12404
+ cron_expression: registered ? minutesToCron(minutes) : null,
12405
+ mechanism
12209
12406
  };
12210
12407
  }
12211
12408
 
12212
12409
  // src/scheduled-sync.ts
12213
12410
  init_config();
12214
12411
  init_adapter();
12215
- import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
12412
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
12216
12413
  import { join as join6 } from "path";
12217
12414
 
12218
12415
  // src/sync-incremental.ts
@@ -12355,7 +12552,7 @@ function discoverSyncableServices() {
12355
12552
  if (!entry.isDirectory())
12356
12553
  continue;
12357
12554
  const dbPath = join6(hasnaDir, entry.name, `${entry.name}.db`);
12358
- if (existsSync5(dbPath)) {
12555
+ if (existsSync6(dbPath)) {
12359
12556
  services.push(entry.name);
12360
12557
  }
12361
12558
  }
@@ -12378,7 +12575,7 @@ async function runScheduledSync() {
12378
12575
  };
12379
12576
  try {
12380
12577
  const dbPath = join6(getDataDir(service), `${service}.db`);
12381
- if (!existsSync5(dbPath)) {
12578
+ if (!existsSync6(dbPath)) {
12382
12579
  continue;
12383
12580
  }
12384
12581
  const local = new SqliteAdapter(dbPath);
@@ -12421,9 +12618,9 @@ async function runScheduledSync() {
12421
12618
  }
12422
12619
 
12423
12620
  // src/discover.ts
12424
- import { readdirSync as readdirSync4, existsSync as existsSync6 } from "fs";
12621
+ import { readdirSync as readdirSync4, existsSync as existsSync7 } from "fs";
12425
12622
  import { join as join7 } from "path";
12426
- import { homedir as homedir5 } from "os";
12623
+ import { homedir as homedir6 } from "os";
12427
12624
  var SYNC_EXCLUDED_TABLE_PATTERNS = [
12428
12625
  /^sqlite_/,
12429
12626
  /_fts$/,
@@ -12435,8 +12632,8 @@ function isSyncExcludedTable(table) {
12435
12632
  return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
12436
12633
  }
12437
12634
  function discoverServices() {
12438
- const dataDir = join7(homedir5(), ".hasna");
12439
- if (!existsSync6(dataDir))
12635
+ const dataDir = join7(homedir6(), ".hasna");
12636
+ if (!existsSync7(dataDir))
12440
12637
  return [];
12441
12638
  try {
12442
12639
  const entries = readdirSync4(dataDir, { withFileTypes: true });
package/dist/index.js CHANGED
@@ -9347,15 +9347,15 @@ __export(exports_discover, {
9347
9347
  SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
9348
9348
  KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
9349
9349
  });
9350
- import { readdirSync as readdirSync3, existsSync as existsSync5 } from "fs";
9350
+ import { readdirSync as readdirSync3, existsSync as existsSync6 } from "fs";
9351
9351
  import { join as join6 } from "path";
9352
- import { homedir as homedir4 } from "os";
9352
+ import { homedir as homedir5 } from "os";
9353
9353
  function isSyncExcludedTable(table) {
9354
9354
  return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
9355
9355
  }
9356
9356
  function discoverServices() {
9357
- const dataDir = join6(homedir4(), ".hasna");
9358
- if (!existsSync5(dataDir))
9357
+ const dataDir = join6(homedir5(), ".hasna");
9358
+ if (!existsSync6(dataDir))
9359
9359
  return [];
9360
9360
  try {
9361
9361
  const entries = readdirSync3(dataDir, { withFileTypes: true });
@@ -9376,8 +9376,8 @@ function discoverSyncableServices2() {
9376
9376
  return local.filter((s) => pgSet.has(s));
9377
9377
  }
9378
9378
  function getServiceDbPath(service) {
9379
- const dataDir = join6(homedir4(), ".hasna", service);
9380
- if (!existsSync5(dataDir))
9379
+ const dataDir = join6(homedir5(), ".hasna", service);
9380
+ if (!existsSync6(dataDir))
9381
9381
  return null;
9382
9382
  const candidates = [
9383
9383
  join6(dataDir, `${service}.db`),
@@ -9393,7 +9393,7 @@ function getServiceDbPath(service) {
9393
9393
  }
9394
9394
  } catch {}
9395
9395
  for (const p of candidates) {
9396
- if (existsSync5(p))
9396
+ if (existsSync6(p))
9397
9397
  return p;
9398
9398
  }
9399
9399
  return null;
@@ -9571,6 +9571,59 @@ async function resolvePrimaryKeys(source, target, table, pkOption) {
9571
9571
  }
9572
9572
  return pks;
9573
9573
  }
9574
+ function pgTypeToSqlite(pgType) {
9575
+ const t = pgType.toLowerCase();
9576
+ if (t.includes("int") || t === "bigint" || t === "smallint" || t === "serial" || t === "bigserial")
9577
+ return "INTEGER";
9578
+ if (t.includes("bool"))
9579
+ return "INTEGER";
9580
+ if (t.includes("float") || t.includes("double") || t === "real" || t === "numeric" || t === "decimal")
9581
+ return "REAL";
9582
+ if (t === "bytea")
9583
+ return "BLOB";
9584
+ return "TEXT";
9585
+ }
9586
+ async function ensureTableInSqliteFromPg(target, source, table) {
9587
+ const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
9588
+ if (existing.length > 0)
9589
+ return false;
9590
+ const cols = await source.all(`SELECT column_name, data_type, is_nullable, column_default
9591
+ FROM information_schema.columns
9592
+ WHERE table_schema = 'public' AND table_name = '${table}'
9593
+ ORDER BY ordinal_position`);
9594
+ if (cols.length === 0)
9595
+ return false;
9596
+ const pkCols = await source.all(`SELECT kcu.column_name
9597
+ FROM information_schema.table_constraints tc
9598
+ JOIN information_schema.key_column_usage kcu
9599
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
9600
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' AND tc.table_name = '${table}'
9601
+ ORDER BY kcu.ordinal_position`);
9602
+ const pkSet = new Set(pkCols.map((c) => c.column_name));
9603
+ const skipTypes = new Set(["tsvector", "tsquery", "user-defined"]);
9604
+ const filteredCols = cols.filter((c) => !skipTypes.has(c.data_type));
9605
+ const colDefs = filteredCols.map((c) => {
9606
+ const sqliteType = pgTypeToSqlite(c.data_type);
9607
+ const notNull = c.is_nullable === "NO" && !pkSet.has(c.column_name) ? " NOT NULL" : "";
9608
+ return `"${c.column_name}" ${sqliteType}${notNull}`;
9609
+ });
9610
+ if (pkSet.size > 0) {
9611
+ const pkList = [...pkSet].map((c) => `"${c}"`).join(", ");
9612
+ colDefs.push(`PRIMARY KEY (${pkList})`);
9613
+ }
9614
+ const sql = `CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`;
9615
+ target.exec(sql);
9616
+ process.stderr.write(` [sync] ${table}: auto-created in SQLite from PG schema
9617
+ `);
9618
+ return true;
9619
+ }
9620
+ async function ensureTablesExist(source, target, tables) {
9621
+ for (const table of tables) {
9622
+ if (!isAsyncAdapter(target) && isAsyncAdapter(source)) {
9623
+ await ensureTableInSqliteFromPg(target, source, table);
9624
+ }
9625
+ }
9626
+ }
9574
9627
  async function filterColumnsForTarget(target, table, sourceColumns) {
9575
9628
  try {
9576
9629
  if (!isAsyncAdapter(target)) {
@@ -9605,6 +9658,7 @@ async function syncTransfer(source, target, options, _direction) {
9605
9658
  } = options;
9606
9659
  const results = [];
9607
9660
  const sqliteTarget = !isAsyncAdapter(target) ? target : null;
9661
+ await ensureTablesExist(source, target, tables);
9608
9662
  if (sqliteTarget) {
9609
9663
  try {
9610
9664
  sqliteTarget.exec("PRAGMA foreign_keys = OFF");
@@ -10536,18 +10590,10 @@ async function runScheduledSync() {
10536
10590
  // src/sync-schedule.ts
10537
10591
  init_config();
10538
10592
  import { join as join5, dirname } from "path";
10539
- var CRON_TITLE = "hasna-cloud-sync";
10540
- function getWorkerPath() {
10541
- const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
10542
- const tsPath = join5(dir, "scheduled-sync.ts");
10543
- const jsPath = join5(dir, "scheduled-sync.js");
10544
- try {
10545
- const { existsSync: existsSync5 } = __require("fs");
10546
- if (existsSync5(tsPath))
10547
- return tsPath;
10548
- } catch {}
10549
- return jsPath;
10550
- }
10593
+ import { existsSync as existsSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
10594
+ import { homedir as homedir4, platform } from "os";
10595
+ var SERVICE_NAME = "hasna-cloud-sync";
10596
+ var CONFIG_DIR2 = join5(homedir4(), ".hasna", "cloud");
10551
10597
  function parseInterval(input) {
10552
10598
  const trimmed = input.trim().toLowerCase();
10553
10599
  const hourMatch = trimmed.match(/^(\d+)\s*h$/);
@@ -10586,19 +10632,161 @@ function minutesToCron(minutes) {
10586
10632
  }
10587
10633
  return `*/${minutes} * * * *`;
10588
10634
  }
10635
+ function getWorkerPath() {
10636
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
10637
+ const tsPath = join5(dir, "scheduled-sync.ts");
10638
+ const jsPath = join5(dir, "scheduled-sync.js");
10639
+ try {
10640
+ if (existsSync5(tsPath))
10641
+ return tsPath;
10642
+ } catch {}
10643
+ return jsPath;
10644
+ }
10645
+ function getBunPath() {
10646
+ const candidates = [
10647
+ join5(homedir4(), ".bun", "bin", "bun"),
10648
+ "/usr/local/bin/bun",
10649
+ "/usr/bin/bun"
10650
+ ];
10651
+ for (const p of candidates) {
10652
+ if (existsSync5(p))
10653
+ return p;
10654
+ }
10655
+ return "bun";
10656
+ }
10657
+ function getLaunchdPlistPath() {
10658
+ return join5(homedir4(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
10659
+ }
10660
+ function createLaunchdPlist(intervalMinutes) {
10661
+ const workerPath = getWorkerPath();
10662
+ const bunPath = getBunPath();
10663
+ const logPath = join5(CONFIG_DIR2, "sync.log");
10664
+ const errorLogPath = join5(CONFIG_DIR2, "sync-error.log");
10665
+ return `<?xml version="1.0" encoding="UTF-8"?>
10666
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
10667
+ <plist version="1.0">
10668
+ <dict>
10669
+ <key>Label</key>
10670
+ <string>com.hasna.cloud-sync</string>
10671
+ <key>ProgramArguments</key>
10672
+ <array>
10673
+ <string>${bunPath}</string>
10674
+ <string>run</string>
10675
+ <string>${workerPath}</string>
10676
+ </array>
10677
+ <key>StartInterval</key>
10678
+ <integer>${intervalMinutes * 60}</integer>
10679
+ <key>RunAtLoad</key>
10680
+ <true/>
10681
+ <key>StandardOutPath</key>
10682
+ <string>${logPath}</string>
10683
+ <key>StandardErrorPath</key>
10684
+ <string>${errorLogPath}</string>
10685
+ <key>EnvironmentVariables</key>
10686
+ <dict>
10687
+ <key>PATH</key>
10688
+ <string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
10689
+ <key>HOME</key>
10690
+ <string>${homedir4()}</string>
10691
+ </dict>
10692
+ </dict>
10693
+ </plist>`;
10694
+ }
10695
+ async function registerLaunchd(intervalMinutes) {
10696
+ const plistPath = getLaunchdPlistPath();
10697
+ const plistDir = dirname(plistPath);
10698
+ mkdirSync3(plistDir, { recursive: true });
10699
+ try {
10700
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
10701
+ } catch {}
10702
+ writeFileSync2(plistPath, createLaunchdPlist(intervalMinutes));
10703
+ await Bun.spawn(["launchctl", "load", plistPath]).exited;
10704
+ }
10705
+ async function removeLaunchd() {
10706
+ const plistPath = getLaunchdPlistPath();
10707
+ try {
10708
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
10709
+ } catch {}
10710
+ try {
10711
+ unlinkSync(plistPath);
10712
+ } catch {}
10713
+ }
10714
+ function getSystemdDir() {
10715
+ return join5(homedir4(), ".config", "systemd", "user");
10716
+ }
10717
+ function createSystemdService() {
10718
+ const workerPath = getWorkerPath();
10719
+ const bunPath = getBunPath();
10720
+ return `[Unit]
10721
+ Description=Hasna Cloud Sync
10722
+ After=network.target
10723
+
10724
+ [Service]
10725
+ Type=oneshot
10726
+ ExecStart=${bunPath} run ${workerPath}
10727
+ Environment=HOME=${homedir4()}
10728
+ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
10729
+
10730
+ [Install]
10731
+ WantedBy=default.target
10732
+ `;
10733
+ }
10734
+ function createSystemdTimer(intervalMinutes) {
10735
+ return `[Unit]
10736
+ Description=Hasna Cloud Sync Timer
10737
+
10738
+ [Timer]
10739
+ OnBootSec=${intervalMinutes}min
10740
+ OnUnitActiveSec=${intervalMinutes}min
10741
+ Persistent=true
10742
+
10743
+ [Install]
10744
+ WantedBy=timers.target
10745
+ `;
10746
+ }
10747
+ async function registerSystemd(intervalMinutes) {
10748
+ const dir = getSystemdDir();
10749
+ mkdirSync3(dir, { recursive: true });
10750
+ writeFileSync2(join5(dir, `${SERVICE_NAME}.service`), createSystemdService());
10751
+ writeFileSync2(join5(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
10752
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
10753
+ await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
10754
+ }
10755
+ async function removeSystemd() {
10756
+ try {
10757
+ await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SERVICE_NAME}.timer`]).exited;
10758
+ } catch {}
10759
+ const dir = getSystemdDir();
10760
+ try {
10761
+ unlinkSync(join5(dir, `${SERVICE_NAME}.service`));
10762
+ } catch {}
10763
+ try {
10764
+ unlinkSync(join5(dir, `${SERVICE_NAME}.timer`));
10765
+ } catch {}
10766
+ try {
10767
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
10768
+ } catch {}
10769
+ }
10589
10770
  async function registerSyncSchedule(intervalMinutes) {
10590
10771
  if (intervalMinutes <= 0) {
10591
10772
  throw new Error("Interval must be a positive number of minutes.");
10592
10773
  }
10593
- const cronExpr = minutesToCron(intervalMinutes);
10594
- const workerPath = getWorkerPath();
10595
- await Bun.cron(workerPath, cronExpr, CRON_TITLE);
10774
+ mkdirSync3(CONFIG_DIR2, { recursive: true });
10775
+ if (platform() === "darwin") {
10776
+ await registerLaunchd(intervalMinutes);
10777
+ } else {
10778
+ await registerSystemd(intervalMinutes);
10779
+ }
10596
10780
  const config = getCloudConfig();
10597
10781
  config.sync.schedule_minutes = intervalMinutes;
10598
10782
  saveCloudConfig(config);
10599
10783
  }
10600
10784
  async function removeSyncSchedule() {
10601
- await Bun.cron.remove(CRON_TITLE);
10785
+ if (platform() === "darwin") {
10786
+ await removeLaunchd();
10787
+ } else {
10788
+ await removeSystemd();
10789
+ }
10602
10790
  const config = getCloudConfig();
10603
10791
  config.sync.schedule_minutes = 0;
10604
10792
  saveCloudConfig(config);
@@ -10607,10 +10795,19 @@ function getSyncScheduleStatus() {
10607
10795
  const config = getCloudConfig();
10608
10796
  const minutes = config.sync.schedule_minutes;
10609
10797
  const registered = minutes > 0;
10798
+ let mechanism = "none";
10799
+ if (registered) {
10800
+ if (platform() === "darwin") {
10801
+ mechanism = existsSync5(getLaunchdPlistPath()) ? "launchd" : "none";
10802
+ } else {
10803
+ mechanism = existsSync5(join5(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
10804
+ }
10805
+ }
10610
10806
  return {
10611
10807
  registered,
10612
10808
  schedule_minutes: minutes,
10613
- cron_expression: registered ? minutesToCron(minutes) : null
10809
+ cron_expression: registered ? minutesToCron(minutes) : null,
10810
+ mechanism
10614
10811
  };
10615
10812
  }
10616
10813
  // src/pg-migrate.ts
package/dist/mcp/index.js CHANGED
@@ -24786,6 +24786,59 @@ async function resolvePrimaryKeys(source, target, table, pkOption) {
24786
24786
  }
24787
24787
  return pks;
24788
24788
  }
24789
+ function pgTypeToSqlite(pgType) {
24790
+ const t = pgType.toLowerCase();
24791
+ if (t.includes("int") || t === "bigint" || t === "smallint" || t === "serial" || t === "bigserial")
24792
+ return "INTEGER";
24793
+ if (t.includes("bool"))
24794
+ return "INTEGER";
24795
+ if (t.includes("float") || t.includes("double") || t === "real" || t === "numeric" || t === "decimal")
24796
+ return "REAL";
24797
+ if (t === "bytea")
24798
+ return "BLOB";
24799
+ return "TEXT";
24800
+ }
24801
+ async function ensureTableInSqliteFromPg(target, source, table) {
24802
+ const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
24803
+ if (existing.length > 0)
24804
+ return false;
24805
+ const cols = await source.all(`SELECT column_name, data_type, is_nullable, column_default
24806
+ FROM information_schema.columns
24807
+ WHERE table_schema = 'public' AND table_name = '${table}'
24808
+ ORDER BY ordinal_position`);
24809
+ if (cols.length === 0)
24810
+ return false;
24811
+ const pkCols = await source.all(`SELECT kcu.column_name
24812
+ FROM information_schema.table_constraints tc
24813
+ JOIN information_schema.key_column_usage kcu
24814
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
24815
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' AND tc.table_name = '${table}'
24816
+ ORDER BY kcu.ordinal_position`);
24817
+ const pkSet = new Set(pkCols.map((c) => c.column_name));
24818
+ const skipTypes = new Set(["tsvector", "tsquery", "user-defined"]);
24819
+ const filteredCols = cols.filter((c) => !skipTypes.has(c.data_type));
24820
+ const colDefs = filteredCols.map((c) => {
24821
+ const sqliteType = pgTypeToSqlite(c.data_type);
24822
+ const notNull = c.is_nullable === "NO" && !pkSet.has(c.column_name) ? " NOT NULL" : "";
24823
+ return `"${c.column_name}" ${sqliteType}${notNull}`;
24824
+ });
24825
+ if (pkSet.size > 0) {
24826
+ const pkList = [...pkSet].map((c) => `"${c}"`).join(", ");
24827
+ colDefs.push(`PRIMARY KEY (${pkList})`);
24828
+ }
24829
+ const sql = `CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`;
24830
+ target.exec(sql);
24831
+ process.stderr.write(` [sync] ${table}: auto-created in SQLite from PG schema
24832
+ `);
24833
+ return true;
24834
+ }
24835
+ async function ensureTablesExist(source, target, tables) {
24836
+ for (const table of tables) {
24837
+ if (!isAsyncAdapter(target) && isAsyncAdapter(source)) {
24838
+ await ensureTableInSqliteFromPg(target, source, table);
24839
+ }
24840
+ }
24841
+ }
24789
24842
  async function filterColumnsForTarget(target, table, sourceColumns) {
24790
24843
  try {
24791
24844
  if (!isAsyncAdapter(target)) {
@@ -24820,6 +24873,7 @@ async function syncTransfer(source, target, options, _direction) {
24820
24873
  } = options;
24821
24874
  const results = [];
24822
24875
  const sqliteTarget = !isAsyncAdapter(target) ? target : null;
24876
+ await ensureTablesExist(source, target, tables);
24823
24877
  if (sqliteTarget) {
24824
24878
  try {
24825
24879
  sqliteTarget.exec("PRAGMA foreign_keys = OFF");
@@ -9,34 +9,28 @@
9
9
  export declare function parseInterval(input: string): number;
10
10
  /**
11
11
  * Convert minutes to a cron expression.
12
- *
13
- * - For intervals that divide evenly into 60: `*\/<n> * * * *`
14
- * - For hourly multiples: `0 *\/<h> * * *`
15
- * - Otherwise: `*\/<n> * * * *` (best-effort)
16
12
  */
17
13
  export declare function minutesToCron(minutes: number): string;
18
14
  export interface SyncScheduleStatus {
19
15
  registered: boolean;
20
16
  schedule_minutes: number;
21
17
  cron_expression: string | null;
18
+ mechanism: "launchd" | "systemd" | "none";
22
19
  }
23
20
  /**
24
- * Register a Bun.cron job that runs the scheduled sync worker on a fixed
25
- * interval.
21
+ * Register a system-level scheduled sync.
26
22
  *
27
- * - Persists `schedule_minutes` in `~/.hasna/cloud/config.json`.
28
- * - Calls `Bun.cron()` to register an OS-level cron job.
23
+ * - macOS: creates a launchd plist in ~/Library/LaunchAgents/
24
+ * - Linux: creates a systemd user timer in ~/.config/systemd/user/
25
+ * - Persists interval in ~/.hasna/cloud/config.json
29
26
  */
30
27
  export declare function registerSyncSchedule(intervalMinutes: number): Promise<void>;
31
28
  /**
32
- * Remove the registered sync cron job.
33
- *
34
- * - Calls `Bun.cron.remove()` to unregister the OS-level job.
35
- * - Sets `schedule_minutes` to 0 in config.
29
+ * Remove the registered sync schedule.
36
30
  */
37
31
  export declare function removeSyncSchedule(): Promise<void>;
38
32
  /**
39
- * Get the current sync schedule status from config.
33
+ * Get the current sync schedule status.
40
34
  */
41
35
  export declare function getSyncScheduleStatus(): SyncScheduleStatus;
42
36
  //# sourceMappingURL=sync-schedule.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync-schedule.d.ts","sourceRoot":"","sources":["../src/sync-schedule.ts"],"names":[],"mappings":"AAkCA;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiCnD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkBrD;AAMD,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,OAAO,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAOxD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,kBAAkB,CAU1D"}
1
+ {"version":3,"file":"sync-schedule.d.ts","sourceRoot":"","sources":["../src/sync-schedule.ts"],"names":[],"mappings":"AAgBA;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiCnD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAiBrD;AAsKD,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,OAAO,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAUxD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,kBAAkB,CAoB1D"}
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAMnD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,sBAAsB;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kCAAkC;IAClC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAMD;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAMD;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAqoBD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,SAAS,GAAG,MAAM,EAAE,CAKxD;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKxE"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAMnD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,sBAAsB;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kCAAkC;IAClC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAMD;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAMD;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAwuBD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,SAAS,GAAG,MAAM,EAAE,CAKxD;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKxE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/cloud",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Shared cloud infrastructure — database adapter (SQLite + PostgreSQL), sync engine, feedback system, unified dotfile config",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",