@hasna/cloud 0.1.16 → 0.1.18

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.
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -11039,7 +11039,8 @@ class PgAdapter {
11039
11039
  _client = null;
11040
11040
  constructor(arg) {
11041
11041
  if (typeof arg === "string") {
11042
- this.pool = new esm_default.Pool({ connectionString: arg });
11042
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
11043
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
11043
11044
  } else {
11044
11045
  this.pool = arg;
11045
11046
  }
@@ -11166,7 +11167,8 @@ class PgAdapterAsync {
11166
11167
  pool;
11167
11168
  constructor(arg) {
11168
11169
  if (typeof arg === "string") {
11169
- this.pool = new esm_default.Pool({ connectionString: arg });
11170
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
11171
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
11170
11172
  } else {
11171
11173
  this.pool = arg;
11172
11174
  }
@@ -11290,7 +11292,10 @@ function getConnectionString2(dbName) {
11290
11292
  if (!host || !username) {
11291
11293
  throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
11292
11294
  }
11293
- const password = process.env[password_env] ?? "";
11295
+ const password = process.env[password_env];
11296
+ if (password === undefined || password === "") {
11297
+ throw new Error(`RDS password not set. Export ${password_env} in your shell or add it to ~/.secrets/hasna/rds/live.env`);
11298
+ }
11294
11299
  const sslParam = ssl ? "?sslmode=require" : "";
11295
11300
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
11296
11301
  }
@@ -11317,7 +11322,7 @@ var init_config = __esm(() => {
11317
11322
  password_env: exports_external.string().default("HASNA_RDS_PASSWORD"),
11318
11323
  ssl: exports_external.boolean().default(true)
11319
11324
  }).default({}),
11320
- mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
11325
+ mode: exports_external.enum(["local", "cloud", "hybrid"]).default("hybrid"),
11321
11326
  auto_sync_interval_minutes: exports_external.number().default(0),
11322
11327
  feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
11323
11328
  sync: exports_external.object({
@@ -11338,15 +11343,15 @@ __export(exports_discover, {
11338
11343
  SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS2,
11339
11344
  KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
11340
11345
  });
11341
- import { readdirSync as readdirSync5, existsSync as existsSync7 } from "fs";
11346
+ import { readdirSync as readdirSync5, existsSync as existsSync8 } from "fs";
11342
11347
  import { join as join8 } from "path";
11343
- import { homedir as homedir6 } from "os";
11348
+ import { homedir as homedir7 } from "os";
11344
11349
  function isSyncExcludedTable2(table) {
11345
11350
  return SYNC_EXCLUDED_TABLE_PATTERNS2.some((p) => p.test(table));
11346
11351
  }
11347
11352
  function discoverServices2() {
11348
- const dataDir = join8(homedir6(), ".hasna");
11349
- if (!existsSync7(dataDir))
11353
+ const dataDir = join8(homedir7(), ".hasna");
11354
+ if (!existsSync8(dataDir))
11350
11355
  return [];
11351
11356
  try {
11352
11357
  const entries = readdirSync5(dataDir, { withFileTypes: true });
@@ -11367,8 +11372,8 @@ function discoverSyncableServices2() {
11367
11372
  return local.filter((s) => pgSet.has(s));
11368
11373
  }
11369
11374
  function getServiceDbPath(service) {
11370
- const dataDir = join8(homedir6(), ".hasna", service);
11371
- if (!existsSync7(dataDir))
11375
+ const dataDir = join8(homedir7(), ".hasna", service);
11376
+ if (!existsSync8(dataDir))
11372
11377
  return null;
11373
11378
  const candidates = [
11374
11379
  join8(dataDir, `${service}.db`),
@@ -11384,7 +11389,7 @@ function getServiceDbPath(service) {
11384
11389
  }
11385
11390
  } catch {}
11386
11391
  for (const p of candidates) {
11387
- if (existsSync7(p))
11392
+ if (existsSync8(p))
11388
11393
  return p;
11389
11394
  }
11390
11395
  return null;
@@ -11468,7 +11473,7 @@ var CloudConfigSchema = exports_external.object({
11468
11473
  password_env: exports_external.string().default("HASNA_RDS_PASSWORD"),
11469
11474
  ssl: exports_external.boolean().default(true)
11470
11475
  }).default({}),
11471
- mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
11476
+ mode: exports_external.enum(["local", "cloud", "hybrid"]).default("hybrid"),
11472
11477
  auto_sync_interval_minutes: exports_external.number().default(0),
11473
11478
  feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
11474
11479
  sync: exports_external.object({
@@ -11499,7 +11504,10 @@ function getConnectionString(dbName) {
11499
11504
  if (!host || !username) {
11500
11505
  throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
11501
11506
  }
11502
- const password = process.env[password_env] ?? "";
11507
+ const password = process.env[password_env];
11508
+ if (password === undefined || password === "") {
11509
+ throw new Error(`RDS password not set. Export ${password_env} in your shell or add it to ~/.secrets/hasna/rds/live.env`);
11510
+ }
11503
11511
  const sslParam = ssl ? "?sslmode=require" : "";
11504
11512
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
11505
11513
  }
@@ -12067,7 +12075,8 @@ class PgAdapterAsync2 {
12067
12075
  pool;
12068
12076
  constructor(arg) {
12069
12077
  if (typeof arg === "string") {
12070
- this.pool = new esm_default.Pool({ connectionString: arg });
12078
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
12079
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
12071
12080
  } else {
12072
12081
  this.pool = arg;
12073
12082
  }
@@ -12122,18 +12131,10 @@ class PgAdapterAsync2 {
12122
12131
  // src/sync-schedule.ts
12123
12132
  init_config();
12124
12133
  import { join as join5, dirname } from "path";
12125
- var CRON_TITLE = "hasna-cloud-sync";
12126
- function getWorkerPath() {
12127
- const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
12128
- const tsPath = join5(dir, "scheduled-sync.ts");
12129
- const jsPath = join5(dir, "scheduled-sync.js");
12130
- try {
12131
- const { existsSync: existsSync5 } = __require("fs");
12132
- if (existsSync5(tsPath))
12133
- return tsPath;
12134
- } catch {}
12135
- return jsPath;
12136
- }
12134
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync5 } from "fs";
12135
+ import { homedir as homedir5, platform } from "os";
12136
+ var SERVICE_NAME = "hasna-cloud-sync";
12137
+ var CONFIG_DIR3 = join5(homedir5(), ".hasna", "cloud");
12137
12138
  function parseInterval(input) {
12138
12139
  const trimmed = input.trim().toLowerCase();
12139
12140
  const hourMatch = trimmed.match(/^(\d+)\s*h$/);
@@ -12172,19 +12173,161 @@ function minutesToCron(minutes) {
12172
12173
  }
12173
12174
  return `*/${minutes} * * * *`;
12174
12175
  }
12176
+ function getWorkerPath() {
12177
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
12178
+ const tsPath = join5(dir, "scheduled-sync.ts");
12179
+ const jsPath = join5(dir, "scheduled-sync.js");
12180
+ try {
12181
+ if (existsSync5(tsPath))
12182
+ return tsPath;
12183
+ } catch {}
12184
+ return jsPath;
12185
+ }
12186
+ function getBunPath() {
12187
+ const candidates = [
12188
+ join5(homedir5(), ".bun", "bin", "bun"),
12189
+ "/usr/local/bin/bun",
12190
+ "/usr/bin/bun"
12191
+ ];
12192
+ for (const p of candidates) {
12193
+ if (existsSync5(p))
12194
+ return p;
12195
+ }
12196
+ return "bun";
12197
+ }
12198
+ function getLaunchdPlistPath() {
12199
+ return join5(homedir5(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
12200
+ }
12201
+ function createLaunchdPlist(intervalMinutes) {
12202
+ const workerPath = getWorkerPath();
12203
+ const bunPath = getBunPath();
12204
+ const logPath = join5(CONFIG_DIR3, "sync.log");
12205
+ const errorLogPath = join5(CONFIG_DIR3, "sync-error.log");
12206
+ return `<?xml version="1.0" encoding="UTF-8"?>
12207
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
12208
+ <plist version="1.0">
12209
+ <dict>
12210
+ <key>Label</key>
12211
+ <string>com.hasna.cloud-sync</string>
12212
+ <key>ProgramArguments</key>
12213
+ <array>
12214
+ <string>${bunPath}</string>
12215
+ <string>run</string>
12216
+ <string>${workerPath}</string>
12217
+ </array>
12218
+ <key>StartInterval</key>
12219
+ <integer>${intervalMinutes * 60}</integer>
12220
+ <key>RunAtLoad</key>
12221
+ <true/>
12222
+ <key>StandardOutPath</key>
12223
+ <string>${logPath}</string>
12224
+ <key>StandardErrorPath</key>
12225
+ <string>${errorLogPath}</string>
12226
+ <key>EnvironmentVariables</key>
12227
+ <dict>
12228
+ <key>PATH</key>
12229
+ <string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
12230
+ <key>HOME</key>
12231
+ <string>${homedir5()}</string>
12232
+ </dict>
12233
+ </dict>
12234
+ </plist>`;
12235
+ }
12236
+ async function registerLaunchd(intervalMinutes) {
12237
+ const plistPath = getLaunchdPlistPath();
12238
+ const plistDir = dirname(plistPath);
12239
+ mkdirSync5(plistDir, { recursive: true });
12240
+ try {
12241
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
12242
+ } catch {}
12243
+ writeFileSync3(plistPath, createLaunchdPlist(intervalMinutes));
12244
+ await Bun.spawn(["launchctl", "load", plistPath]).exited;
12245
+ }
12246
+ async function removeLaunchd() {
12247
+ const plistPath = getLaunchdPlistPath();
12248
+ try {
12249
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
12250
+ } catch {}
12251
+ try {
12252
+ unlinkSync(plistPath);
12253
+ } catch {}
12254
+ }
12255
+ function getSystemdDir() {
12256
+ return join5(homedir5(), ".config", "systemd", "user");
12257
+ }
12258
+ function createSystemdService() {
12259
+ const workerPath = getWorkerPath();
12260
+ const bunPath = getBunPath();
12261
+ return `[Unit]
12262
+ Description=Hasna Cloud Sync
12263
+ After=network.target
12264
+
12265
+ [Service]
12266
+ Type=oneshot
12267
+ ExecStart=${bunPath} run ${workerPath}
12268
+ Environment=HOME=${homedir5()}
12269
+ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
12270
+
12271
+ [Install]
12272
+ WantedBy=default.target
12273
+ `;
12274
+ }
12275
+ function createSystemdTimer(intervalMinutes) {
12276
+ return `[Unit]
12277
+ Description=Hasna Cloud Sync Timer
12278
+
12279
+ [Timer]
12280
+ OnBootSec=${intervalMinutes}min
12281
+ OnUnitActiveSec=${intervalMinutes}min
12282
+ Persistent=true
12283
+
12284
+ [Install]
12285
+ WantedBy=timers.target
12286
+ `;
12287
+ }
12288
+ async function registerSystemd(intervalMinutes) {
12289
+ const dir = getSystemdDir();
12290
+ mkdirSync5(dir, { recursive: true });
12291
+ writeFileSync3(join5(dir, `${SERVICE_NAME}.service`), createSystemdService());
12292
+ writeFileSync3(join5(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
12293
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
12294
+ await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
12295
+ }
12296
+ async function removeSystemd() {
12297
+ try {
12298
+ await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SERVICE_NAME}.timer`]).exited;
12299
+ } catch {}
12300
+ const dir = getSystemdDir();
12301
+ try {
12302
+ unlinkSync(join5(dir, `${SERVICE_NAME}.service`));
12303
+ } catch {}
12304
+ try {
12305
+ unlinkSync(join5(dir, `${SERVICE_NAME}.timer`));
12306
+ } catch {}
12307
+ try {
12308
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
12309
+ } catch {}
12310
+ }
12175
12311
  async function registerSyncSchedule(intervalMinutes) {
12176
12312
  if (intervalMinutes <= 0) {
12177
12313
  throw new Error("Interval must be a positive number of minutes.");
12178
12314
  }
12179
- const cronExpr = minutesToCron(intervalMinutes);
12180
- const workerPath = getWorkerPath();
12181
- await Bun.cron(workerPath, cronExpr, CRON_TITLE);
12315
+ mkdirSync5(CONFIG_DIR3, { recursive: true });
12316
+ if (platform() === "darwin") {
12317
+ await registerLaunchd(intervalMinutes);
12318
+ } else {
12319
+ await registerSystemd(intervalMinutes);
12320
+ }
12182
12321
  const config = getCloudConfig2();
12183
12322
  config.sync.schedule_minutes = intervalMinutes;
12184
12323
  saveCloudConfig2(config);
12185
12324
  }
12186
12325
  async function removeSyncSchedule() {
12187
- await Bun.cron.remove(CRON_TITLE);
12326
+ if (platform() === "darwin") {
12327
+ await removeLaunchd();
12328
+ } else {
12329
+ await removeSystemd();
12330
+ }
12188
12331
  const config = getCloudConfig2();
12189
12332
  config.sync.schedule_minutes = 0;
12190
12333
  saveCloudConfig2(config);
@@ -12193,17 +12336,26 @@ function getSyncScheduleStatus() {
12193
12336
  const config = getCloudConfig2();
12194
12337
  const minutes = config.sync.schedule_minutes;
12195
12338
  const registered = minutes > 0;
12339
+ let mechanism = "none";
12340
+ if (registered) {
12341
+ if (platform() === "darwin") {
12342
+ mechanism = existsSync5(getLaunchdPlistPath()) ? "launchd" : "none";
12343
+ } else {
12344
+ mechanism = existsSync5(join5(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
12345
+ }
12346
+ }
12196
12347
  return {
12197
12348
  registered,
12198
12349
  schedule_minutes: minutes,
12199
- cron_expression: registered ? minutesToCron(minutes) : null
12350
+ cron_expression: registered ? minutesToCron(minutes) : null,
12351
+ mechanism
12200
12352
  };
12201
12353
  }
12202
12354
 
12203
12355
  // src/scheduled-sync.ts
12204
12356
  init_config();
12205
12357
  init_adapter();
12206
- import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
12358
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
12207
12359
  import { join as join6 } from "path";
12208
12360
 
12209
12361
  // src/sync-incremental.ts
@@ -12346,7 +12498,7 @@ function discoverSyncableServices() {
12346
12498
  if (!entry.isDirectory())
12347
12499
  continue;
12348
12500
  const dbPath = join6(hasnaDir, entry.name, `${entry.name}.db`);
12349
- if (existsSync5(dbPath)) {
12501
+ if (existsSync6(dbPath)) {
12350
12502
  services.push(entry.name);
12351
12503
  }
12352
12504
  }
@@ -12369,7 +12521,7 @@ async function runScheduledSync() {
12369
12521
  };
12370
12522
  try {
12371
12523
  const dbPath = join6(getDataDir(service), `${service}.db`);
12372
- if (!existsSync5(dbPath)) {
12524
+ if (!existsSync6(dbPath)) {
12373
12525
  continue;
12374
12526
  }
12375
12527
  const local = new SqliteAdapter(dbPath);
@@ -12412,9 +12564,9 @@ async function runScheduledSync() {
12412
12564
  }
12413
12565
 
12414
12566
  // src/discover.ts
12415
- import { readdirSync as readdirSync4, existsSync as existsSync6 } from "fs";
12567
+ import { readdirSync as readdirSync4, existsSync as existsSync7 } from "fs";
12416
12568
  import { join as join7 } from "path";
12417
- import { homedir as homedir5 } from "os";
12569
+ import { homedir as homedir6 } from "os";
12418
12570
  var SYNC_EXCLUDED_TABLE_PATTERNS = [
12419
12571
  /^sqlite_/,
12420
12572
  /_fts$/,
@@ -12426,8 +12578,8 @@ function isSyncExcludedTable(table) {
12426
12578
  return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
12427
12579
  }
12428
12580
  function discoverServices() {
12429
- const dataDir = join7(homedir5(), ".hasna");
12430
- if (!existsSync6(dataDir))
12581
+ const dataDir = join7(homedir6(), ".hasna");
12582
+ if (!existsSync7(dataDir))
12431
12583
  return [];
12432
12584
  try {
12433
12585
  const entries = readdirSync4(dataDir, { withFileTypes: true });
package/dist/config.d.ts CHANGED
@@ -7,17 +7,17 @@ export declare const CloudConfigSchema: z.ZodObject<{
7
7
  password_env: z.ZodDefault<z.ZodString>;
8
8
  ssl: z.ZodDefault<z.ZodBoolean>;
9
9
  }, "strip", z.ZodTypeAny, {
10
+ ssl: boolean;
10
11
  host: string;
11
12
  port: number;
12
13
  username: string;
13
14
  password_env: string;
14
- ssl: boolean;
15
15
  }, {
16
+ ssl?: boolean | undefined;
16
17
  host?: string | undefined;
17
18
  port?: number | undefined;
18
19
  username?: string | undefined;
19
20
  password_env?: string | undefined;
20
- ssl?: boolean | undefined;
21
21
  }>>;
22
22
  mode: z.ZodDefault<z.ZodEnum<["local", "cloud", "hybrid"]>>;
23
23
  auto_sync_interval_minutes: z.ZodDefault<z.ZodNumber>;
@@ -31,11 +31,11 @@ export declare const CloudConfigSchema: z.ZodObject<{
31
31
  }>>;
32
32
  }, "strip", z.ZodTypeAny, {
33
33
  rds: {
34
+ ssl: boolean;
34
35
  host: string;
35
36
  port: number;
36
37
  username: string;
37
38
  password_env: string;
38
- ssl: boolean;
39
39
  };
40
40
  mode: "local" | "cloud" | "hybrid";
41
41
  auto_sync_interval_minutes: number;
@@ -45,11 +45,11 @@ export declare const CloudConfigSchema: z.ZodObject<{
45
45
  };
46
46
  }, {
47
47
  rds?: {
48
+ ssl?: boolean | undefined;
48
49
  host?: string | undefined;
49
50
  port?: number | undefined;
50
51
  username?: string | undefined;
51
52
  password_env?: string | undefined;
52
- ssl?: boolean | undefined;
53
53
  } | undefined;
54
54
  mode?: "local" | "cloud" | "hybrid" | undefined;
55
55
  auto_sync_interval_minutes?: number | undefined;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
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,CAoB1D;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.js CHANGED
@@ -5048,7 +5048,8 @@ class PgAdapter {
5048
5048
  _client = null;
5049
5049
  constructor(arg) {
5050
5050
  if (typeof arg === "string") {
5051
- this.pool = new esm_default.Pool({ connectionString: arg });
5051
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
5052
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
5052
5053
  } else {
5053
5054
  this.pool = arg;
5054
5055
  }
@@ -5175,7 +5176,8 @@ class PgAdapterAsync {
5175
5176
  pool;
5176
5177
  constructor(arg) {
5177
5178
  if (typeof arg === "string") {
5178
- this.pool = new esm_default.Pool({ connectionString: arg });
5179
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
5180
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
5179
5181
  } else {
5180
5182
  this.pool = arg;
5181
5183
  }
@@ -9294,7 +9296,10 @@ function getConnectionString(dbName) {
9294
9296
  if (!host || !username) {
9295
9297
  throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
9296
9298
  }
9297
- const password = process.env[password_env] ?? "";
9299
+ const password = process.env[password_env];
9300
+ if (password === undefined || password === "") {
9301
+ throw new Error(`RDS password not set. Export ${password_env} in your shell or add it to ~/.secrets/hasna/rds/live.env`);
9302
+ }
9298
9303
  const sslParam = ssl ? "?sslmode=require" : "";
9299
9304
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
9300
9305
  }
@@ -9321,7 +9326,7 @@ var init_config = __esm(() => {
9321
9326
  password_env: exports_external.string().default("HASNA_RDS_PASSWORD"),
9322
9327
  ssl: exports_external.boolean().default(true)
9323
9328
  }).default({}),
9324
- mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
9329
+ mode: exports_external.enum(["local", "cloud", "hybrid"]).default("hybrid"),
9325
9330
  auto_sync_interval_minutes: exports_external.number().default(0),
9326
9331
  feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
9327
9332
  sync: exports_external.object({
@@ -9342,15 +9347,15 @@ __export(exports_discover, {
9342
9347
  SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
9343
9348
  KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
9344
9349
  });
9345
- import { readdirSync as readdirSync3, existsSync as existsSync5 } from "fs";
9350
+ import { readdirSync as readdirSync3, existsSync as existsSync6 } from "fs";
9346
9351
  import { join as join6 } from "path";
9347
- import { homedir as homedir4 } from "os";
9352
+ import { homedir as homedir5 } from "os";
9348
9353
  function isSyncExcludedTable(table) {
9349
9354
  return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
9350
9355
  }
9351
9356
  function discoverServices() {
9352
- const dataDir = join6(homedir4(), ".hasna");
9353
- if (!existsSync5(dataDir))
9357
+ const dataDir = join6(homedir5(), ".hasna");
9358
+ if (!existsSync6(dataDir))
9354
9359
  return [];
9355
9360
  try {
9356
9361
  const entries = readdirSync3(dataDir, { withFileTypes: true });
@@ -9371,8 +9376,8 @@ function discoverSyncableServices2() {
9371
9376
  return local.filter((s) => pgSet.has(s));
9372
9377
  }
9373
9378
  function getServiceDbPath(service) {
9374
- const dataDir = join6(homedir4(), ".hasna", service);
9375
- if (!existsSync5(dataDir))
9379
+ const dataDir = join6(homedir5(), ".hasna", service);
9380
+ if (!existsSync6(dataDir))
9376
9381
  return null;
9377
9382
  const candidates = [
9378
9383
  join6(dataDir, `${service}.db`),
@@ -9388,7 +9393,7 @@ function getServiceDbPath(service) {
9388
9393
  }
9389
9394
  } catch {}
9390
9395
  for (const p of candidates) {
9391
- if (existsSync5(p))
9396
+ if (existsSync6(p))
9392
9397
  return p;
9393
9398
  }
9394
9399
  return null;
@@ -10531,18 +10536,10 @@ async function runScheduledSync() {
10531
10536
  // src/sync-schedule.ts
10532
10537
  init_config();
10533
10538
  import { join as join5, dirname } from "path";
10534
- var CRON_TITLE = "hasna-cloud-sync";
10535
- function getWorkerPath() {
10536
- const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
10537
- const tsPath = join5(dir, "scheduled-sync.ts");
10538
- const jsPath = join5(dir, "scheduled-sync.js");
10539
- try {
10540
- const { existsSync: existsSync5 } = __require("fs");
10541
- if (existsSync5(tsPath))
10542
- return tsPath;
10543
- } catch {}
10544
- return jsPath;
10545
- }
10539
+ import { existsSync as existsSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
10540
+ import { homedir as homedir4, platform } from "os";
10541
+ var SERVICE_NAME = "hasna-cloud-sync";
10542
+ var CONFIG_DIR2 = join5(homedir4(), ".hasna", "cloud");
10546
10543
  function parseInterval(input) {
10547
10544
  const trimmed = input.trim().toLowerCase();
10548
10545
  const hourMatch = trimmed.match(/^(\d+)\s*h$/);
@@ -10581,19 +10578,161 @@ function minutesToCron(minutes) {
10581
10578
  }
10582
10579
  return `*/${minutes} * * * *`;
10583
10580
  }
10581
+ function getWorkerPath() {
10582
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
10583
+ const tsPath = join5(dir, "scheduled-sync.ts");
10584
+ const jsPath = join5(dir, "scheduled-sync.js");
10585
+ try {
10586
+ if (existsSync5(tsPath))
10587
+ return tsPath;
10588
+ } catch {}
10589
+ return jsPath;
10590
+ }
10591
+ function getBunPath() {
10592
+ const candidates = [
10593
+ join5(homedir4(), ".bun", "bin", "bun"),
10594
+ "/usr/local/bin/bun",
10595
+ "/usr/bin/bun"
10596
+ ];
10597
+ for (const p of candidates) {
10598
+ if (existsSync5(p))
10599
+ return p;
10600
+ }
10601
+ return "bun";
10602
+ }
10603
+ function getLaunchdPlistPath() {
10604
+ return join5(homedir4(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
10605
+ }
10606
+ function createLaunchdPlist(intervalMinutes) {
10607
+ const workerPath = getWorkerPath();
10608
+ const bunPath = getBunPath();
10609
+ const logPath = join5(CONFIG_DIR2, "sync.log");
10610
+ const errorLogPath = join5(CONFIG_DIR2, "sync-error.log");
10611
+ return `<?xml version="1.0" encoding="UTF-8"?>
10612
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
10613
+ <plist version="1.0">
10614
+ <dict>
10615
+ <key>Label</key>
10616
+ <string>com.hasna.cloud-sync</string>
10617
+ <key>ProgramArguments</key>
10618
+ <array>
10619
+ <string>${bunPath}</string>
10620
+ <string>run</string>
10621
+ <string>${workerPath}</string>
10622
+ </array>
10623
+ <key>StartInterval</key>
10624
+ <integer>${intervalMinutes * 60}</integer>
10625
+ <key>RunAtLoad</key>
10626
+ <true/>
10627
+ <key>StandardOutPath</key>
10628
+ <string>${logPath}</string>
10629
+ <key>StandardErrorPath</key>
10630
+ <string>${errorLogPath}</string>
10631
+ <key>EnvironmentVariables</key>
10632
+ <dict>
10633
+ <key>PATH</key>
10634
+ <string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
10635
+ <key>HOME</key>
10636
+ <string>${homedir4()}</string>
10637
+ </dict>
10638
+ </dict>
10639
+ </plist>`;
10640
+ }
10641
+ async function registerLaunchd(intervalMinutes) {
10642
+ const plistPath = getLaunchdPlistPath();
10643
+ const plistDir = dirname(plistPath);
10644
+ mkdirSync3(plistDir, { recursive: true });
10645
+ try {
10646
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
10647
+ } catch {}
10648
+ writeFileSync2(plistPath, createLaunchdPlist(intervalMinutes));
10649
+ await Bun.spawn(["launchctl", "load", plistPath]).exited;
10650
+ }
10651
+ async function removeLaunchd() {
10652
+ const plistPath = getLaunchdPlistPath();
10653
+ try {
10654
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
10655
+ } catch {}
10656
+ try {
10657
+ unlinkSync(plistPath);
10658
+ } catch {}
10659
+ }
10660
+ function getSystemdDir() {
10661
+ return join5(homedir4(), ".config", "systemd", "user");
10662
+ }
10663
+ function createSystemdService() {
10664
+ const workerPath = getWorkerPath();
10665
+ const bunPath = getBunPath();
10666
+ return `[Unit]
10667
+ Description=Hasna Cloud Sync
10668
+ After=network.target
10669
+
10670
+ [Service]
10671
+ Type=oneshot
10672
+ ExecStart=${bunPath} run ${workerPath}
10673
+ Environment=HOME=${homedir4()}
10674
+ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
10675
+
10676
+ [Install]
10677
+ WantedBy=default.target
10678
+ `;
10679
+ }
10680
+ function createSystemdTimer(intervalMinutes) {
10681
+ return `[Unit]
10682
+ Description=Hasna Cloud Sync Timer
10683
+
10684
+ [Timer]
10685
+ OnBootSec=${intervalMinutes}min
10686
+ OnUnitActiveSec=${intervalMinutes}min
10687
+ Persistent=true
10688
+
10689
+ [Install]
10690
+ WantedBy=timers.target
10691
+ `;
10692
+ }
10693
+ async function registerSystemd(intervalMinutes) {
10694
+ const dir = getSystemdDir();
10695
+ mkdirSync3(dir, { recursive: true });
10696
+ writeFileSync2(join5(dir, `${SERVICE_NAME}.service`), createSystemdService());
10697
+ writeFileSync2(join5(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
10698
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
10699
+ await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
10700
+ }
10701
+ async function removeSystemd() {
10702
+ try {
10703
+ await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SERVICE_NAME}.timer`]).exited;
10704
+ } catch {}
10705
+ const dir = getSystemdDir();
10706
+ try {
10707
+ unlinkSync(join5(dir, `${SERVICE_NAME}.service`));
10708
+ } catch {}
10709
+ try {
10710
+ unlinkSync(join5(dir, `${SERVICE_NAME}.timer`));
10711
+ } catch {}
10712
+ try {
10713
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
10714
+ } catch {}
10715
+ }
10584
10716
  async function registerSyncSchedule(intervalMinutes) {
10585
10717
  if (intervalMinutes <= 0) {
10586
10718
  throw new Error("Interval must be a positive number of minutes.");
10587
10719
  }
10588
- const cronExpr = minutesToCron(intervalMinutes);
10589
- const workerPath = getWorkerPath();
10590
- await Bun.cron(workerPath, cronExpr, CRON_TITLE);
10720
+ mkdirSync3(CONFIG_DIR2, { recursive: true });
10721
+ if (platform() === "darwin") {
10722
+ await registerLaunchd(intervalMinutes);
10723
+ } else {
10724
+ await registerSystemd(intervalMinutes);
10725
+ }
10591
10726
  const config = getCloudConfig();
10592
10727
  config.sync.schedule_minutes = intervalMinutes;
10593
10728
  saveCloudConfig(config);
10594
10729
  }
10595
10730
  async function removeSyncSchedule() {
10596
- await Bun.cron.remove(CRON_TITLE);
10731
+ if (platform() === "darwin") {
10732
+ await removeLaunchd();
10733
+ } else {
10734
+ await removeSystemd();
10735
+ }
10597
10736
  const config = getCloudConfig();
10598
10737
  config.sync.schedule_minutes = 0;
10599
10738
  saveCloudConfig(config);
@@ -10602,10 +10741,19 @@ function getSyncScheduleStatus() {
10602
10741
  const config = getCloudConfig();
10603
10742
  const minutes = config.sync.schedule_minutes;
10604
10743
  const registered = minutes > 0;
10744
+ let mechanism = "none";
10745
+ if (registered) {
10746
+ if (platform() === "darwin") {
10747
+ mechanism = existsSync5(getLaunchdPlistPath()) ? "launchd" : "none";
10748
+ } else {
10749
+ mechanism = existsSync5(join5(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
10750
+ }
10751
+ }
10605
10752
  return {
10606
10753
  registered,
10607
10754
  schedule_minutes: minutes,
10608
- cron_expression: registered ? minutesToCron(minutes) : null
10755
+ cron_expression: registered ? minutesToCron(minutes) : null,
10756
+ mechanism
10609
10757
  };
10610
10758
  }
10611
10759
  // src/pg-migrate.ts
package/dist/mcp/index.js CHANGED
@@ -24468,7 +24468,8 @@ class PgAdapter {
24468
24468
  _client = null;
24469
24469
  constructor(arg) {
24470
24470
  if (typeof arg === "string") {
24471
- this.pool = new esm_default.Pool({ connectionString: arg });
24471
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
24472
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
24472
24473
  } else {
24473
24474
  this.pool = arg;
24474
24475
  }
@@ -24620,7 +24621,7 @@ var CloudConfigSchema = exports_external.object({
24620
24621
  password_env: exports_external.string().default("HASNA_RDS_PASSWORD"),
24621
24622
  ssl: exports_external.boolean().default(true)
24622
24623
  }).default({}),
24623
- mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
24624
+ mode: exports_external.enum(["local", "cloud", "hybrid"]).default("hybrid"),
24624
24625
  auto_sync_interval_minutes: exports_external.number().default(0),
24625
24626
  feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
24626
24627
  sync: exports_external.object({
@@ -24646,7 +24647,10 @@ function getConnectionString(dbName) {
24646
24647
  if (!host || !username) {
24647
24648
  throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
24648
24649
  }
24649
- const password = process.env[password_env] ?? "";
24650
+ const password = process.env[password_env];
24651
+ if (password === undefined || password === "") {
24652
+ throw new Error(`RDS password not set. Export ${password_env} in your shell or add it to ~/.secrets/hasna/rds/live.env`);
24653
+ }
24650
24654
  const sslParam = ssl ? "?sslmode=require" : "";
24651
24655
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
24652
24656
  }
@@ -25051,7 +25055,7 @@ var CloudConfigSchema2 = exports_external.object({
25051
25055
  password_env: exports_external.string().default("HASNA_RDS_PASSWORD"),
25052
25056
  ssl: exports_external.boolean().default(true)
25053
25057
  }).default({}),
25054
- mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
25058
+ mode: exports_external.enum(["local", "cloud", "hybrid"]).default("hybrid"),
25055
25059
  auto_sync_interval_minutes: exports_external.number().default(0),
25056
25060
  feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
25057
25061
  sync: exports_external.object({
@@ -25220,7 +25224,8 @@ class PgAdapterAsync {
25220
25224
  pool;
25221
25225
  constructor(arg) {
25222
25226
  if (typeof arg === "string") {
25223
- this.pool = new esm_default.Pool({ connectionString: arg });
25227
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
25228
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
25224
25229
  } else {
25225
25230
  this.pool = arg;
25226
25231
  }
@@ -9014,7 +9014,8 @@ class PgAdapterAsync {
9014
9014
  pool;
9015
9015
  constructor(arg) {
9016
9016
  if (typeof arg === "string") {
9017
- this.pool = new esm_default.Pool({ connectionString: arg });
9017
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
9018
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
9018
9019
  } else {
9019
9020
  this.pool = arg;
9020
9021
  }
@@ -9095,7 +9096,7 @@ var CloudConfigSchema = exports_external.object({
9095
9096
  password_env: exports_external.string().default("HASNA_RDS_PASSWORD"),
9096
9097
  ssl: exports_external.boolean().default(true)
9097
9098
  }).default({}),
9098
- mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
9099
+ mode: exports_external.enum(["local", "cloud", "hybrid"]).default("hybrid"),
9099
9100
  auto_sync_interval_minutes: exports_external.number().default(0),
9100
9101
  feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
9101
9102
  sync: exports_external.object({
@@ -9121,7 +9122,10 @@ function getConnectionString(dbName) {
9121
9122
  if (!host || !username) {
9122
9123
  throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
9123
9124
  }
9124
- const password = process.env[password_env] ?? "";
9125
+ const password = process.env[password_env];
9126
+ if (password === undefined || password === "") {
9127
+ throw new Error(`RDS password not set. Export ${password_env} in your shell or add it to ~/.secrets/hasna/rds/live.env`);
9128
+ }
9125
9129
  const sslParam = ssl ? "?sslmode=require" : "";
9126
9130
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
9127
9131
  }
@@ -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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/cloud",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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",