@hasna/cloud 0.1.30 → 0.1.32

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.
Files changed (45) hide show
  1. package/LICENSE +2 -1
  2. package/README.md +17 -0
  3. package/dist/adapter.test.d.ts +2 -0
  4. package/dist/adapter.test.d.ts.map +1 -0
  5. package/dist/auto-sync.d.ts.map +1 -1
  6. package/dist/cli/cmd-doctor.d.ts +3 -0
  7. package/dist/cli/cmd-doctor.d.ts.map +1 -0
  8. package/dist/cli/cmd-feedback.d.ts +3 -0
  9. package/dist/cli/cmd-feedback.d.ts.map +1 -0
  10. package/dist/cli/cmd-migrate.d.ts +3 -0
  11. package/dist/cli/cmd-migrate.d.ts.map +1 -0
  12. package/dist/cli/cmd-setup.d.ts +3 -0
  13. package/dist/cli/cmd-setup.d.ts.map +1 -0
  14. package/dist/cli/cmd-sync.d.ts +4 -0
  15. package/dist/cli/cmd-sync.d.ts.map +1 -0
  16. package/dist/cli/index.js +2330 -1079
  17. package/dist/config.d.ts +138 -4
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/daemon-sync.d.ts +108 -0
  20. package/dist/daemon-sync.d.ts.map +1 -0
  21. package/dist/dialect.test.d.ts +2 -0
  22. package/dist/dialect.test.d.ts.map +1 -0
  23. package/dist/discover.test.d.ts +2 -0
  24. package/dist/discover.test.d.ts.map +1 -0
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1143 -153
  28. package/dist/machines.d.ts +63 -0
  29. package/dist/machines.d.ts.map +1 -0
  30. package/dist/mcp/http.d.ts +27 -0
  31. package/dist/mcp/http.d.ts.map +1 -0
  32. package/dist/mcp/index.d.ts +2 -1
  33. package/dist/mcp/index.d.ts.map +1 -1
  34. package/dist/mcp/index.js +2125 -438
  35. package/dist/scheduled-sync.js +205 -44
  36. package/dist/sync-conflicts.test.d.ts +2 -0
  37. package/dist/sync-conflicts.test.d.ts.map +1 -0
  38. package/dist/sync-incremental.d.ts +5 -0
  39. package/dist/sync-incremental.d.ts.map +1 -1
  40. package/dist/sync-schedule.test.d.ts +2 -0
  41. package/dist/sync-schedule.test.d.ts.map +1 -0
  42. package/dist/sync.d.ts.map +1 -1
  43. package/dist/sync.test.d.ts +2 -0
  44. package/dist/sync.test.d.ts.map +1 -0
  45. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -156,7 +156,7 @@ var require_arrayParser = __commonJS((exports, module) => {
156
156
  };
157
157
  });
158
158
 
159
- // node_modules/pg-types/node_modules/postgres-date/index.js
159
+ // node_modules/postgres-date/index.js
160
160
  var require_postgres_date = __commonJS((exports, module) => {
161
161
  var DATE_TIME = /(\d{1,})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?.*?( BC)?$/;
162
162
  var DATE = /^(\d{1,})-(\d{2})-(\d{2})( BC)?$/;
@@ -258,7 +258,7 @@ var require_mutable = __commonJS((exports, module) => {
258
258
  }
259
259
  });
260
260
 
261
- // node_modules/pg-types/node_modules/postgres-interval/index.js
261
+ // node_modules/postgres-interval/index.js
262
262
  var require_postgres_interval = __commonJS((exports, module) => {
263
263
  var extend = require_mutable();
264
264
  module.exports = PostgresInterval;
@@ -350,7 +350,7 @@ var require_postgres_interval = __commonJS((exports, module) => {
350
350
  }
351
351
  });
352
352
 
353
- // node_modules/pg-types/node_modules/postgres-bytea/index.js
353
+ // node_modules/postgres-bytea/index.js
354
354
  var require_postgres_bytea = __commonJS((exports, module) => {
355
355
  var bufferFrom = Buffer.from || Buffer;
356
356
  module.exports = function parseBytea(input) {
@@ -9267,89 +9267,6 @@ var init_dotfile = __esm(() => {
9267
9267
  HASNA_DIR = join(homedir(), ".hasna");
9268
9268
  });
9269
9269
 
9270
- // src/config.ts
9271
- var exports_config = {};
9272
- __export(exports_config, {
9273
- saveCloudConfig: () => saveCloudConfig,
9274
- getConnectionString: () => getConnectionString,
9275
- getConfigPath: () => getConfigPath,
9276
- getConfigDir: () => getConfigDir,
9277
- getCloudConfig: () => getCloudConfig,
9278
- createDatabase: () => createDatabase,
9279
- CloudConfigSchema: () => CloudConfigSchema
9280
- });
9281
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
9282
- import { homedir as homedir2 } from "os";
9283
- import { join as join2 } from "path";
9284
- function getConfigDir() {
9285
- return CONFIG_DIR;
9286
- }
9287
- function getConfigPath() {
9288
- return CONFIG_PATH;
9289
- }
9290
- function getCloudConfig() {
9291
- if (!existsSync2(CONFIG_PATH)) {
9292
- return CloudConfigSchema.parse({});
9293
- }
9294
- try {
9295
- const raw = readFileSync(CONFIG_PATH, "utf-8");
9296
- return CloudConfigSchema.parse(JSON.parse(raw));
9297
- } catch {
9298
- return CloudConfigSchema.parse({});
9299
- }
9300
- }
9301
- function saveCloudConfig(config) {
9302
- mkdirSync2(CONFIG_DIR, { recursive: true });
9303
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
9304
- `, "utf-8");
9305
- }
9306
- function getConnectionString(dbName) {
9307
- const config = getCloudConfig();
9308
- const { host, port, username, password_env, ssl } = config.rds;
9309
- if (!host || !username) {
9310
- throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
9311
- }
9312
- const password = process.env[password_env];
9313
- if (password === undefined || password === "") {
9314
- throw new Error(`RDS password not set. Export ${password_env} in your shell or add it to ~/.secrets/hasna/rds/live.env`);
9315
- }
9316
- const sslParam = ssl ? "?sslmode=require" : "";
9317
- return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
9318
- }
9319
- function createDatabase(options) {
9320
- const config = getCloudConfig();
9321
- const mode = options.mode ?? config.mode;
9322
- if (mode === "cloud") {
9323
- const connStr = options.pgConnectionString ?? getConnectionString(options.service);
9324
- return new PgAdapter(connStr);
9325
- }
9326
- const dbPath = options.sqlitePath ?? getDbPath(options.service);
9327
- return new SqliteAdapter(dbPath);
9328
- }
9329
- var CloudConfigSchema, CONFIG_DIR, CONFIG_PATH;
9330
- var init_config = __esm(() => {
9331
- init_zod();
9332
- init_adapter();
9333
- init_dotfile();
9334
- CloudConfigSchema = exports_external.object({
9335
- rds: exports_external.object({
9336
- host: exports_external.string().default(""),
9337
- port: exports_external.number().default(5432),
9338
- username: exports_external.string().default(""),
9339
- password_env: exports_external.string().default("HASNA_RDS_PASSWORD"),
9340
- ssl: exports_external.boolean().default(true)
9341
- }).default({}),
9342
- mode: exports_external.enum(["local", "cloud", "hybrid"]).default("hybrid"),
9343
- auto_sync_interval_minutes: exports_external.number().default(0),
9344
- feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
9345
- sync: exports_external.object({
9346
- schedule_minutes: exports_external.number().default(0)
9347
- }).default({})
9348
- });
9349
- CONFIG_DIR = join2(homedir2(), ".hasna", "cloud");
9350
- CONFIG_PATH = join2(CONFIG_DIR, "config.json");
9351
- });
9352
-
9353
9270
  // src/discover.ts
9354
9271
  var exports_discover = {};
9355
9272
  __export(exports_discover, {
@@ -9360,15 +9277,15 @@ __export(exports_discover, {
9360
9277
  SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
9361
9278
  KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
9362
9279
  });
9363
- import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
9364
- import { join as join3 } from "path";
9365
- import { homedir as homedir3 } from "os";
9280
+ import { readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
9281
+ import { join as join2 } from "path";
9282
+ import { homedir as homedir2 } from "os";
9366
9283
  function isSyncExcludedTable(table) {
9367
9284
  return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
9368
9285
  }
9369
9286
  function discoverServices() {
9370
- const dataDir = join3(homedir3(), ".hasna");
9371
- if (!existsSync3(dataDir))
9287
+ const dataDir = join2(homedir2(), ".hasna");
9288
+ if (!existsSync2(dataDir))
9372
9289
  return [];
9373
9290
  try {
9374
9291
  const entries = readdirSync2(dataDir, { withFileTypes: true });
@@ -9389,24 +9306,24 @@ function discoverSyncableServices() {
9389
9306
  return local.filter((s) => pgSet.has(s));
9390
9307
  }
9391
9308
  function getServiceDbPath(service) {
9392
- const dataDir = join3(homedir3(), ".hasna", service);
9393
- if (!existsSync3(dataDir))
9309
+ const dataDir = join2(homedir2(), ".hasna", service);
9310
+ if (!existsSync2(dataDir))
9394
9311
  return null;
9395
9312
  const candidates = [
9396
- join3(dataDir, `${service}.db`),
9397
- join3(dataDir, "data.db"),
9398
- join3(dataDir, "database.db")
9313
+ join2(dataDir, `${service}.db`),
9314
+ join2(dataDir, "data.db"),
9315
+ join2(dataDir, "database.db")
9399
9316
  ];
9400
9317
  try {
9401
9318
  const files = readdirSync2(dataDir);
9402
9319
  for (const f of files) {
9403
9320
  if (f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm")) {
9404
- candidates.push(join3(dataDir, f));
9321
+ candidates.push(join2(dataDir, f));
9405
9322
  }
9406
9323
  }
9407
9324
  } catch {}
9408
9325
  for (const p of candidates) {
9409
- if (existsSync3(p))
9326
+ if (existsSync2(p))
9410
9327
  return p;
9411
9328
  }
9412
9329
  return null;
@@ -9459,9 +9376,515 @@ var init_discover = __esm(() => {
9459
9376
  ];
9460
9377
  });
9461
9378
 
9379
+ // src/machines.ts
9380
+ import { spawnSync } from "child_process";
9381
+ import { existsSync as existsSync3 } from "fs";
9382
+ import { homedir as homedir3, hostname, platform, arch, userInfo } from "os";
9383
+ import { dirname, join as join3 } from "path";
9384
+ function quoteSqlString(value) {
9385
+ return `'${value.replace(/'/g, "''")}'`;
9386
+ }
9387
+ function normalizePlatform(value) {
9388
+ if (value === "darwin")
9389
+ return "macos";
9390
+ if (value === "win32")
9391
+ return "windows";
9392
+ return value;
9393
+ }
9394
+ function detectWorkspacePath() {
9395
+ const home = homedir3();
9396
+ const candidates = [join3(home, "workspace"), join3(home, "Workspace")];
9397
+ for (const candidate of candidates) {
9398
+ if (existsSync3(candidate))
9399
+ return candidate;
9400
+ }
9401
+ const cwd = process.cwd();
9402
+ const workspaceIdx = cwd.indexOf("/workspace/");
9403
+ if (workspaceIdx >= 0) {
9404
+ return cwd.slice(0, workspaceIdx + "/workspace".length);
9405
+ }
9406
+ const workspaceUpperIdx = cwd.indexOf("/Workspace/");
9407
+ if (workspaceUpperIdx >= 0) {
9408
+ return cwd.slice(0, workspaceUpperIdx + "/Workspace".length);
9409
+ }
9410
+ return cwd;
9411
+ }
9412
+ function detectBunPath() {
9413
+ return dirname(process.execPath);
9414
+ }
9415
+ function toFlag(value, fallback = 0) {
9416
+ if (value === undefined)
9417
+ return fallback;
9418
+ return value ? 1 : 0;
9419
+ }
9420
+ function getCurrentMachineId() {
9421
+ return hostname();
9422
+ }
9423
+ function detectCurrentMachine(opts = {}) {
9424
+ const id = opts.id ?? getCurrentMachineId();
9425
+ const username = userInfo().username;
9426
+ return {
9427
+ id,
9428
+ ssh_address: opts.ssh_address ?? `${username}@${id}`,
9429
+ arch: opts.arch ?? `${normalizePlatform(platform())}-${arch()}`,
9430
+ workspace_path: opts.workspace_path ?? detectWorkspacePath(),
9431
+ bun_path: opts.bun_path ?? detectBunPath(),
9432
+ is_primary: opts.is_primary,
9433
+ archived: opts.archived,
9434
+ last_seen_at: opts.last_seen_at,
9435
+ registered_at: opts.registered_at
9436
+ };
9437
+ }
9438
+ function ensureMachinesTable(db) {
9439
+ db.exec(MACHINES_TABLE_SQL);
9440
+ }
9441
+ function getMachineRecord(db, id) {
9442
+ ensureMachinesTable(db);
9443
+ return db.get(`SELECT id, ssh_address, arch, workspace_path, bun_path, is_primary, last_seen_at, registered_at, archived
9444
+ FROM machines
9445
+ WHERE id = ?`, id) ?? null;
9446
+ }
9447
+ function registerMachine(db, opts = {}) {
9448
+ ensureMachinesTable(db);
9449
+ const detected = detectCurrentMachine(opts);
9450
+ const id = detected.id ?? getCurrentMachineId();
9451
+ const now = new Date().toISOString();
9452
+ const existing = getMachineRecord(db, id);
9453
+ const isPrimary = toFlag(detected.is_primary, existing?.is_primary ?? 0);
9454
+ const archived = toFlag(detected.archived, existing?.archived ?? 0);
9455
+ if (isPrimary === 1 && archived === 1) {
9456
+ throw new Error(`Primary machine "${id}" cannot be archived.`);
9457
+ }
9458
+ const record = {
9459
+ id,
9460
+ ssh_address: detected.ssh_address ?? existing?.ssh_address ?? "",
9461
+ arch: detected.arch ?? existing?.arch ?? "",
9462
+ workspace_path: detected.workspace_path ?? existing?.workspace_path ?? "",
9463
+ bun_path: detected.bun_path ?? existing?.bun_path ?? "",
9464
+ is_primary: isPrimary,
9465
+ last_seen_at: detected.last_seen_at ?? now,
9466
+ registered_at: existing?.registered_at ?? detected.registered_at ?? now,
9467
+ archived
9468
+ };
9469
+ db.run(`INSERT INTO machines (
9470
+ id,
9471
+ ssh_address,
9472
+ arch,
9473
+ workspace_path,
9474
+ bun_path,
9475
+ is_primary,
9476
+ last_seen_at,
9477
+ registered_at,
9478
+ archived
9479
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
9480
+ ON CONFLICT(id) DO UPDATE SET
9481
+ ssh_address = excluded.ssh_address,
9482
+ arch = excluded.arch,
9483
+ workspace_path = excluded.workspace_path,
9484
+ bun_path = excluded.bun_path,
9485
+ is_primary = excluded.is_primary,
9486
+ last_seen_at = excluded.last_seen_at,
9487
+ registered_at = excluded.registered_at,
9488
+ archived = excluded.archived`, record.id, record.ssh_address, record.arch, record.workspace_path, record.bun_path, record.is_primary, record.last_seen_at, record.registered_at, record.archived);
9489
+ return getMachineRecord(db, record.id) ?? record;
9490
+ }
9491
+ function listMachines(db, opts = {}) {
9492
+ ensureMachinesTable(db);
9493
+ const includeArchived = opts.includeArchived ?? false;
9494
+ const whereClause = includeArchived ? "" : "WHERE archived = 0";
9495
+ return db.all(`SELECT id, ssh_address, arch, workspace_path, bun_path, is_primary, last_seen_at, registered_at, archived
9496
+ FROM machines
9497
+ ${whereClause}
9498
+ ORDER BY is_primary DESC, id ASC`);
9499
+ }
9500
+ function pingMachine(machine) {
9501
+ const record = typeof machine === "string" ? {
9502
+ id: machine,
9503
+ ssh_address: machine
9504
+ } : machine;
9505
+ const startedAt = Date.now();
9506
+ const checkedAt = new Date().toISOString();
9507
+ const currentId = getCurrentMachineId();
9508
+ if (record.id === currentId) {
9509
+ return {
9510
+ id: record.id,
9511
+ online: true,
9512
+ checked_at: checkedAt,
9513
+ latency_ms: Date.now() - startedAt
9514
+ };
9515
+ }
9516
+ const target = record.ssh_address || record.id;
9517
+ if (!target) {
9518
+ return {
9519
+ id: record.id,
9520
+ online: false,
9521
+ error: "Machine has no ssh target.",
9522
+ checked_at: checkedAt,
9523
+ latency_ms: Date.now() - startedAt
9524
+ };
9525
+ }
9526
+ const result = spawnSync("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=5", target, "true"], {
9527
+ encoding: "utf-8",
9528
+ timeout: 6000
9529
+ });
9530
+ return {
9531
+ id: record.id,
9532
+ online: result.status === 0,
9533
+ error: result.status === 0 ? undefined : (result.stderr || result.error?.message || "SSH health check failed").trim(),
9534
+ checked_at: checkedAt,
9535
+ latency_ms: Date.now() - startedAt
9536
+ };
9537
+ }
9538
+ function getMachineStatus(db, opts = {}) {
9539
+ return listMachines(db, opts).map((machine) => pingMachine(machine));
9540
+ }
9541
+ function tableExists(db, table) {
9542
+ try {
9543
+ if (typeof db.query === "function") {
9544
+ const rows2 = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`, table);
9545
+ return rows2.length > 0;
9546
+ }
9547
+ const rows = db.all(`SELECT table_name
9548
+ FROM information_schema.tables
9549
+ WHERE table_schema = 'public' AND table_name = ?`, table);
9550
+ return rows.length > 0;
9551
+ } catch {
9552
+ return false;
9553
+ }
9554
+ }
9555
+ function hasMachineIdColumn(db, table) {
9556
+ try {
9557
+ if (typeof db.query === "function") {
9558
+ const rows2 = db.all(`PRAGMA table_info("${table}")`);
9559
+ return rows2.some((row) => row.name === "machine_id");
9560
+ }
9561
+ const rows = db.all(`SELECT column_name
9562
+ FROM information_schema.columns
9563
+ WHERE table_schema = 'public' AND table_name = ? AND column_name = 'machine_id'`, table);
9564
+ return rows.length > 0;
9565
+ } catch {
9566
+ return false;
9567
+ }
9568
+ }
9569
+ function shouldTrackMachineId(table) {
9570
+ return table !== "machines" && !isSyncExcludedTable(table);
9571
+ }
9572
+ function ensureMachineIdColumn(db, table) {
9573
+ if (!shouldTrackMachineId(table) || !tableExists(db, table)) {
9574
+ return false;
9575
+ }
9576
+ if (hasMachineIdColumn(db, table)) {
9577
+ return false;
9578
+ }
9579
+ db.exec(`ALTER TABLE "${table}" ADD COLUMN machine_id TEXT DEFAULT ''`);
9580
+ return true;
9581
+ }
9582
+ function extractTableName(sql, pattern) {
9583
+ const match = sql.match(pattern);
9584
+ if (!match)
9585
+ return null;
9586
+ return match[2] ?? match[3] ?? null;
9587
+ }
9588
+ function appendLiteralToValueTuples(sql, literal) {
9589
+ let output = "";
9590
+ let depth = 0;
9591
+ let inSingle = false;
9592
+ let inDouble = false;
9593
+ let sawTuple = false;
9594
+ let inValues = true;
9595
+ for (let i = 0;i < sql.length; i++) {
9596
+ const char = sql[i];
9597
+ const prev = sql[i - 1];
9598
+ if (!inDouble && char === "'" && prev !== "\\") {
9599
+ inSingle = !inSingle;
9600
+ output += char;
9601
+ continue;
9602
+ }
9603
+ if (!inSingle && char === `"` && prev !== "\\") {
9604
+ inDouble = !inDouble;
9605
+ output += char;
9606
+ continue;
9607
+ }
9608
+ if (inSingle || inDouble) {
9609
+ output += char;
9610
+ continue;
9611
+ }
9612
+ if (inValues && char === "(") {
9613
+ depth += 1;
9614
+ if (depth === 1)
9615
+ sawTuple = true;
9616
+ output += char;
9617
+ continue;
9618
+ }
9619
+ if (inValues && char === ")" && depth === 1) {
9620
+ output += `, ${literal})`;
9621
+ depth = 0;
9622
+ continue;
9623
+ }
9624
+ if (inValues && char === ")" && depth > 1) {
9625
+ depth -= 1;
9626
+ output += char;
9627
+ continue;
9628
+ }
9629
+ if (inValues && depth === 0 && sawTuple && /[A-Za-z]/.test(char)) {
9630
+ inValues = false;
9631
+ }
9632
+ output += char;
9633
+ }
9634
+ return sawTuple ? output : null;
9635
+ }
9636
+ function rewriteInsertSql(sql, machineId) {
9637
+ const match = sql.match(/^\s*(insert(?:\s+or\s+\w+)?\s+into\s+((?:"([^"]+)")|([A-Za-z_][\w$]*))\s*)\(([^)]*)\)(\s*values\s*)([\s\S]*)$/i);
9638
+ if (!match)
9639
+ return null;
9640
+ const table = match[3] ?? match[4];
9641
+ if (!table || !shouldTrackMachineId(table))
9642
+ return null;
9643
+ const columns = match[5].split(",").map((column) => column.trim().replace(/^"|"$/g, ""));
9644
+ if (columns.some((column) => column.toLowerCase() === "machine_id")) {
9645
+ return { table, sql };
9646
+ }
9647
+ const rewrittenValues = appendLiteralToValueTuples(match[7], quoteSqlString(machineId));
9648
+ if (!rewrittenValues)
9649
+ return { table, sql };
9650
+ const nextColumns = `${match[5].trim()}, "machine_id"`;
9651
+ return {
9652
+ table,
9653
+ sql: `${match[1]}(${nextColumns})${match[6]}${rewrittenValues}`
9654
+ };
9655
+ }
9656
+ function rewriteUpdateSql(sql, machineId) {
9657
+ const match = sql.match(/^\s*(update\s+((?:"([^"]+)")|([A-Za-z_][\w$]*))\s+set\s*)([\s\S]*?)(\s+(?:where|returning)\b[\s\S]*|\s*)$/i);
9658
+ if (!match)
9659
+ return null;
9660
+ const table = match[3] ?? match[4];
9661
+ if (!table || !shouldTrackMachineId(table))
9662
+ return null;
9663
+ if (/\bmachine_id\b/i.test(match[5])) {
9664
+ return { table, sql };
9665
+ }
9666
+ return {
9667
+ table,
9668
+ sql: `${match[1]}${match[5].trimEnd()}, "machine_id" = ${quoteSqlString(machineId)}${match[6]}`
9669
+ };
9670
+ }
9671
+ function maybeRewriteMachineSql(db, sql, machineId) {
9672
+ const trimmed = sql.trimStart();
9673
+ if (/^insert\b/i.test(trimmed)) {
9674
+ const rewritten = rewriteInsertSql(sql, machineId);
9675
+ if (rewritten?.table) {
9676
+ ensureMachineIdColumn(db, rewritten.table);
9677
+ return rewritten.sql;
9678
+ }
9679
+ return sql;
9680
+ }
9681
+ if (/^update\b/i.test(trimmed)) {
9682
+ const rewritten = rewriteUpdateSql(sql, machineId);
9683
+ if (rewritten?.table) {
9684
+ ensureMachineIdColumn(db, rewritten.table);
9685
+ return rewritten.sql;
9686
+ }
9687
+ return sql;
9688
+ }
9689
+ return sql;
9690
+ }
9691
+ function maybeEnsureCreatedTableHasMachineId(db, sql) {
9692
+ const table = extractTableName(sql, /^\s*(create\s+table(?:\s+if\s+not\s+exists)?\s+((?:"([^"]+)")|([A-Za-z_][\w$]*)))/i);
9693
+ if (table) {
9694
+ ensureMachineIdColumn(db, table);
9695
+ }
9696
+ }
9697
+ function createMachineRegistry(db, machineId = getCurrentMachineId()) {
9698
+ return {
9699
+ register(opts = {}) {
9700
+ return registerMachine(db, { ...opts, id: opts.id ?? machineId });
9701
+ },
9702
+ list(opts = {}) {
9703
+ return listMachines(db, opts);
9704
+ },
9705
+ ping(machine) {
9706
+ if (!machine) {
9707
+ return pingMachine(registerMachine(db, { id: machineId }));
9708
+ }
9709
+ return pingMachine(typeof machine === "string" ? getMachineRecord(db, machine) ?? machine : machine);
9710
+ },
9711
+ status(opts = {}) {
9712
+ return getMachineStatus(db, opts);
9713
+ },
9714
+ currentMachine() {
9715
+ return registerMachine(db, { id: machineId });
9716
+ }
9717
+ };
9718
+ }
9719
+ function createMachineAwareAdapter(db) {
9720
+ ensureMachinesTable(db);
9721
+ const machineId = registerMachine(db).id;
9722
+ const machines = createMachineRegistry(db, machineId);
9723
+ const wrapped = {
9724
+ machine_id: machineId,
9725
+ machines,
9726
+ run(sql, ...params) {
9727
+ return db.run(maybeRewriteMachineSql(db, sql, machineId), ...params);
9728
+ },
9729
+ get(sql, ...params) {
9730
+ return db.get(sql, ...params);
9731
+ },
9732
+ all(sql, ...params) {
9733
+ return db.all(sql, ...params);
9734
+ },
9735
+ exec(sql) {
9736
+ db.exec(sql);
9737
+ maybeEnsureCreatedTableHasMachineId(db, sql);
9738
+ },
9739
+ prepare(sql) {
9740
+ const statement = db.prepare(maybeRewriteMachineSql(db, sql, machineId));
9741
+ return {
9742
+ run(...params) {
9743
+ return statement.run(...params);
9744
+ },
9745
+ get(...params) {
9746
+ return statement.get(...params);
9747
+ },
9748
+ all(...params) {
9749
+ return statement.all(...params);
9750
+ },
9751
+ finalize() {
9752
+ statement.finalize();
9753
+ }
9754
+ };
9755
+ },
9756
+ close() {
9757
+ db.close();
9758
+ },
9759
+ transaction(fn) {
9760
+ return db.transaction(fn);
9761
+ },
9762
+ raw: db.raw,
9763
+ query: typeof db.query === "function" ? db.query.bind(db) : undefined
9764
+ };
9765
+ return wrapped;
9766
+ }
9767
+ var MACHINES_TABLE_SQL = `
9768
+ CREATE TABLE IF NOT EXISTS machines (
9769
+ id TEXT PRIMARY KEY,
9770
+ ssh_address TEXT DEFAULT '',
9771
+ arch TEXT DEFAULT '',
9772
+ workspace_path TEXT DEFAULT '',
9773
+ bun_path TEXT DEFAULT '',
9774
+ is_primary INTEGER DEFAULT 0 CHECK (is_primary IN (0, 1)),
9775
+ last_seen_at TEXT,
9776
+ registered_at TEXT,
9777
+ archived INTEGER DEFAULT 0 CHECK (archived IN (0, 1)),
9778
+ CHECK (NOT (is_primary = 1 AND archived = 1))
9779
+ )`;
9780
+ var init_machines = __esm(() => {
9781
+ init_discover();
9782
+ });
9783
+
9784
+ // src/config.ts
9785
+ var exports_config = {};
9786
+ __export(exports_config, {
9787
+ saveCloudConfig: () => saveCloudConfig,
9788
+ getConnectionString: () => getConnectionString,
9789
+ getConfigPath: () => getConfigPath,
9790
+ getConfigDir: () => getConfigDir,
9791
+ getCloudConfig: () => getCloudConfig,
9792
+ createDatabase: () => createDatabase,
9793
+ CloudConfigSchema: () => CloudConfigSchema
9794
+ });
9795
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
9796
+ import { homedir as homedir4 } from "os";
9797
+ import { join as join4 } from "path";
9798
+ function getConfigDir() {
9799
+ return CONFIG_DIR;
9800
+ }
9801
+ function getConfigPath() {
9802
+ return CONFIG_PATH;
9803
+ }
9804
+ function getCloudConfig() {
9805
+ if (!existsSync4(CONFIG_PATH)) {
9806
+ return CloudConfigSchema.parse({});
9807
+ }
9808
+ try {
9809
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
9810
+ return CloudConfigSchema.parse(JSON.parse(raw));
9811
+ } catch {
9812
+ return CloudConfigSchema.parse({});
9813
+ }
9814
+ }
9815
+ function saveCloudConfig(config) {
9816
+ mkdirSync2(CONFIG_DIR, { recursive: true });
9817
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
9818
+ `, "utf-8");
9819
+ }
9820
+ function getConnectionString(dbName) {
9821
+ const config = getCloudConfig();
9822
+ const { host, port, username, password_env, ssl } = config.rds;
9823
+ if (!host || !username) {
9824
+ throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
9825
+ }
9826
+ const password = process.env[password_env];
9827
+ if (password === undefined || password === "") {
9828
+ throw new Error(`RDS password not set. Export ${password_env} in your shell or add it to ~/.secrets/hasna/rds/live.env`);
9829
+ }
9830
+ const sslParam = ssl ? "?sslmode=require" : "";
9831
+ return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
9832
+ }
9833
+ function createDatabase(options) {
9834
+ const config = getCloudConfig();
9835
+ const mode = options.mode ?? config.mode;
9836
+ if (mode === "cloud") {
9837
+ const connStr = options.pgConnectionString ?? getConnectionString(options.service);
9838
+ return createMachineAwareAdapter(new PgAdapter(connStr));
9839
+ }
9840
+ const dbPath = options.sqlitePath ?? getDbPath(options.service);
9841
+ return createMachineAwareAdapter(new SqliteAdapter(dbPath));
9842
+ }
9843
+ var DaemonConfigSchema, CloudConfigSchema, CONFIG_DIR, CONFIG_PATH;
9844
+ var init_config = __esm(() => {
9845
+ init_zod();
9846
+ init_adapter();
9847
+ init_dotfile();
9848
+ init_machines();
9849
+ DaemonConfigSchema = exports_external.object({
9850
+ enabled: exports_external.boolean().default(false),
9851
+ paused: exports_external.boolean().default(false),
9852
+ watch_interval_seconds: exports_external.number().int().positive().default(5),
9853
+ pull_interval_seconds: exports_external.number().int().positive().default(60),
9854
+ push_debounce_seconds: exports_external.number().int().positive().default(5),
9855
+ conflict_strategy: exports_external.enum(["newest-wins", "local-wins", "remote-wins"]).default("newest-wins"),
9856
+ services: exports_external.array(exports_external.string()).default([]),
9857
+ table_intervals: exports_external.record(exports_external.string(), exports_external.record(exports_external.string(), exports_external.number().int().positive())).default({}),
9858
+ file_rules: exports_external.array(exports_external.object({
9859
+ path: exports_external.string(),
9860
+ interval_seconds: exports_external.number().int().positive().default(30),
9861
+ enabled: exports_external.boolean().default(true)
9862
+ })).default([])
9863
+ }).default({});
9864
+ CloudConfigSchema = exports_external.object({
9865
+ rds: exports_external.object({
9866
+ host: exports_external.string().default(""),
9867
+ port: exports_external.number().default(5432),
9868
+ username: exports_external.string().default(""),
9869
+ password_env: exports_external.string().default("HASNA_RDS_PASSWORD"),
9870
+ ssl: exports_external.boolean().default(true)
9871
+ }).default({}),
9872
+ mode: exports_external.enum(["local", "cloud", "hybrid"]).default("hybrid"),
9873
+ auto_sync_interval_minutes: exports_external.number().default(0),
9874
+ feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
9875
+ sync: exports_external.object({
9876
+ schedule_minutes: exports_external.number().default(0)
9877
+ }).default({}),
9878
+ daemon: DaemonConfigSchema
9879
+ });
9880
+ CONFIG_DIR = join4(homedir4(), ".hasna", "cloud");
9881
+ CONFIG_PATH = join4(CONFIG_DIR, "config.json");
9882
+ });
9883
+
9462
9884
  // src/index.ts
9463
9885
  init_adapter();
9464
9886
  init_config();
9887
+ init_machines();
9465
9888
 
9466
9889
  // src/sync.ts
9467
9890
  async function syncPush(local, remote, options) {
@@ -9639,6 +10062,9 @@ async function ensureTablesExist(source, target, tables) {
9639
10062
  }
9640
10063
  async function filterColumnsForTarget(target, table, sourceColumns) {
9641
10064
  try {
10065
+ if (sourceColumns.includes("machine_id") && table !== "machines") {
10066
+ await ensureMachineIdColumnInTarget(target, table);
10067
+ }
9642
10068
  if (!isAsyncAdapter(target)) {
9643
10069
  const colInfo = target.all(`PRAGMA table_info("${table}")`);
9644
10070
  if (Array.isArray(colInfo) && colInfo.length > 0) {
@@ -9661,6 +10087,22 @@ async function filterColumnsForTarget(target, table, sourceColumns) {
9661
10087
  } catch {}
9662
10088
  return sourceColumns;
9663
10089
  }
10090
+ async function ensureMachineIdColumnInTarget(target, table) {
10091
+ if (!isAsyncAdapter(target)) {
10092
+ const colInfo2 = target.all(`PRAGMA table_info("${table}")`);
10093
+ const hasMachineId = Array.isArray(colInfo2) ? colInfo2.some((column) => column.name === "machine_id") : false;
10094
+ if (!hasMachineId) {
10095
+ target.exec(`ALTER TABLE "${table}" ADD COLUMN machine_id TEXT DEFAULT ''`);
10096
+ }
10097
+ return;
10098
+ }
10099
+ const colInfo = await target.all(`SELECT column_name
10100
+ FROM information_schema.columns
10101
+ WHERE table_schema = 'public' AND table_name = '${table}' AND column_name = 'machine_id'`);
10102
+ if (colInfo.length === 0) {
10103
+ await target.exec(`ALTER TABLE "${table}" ADD COLUMN machine_id TEXT DEFAULT ''`);
10104
+ }
10105
+ }
9664
10106
  async function syncTransfer(source, target, options, _direction) {
9665
10107
  const {
9666
10108
  tables,
@@ -9895,7 +10337,7 @@ async function listPgTables(db) {
9895
10337
  }
9896
10338
  // src/feedback.ts
9897
10339
  init_config();
9898
- import { hostname } from "os";
10340
+ import { hostname as hostname2 } from "os";
9899
10341
  var FEEDBACK_TABLE_SQL = `
9900
10342
  CREATE TABLE IF NOT EXISTS feedback (
9901
10343
  id TEXT PRIMARY KEY,
@@ -9913,7 +10355,7 @@ function saveFeedback(db, feedback) {
9913
10355
  ensureFeedbackTable(db);
9914
10356
  const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
9915
10357
  const now = new Date().toISOString();
9916
- const machineId = feedback.machine_id ?? hostname();
10358
+ const machineId = feedback.machine_id ?? hostname2();
9917
10359
  db.run(`INSERT INTO feedback (id, service, version, message, email, machine_id, created_at)
9918
10360
  VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ?? now);
9919
10361
  return id;
@@ -9921,7 +10363,7 @@ function saveFeedback(db, feedback) {
9921
10363
  async function sendFeedback(feedback, db) {
9922
10364
  const config = getCloudConfig();
9923
10365
  const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
9924
- const machineId = feedback.machine_id ?? hostname();
10366
+ const machineId = feedback.machine_id ?? hostname2();
9925
10367
  const now = new Date().toISOString();
9926
10368
  const payload = {
9927
10369
  id,
@@ -10217,6 +10659,7 @@ function purgeResolvedConflicts(db) {
10217
10659
  return result.changes;
10218
10660
  }
10219
10661
  // src/sync-incremental.ts
10662
+ init_machines();
10220
10663
  var SYNC_META_TABLE_SQL = `
10221
10664
  CREATE TABLE IF NOT EXISTS _sync_meta (
10222
10665
  table_name TEXT PRIMARY KEY,
@@ -10248,6 +10691,11 @@ function transferRows(source, target, table, rows, options) {
10248
10691
  if (rows.length === 0)
10249
10692
  return { written, skipped, errors: errors2 };
10250
10693
  const columns = Object.keys(rows[0]);
10694
+ if (columns.includes("machine_id") && table !== "machines") {
10695
+ try {
10696
+ ensureMachineIdColumn(target, table);
10697
+ } catch {}
10698
+ }
10251
10699
  const hasConflictCol = columns.includes(conflictColumn);
10252
10700
  const hasPrimaryKey = columns.includes(primaryKey);
10253
10701
  if (!hasPrimaryKey) {
@@ -10258,12 +10706,21 @@ function transferRows(source, target, table, rows, options) {
10258
10706
  try {
10259
10707
  const existing = target.get(`SELECT "${primaryKey}"${hasConflictCol ? `, "${conflictColumn}"` : ""} FROM "${table}" WHERE "${primaryKey}" = ?`, row[primaryKey]);
10260
10708
  if (existing) {
10261
- if (hasConflictCol && existing[conflictColumn] && row[conflictColumn]) {
10262
- const existingTime = new Date(existing[conflictColumn]).getTime();
10263
- const incomingTime = new Date(row[conflictColumn]).getTime();
10264
- if (existingTime >= incomingTime) {
10265
- skipped++;
10266
- continue;
10709
+ const conflictStrategy = options.conflictStrategy ?? "newest-wins";
10710
+ const sourceRole = options.sourceRole ?? "local";
10711
+ const sourceWins = conflictStrategy === "local-wins" && sourceRole === "local" || conflictStrategy === "remote-wins" && sourceRole === "remote";
10712
+ if (!sourceWins && conflictStrategy !== "newest-wins") {
10713
+ skipped++;
10714
+ continue;
10715
+ }
10716
+ if (conflictStrategy === "newest-wins") {
10717
+ if (hasConflictCol && existing[conflictColumn] && row[conflictColumn]) {
10718
+ const existingTime = new Date(existing[conflictColumn]).getTime();
10719
+ const incomingTime = new Date(row[conflictColumn]).getTime();
10720
+ if (existingTime >= incomingTime) {
10721
+ skipped++;
10722
+ continue;
10723
+ }
10267
10724
  }
10268
10725
  }
10269
10726
  const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
@@ -10314,7 +10771,10 @@ function incrementalSyncPush(local, remote, tables, options = {}) {
10314
10771
  }
10315
10772
  for (let offset = 0;offset < rows.length; offset += batchSize) {
10316
10773
  const batch = rows.slice(offset, offset + batchSize);
10317
- const result = transferRows(local, remote, table, batch, options);
10774
+ const result = transferRows(local, remote, table, batch, {
10775
+ ...options,
10776
+ sourceRole: "local"
10777
+ });
10318
10778
  stat.synced_rows += result.written;
10319
10779
  stat.skipped_rows += result.skipped;
10320
10780
  stat.errors.push(...result.errors);
@@ -10367,7 +10827,10 @@ function incrementalSyncPull(remote, local, tables, options = {}) {
10367
10827
  }
10368
10828
  for (let offset = 0;offset < rows.length; offset += batchSize) {
10369
10829
  const batch = rows.slice(offset, offset + batchSize);
10370
- const result = transferRows(remote, local, table, batch, options);
10830
+ const result = transferRows(remote, local, table, batch, {
10831
+ ...options,
10832
+ sourceRole: "remote"
10833
+ });
10371
10834
  stat.synced_rows += result.written;
10372
10835
  stat.skipped_rows += result.skipped;
10373
10836
  stat.errors.push(...result.errors);
@@ -10407,18 +10870,18 @@ function resetAllSyncMeta(db) {
10407
10870
  // src/auto-sync.ts
10408
10871
  init_adapter();
10409
10872
  init_config();
10410
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
10411
- import { homedir as homedir4 } from "os";
10412
- import { join as join4 } from "path";
10873
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
10874
+ import { homedir as homedir5 } from "os";
10875
+ import { join as join5 } from "path";
10413
10876
  init_discover();
10414
- var AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
10877
+ var AUTO_SYNC_CONFIG_PATH = join5(homedir5(), ".hasna", "cloud", "config.json");
10415
10878
  var DEFAULT_AUTO_SYNC_CONFIG = {
10416
10879
  auto_sync_on_start: true,
10417
10880
  auto_sync_on_stop: true
10418
10881
  };
10419
10882
  function getAutoSyncConfig() {
10420
10883
  try {
10421
- if (!existsSync4(AUTO_SYNC_CONFIG_PATH)) {
10884
+ if (!existsSync5(AUTO_SYNC_CONFIG_PATH)) {
10422
10885
  return { ...DEFAULT_AUTO_SYNC_CONFIG };
10423
10886
  }
10424
10887
  const raw = JSON.parse(readFileSync2(AUTO_SYNC_CONFIG_PATH, "utf-8"));
@@ -10510,20 +10973,24 @@ function setupAutoSync(serviceName, server, local, remote, tables) {
10510
10973
  if (server && typeof server.onconnect === "function") {
10511
10974
  const origOnConnect = server.onconnect;
10512
10975
  server.onconnect = async (...args) => {
10513
- await syncOnStart();
10976
+ syncOnStart();
10514
10977
  return origOnConnect.apply(server, args);
10515
10978
  };
10516
10979
  } else if (server && typeof server.on === "function") {
10517
- server.on("connect", () => syncOnStart());
10980
+ server.on("connect", () => {
10981
+ syncOnStart();
10982
+ });
10518
10983
  }
10519
10984
  if (server && typeof server.ondisconnect === "function") {
10520
10985
  const origOnDisconnect = server.ondisconnect;
10521
10986
  server.ondisconnect = async (...args) => {
10522
- await syncOnStop();
10987
+ syncOnStop();
10523
10988
  return origOnDisconnect.apply(server, args);
10524
10989
  };
10525
10990
  } else if (server && typeof server.on === "function") {
10526
- server.on("disconnect", () => syncOnStop());
10991
+ server.on("disconnect", () => {
10992
+ syncOnStop();
10993
+ });
10527
10994
  }
10528
10995
  installSignalHandlers();
10529
10996
  cleanupHandlers.push(async () => {
@@ -10537,8 +11004,8 @@ function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
10537
11004
  // src/scheduled-sync.ts
10538
11005
  init_config();
10539
11006
  init_adapter();
10540
- import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
10541
- import { join as join5 } from "path";
11007
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
11008
+ import { join as join6 } from "path";
10542
11009
  init_dotfile();
10543
11010
  function discoverSyncableServices2() {
10544
11011
  const hasnaDir = getHasnaDir();
@@ -10548,8 +11015,8 @@ function discoverSyncableServices2() {
10548
11015
  for (const entry of entries) {
10549
11016
  if (!entry.isDirectory())
10550
11017
  continue;
10551
- const dbPath = join5(hasnaDir, entry.name, `${entry.name}.db`);
10552
- if (existsSync5(dbPath)) {
11018
+ const dbPath = join6(hasnaDir, entry.name, `${entry.name}.db`);
11019
+ if (existsSync6(dbPath)) {
10553
11020
  services.push(entry.name);
10554
11021
  }
10555
11022
  }
@@ -10571,8 +11038,8 @@ async function runScheduledSync() {
10571
11038
  errors: []
10572
11039
  };
10573
11040
  try {
10574
- const dbPath = join5(getDataDir(service), `${service}.db`);
10575
- if (!existsSync5(dbPath)) {
11041
+ const dbPath = join6(getDataDir(service), `${service}.db`);
11042
+ if (!existsSync6(dbPath)) {
10576
11043
  continue;
10577
11044
  }
10578
11045
  const local = new SqliteAdapter(dbPath);
@@ -10583,7 +11050,7 @@ async function runScheduledSync() {
10583
11050
  }
10584
11051
  try {
10585
11052
  const connStr = getConnectionString(service);
10586
- remote = new PgAdapterAsync(connStr);
11053
+ remote = new PgAdapter(connStr);
10587
11054
  } catch (err) {
10588
11055
  result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
10589
11056
  local.close();
@@ -10599,7 +11066,7 @@ async function runScheduledSync() {
10599
11066
  result.errors.push(...s.errors);
10600
11067
  }
10601
11068
  local.close();
10602
- await remote.close();
11069
+ remote.close();
10603
11070
  remote = null;
10604
11071
  } catch (err) {
10605
11072
  result.errors.push(err?.message ?? String(err));
@@ -10608,18 +11075,18 @@ async function runScheduledSync() {
10608
11075
  }
10609
11076
  if (remote) {
10610
11077
  try {
10611
- await remote.close();
11078
+ remote.close();
10612
11079
  } catch {}
10613
11080
  }
10614
11081
  return results;
10615
11082
  }
10616
11083
  // src/sync-schedule.ts
10617
11084
  init_config();
10618
- import { join as join6, dirname } from "path";
10619
- import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
10620
- import { homedir as homedir5, platform } from "os";
11085
+ import { join as join7, dirname as dirname2 } from "path";
11086
+ import { existsSync as existsSync7, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
11087
+ import { homedir as homedir6, platform as platform2 } from "os";
10621
11088
  var SERVICE_NAME = "hasna-cloud-sync";
10622
- var CONFIG_DIR2 = join6(homedir5(), ".hasna", "cloud");
11089
+ var CONFIG_DIR2 = join7(homedir6(), ".hasna", "cloud");
10623
11090
  function parseInterval(input) {
10624
11091
  const trimmed = input.trim().toLowerCase();
10625
11092
  const hourMatch = trimmed.match(/^(\d+)\s*h$/);
@@ -10659,35 +11126,35 @@ function minutesToCron(minutes) {
10659
11126
  return `*/${minutes} * * * *`;
10660
11127
  }
10661
11128
  function getWorkerPath() {
10662
- const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
10663
- const tsPath = join6(dir, "scheduled-sync.ts");
10664
- const jsPath = join6(dir, "scheduled-sync.js");
11129
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname2(import.meta.url.replace("file://", ""));
11130
+ const tsPath = join7(dir, "scheduled-sync.ts");
11131
+ const jsPath = join7(dir, "scheduled-sync.js");
10665
11132
  try {
10666
- if (existsSync6(tsPath))
11133
+ if (existsSync7(tsPath))
10667
11134
  return tsPath;
10668
11135
  } catch {}
10669
11136
  return jsPath;
10670
11137
  }
10671
11138
  function getBunPath() {
10672
11139
  const candidates = [
10673
- join6(homedir5(), ".bun", "bin", "bun"),
11140
+ join7(homedir6(), ".bun", "bin", "bun"),
10674
11141
  "/usr/local/bin/bun",
10675
11142
  "/usr/bin/bun"
10676
11143
  ];
10677
11144
  for (const p of candidates) {
10678
- if (existsSync6(p))
11145
+ if (existsSync7(p))
10679
11146
  return p;
10680
11147
  }
10681
11148
  return "bun";
10682
11149
  }
10683
11150
  function getLaunchdPlistPath() {
10684
- return join6(homedir5(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
11151
+ return join7(homedir6(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
10685
11152
  }
10686
11153
  function createLaunchdPlist(intervalMinutes) {
10687
11154
  const workerPath = getWorkerPath();
10688
11155
  const bunPath = getBunPath();
10689
- const logPath = join6(CONFIG_DIR2, "sync.log");
10690
- const errorLogPath = join6(CONFIG_DIR2, "sync-error.log");
11156
+ const logPath = join7(CONFIG_DIR2, "sync.log");
11157
+ const errorLogPath = join7(CONFIG_DIR2, "sync-error.log");
10691
11158
  return `<?xml version="1.0" encoding="UTF-8"?>
10692
11159
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
10693
11160
  <plist version="1.0">
@@ -10713,14 +11180,14 @@ function createLaunchdPlist(intervalMinutes) {
10713
11180
  <key>PATH</key>
10714
11181
  <string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
10715
11182
  <key>HOME</key>
10716
- <string>${homedir5()}</string>
11183
+ <string>${homedir6()}</string>
10717
11184
  </dict>
10718
11185
  </dict>
10719
11186
  </plist>`;
10720
11187
  }
10721
11188
  async function registerLaunchd(intervalMinutes) {
10722
11189
  const plistPath = getLaunchdPlistPath();
10723
- const plistDir = dirname(plistPath);
11190
+ const plistDir = dirname2(plistPath);
10724
11191
  mkdirSync3(plistDir, { recursive: true });
10725
11192
  try {
10726
11193
  await Bun.spawn(["launchctl", "unload", plistPath]).exited;
@@ -10738,7 +11205,7 @@ async function removeLaunchd() {
10738
11205
  } catch {}
10739
11206
  }
10740
11207
  function getSystemdDir() {
10741
- return join6(homedir5(), ".config", "systemd", "user");
11208
+ return join7(homedir6(), ".config", "systemd", "user");
10742
11209
  }
10743
11210
  function createSystemdService() {
10744
11211
  const workerPath = getWorkerPath();
@@ -10750,7 +11217,7 @@ After=network.target
10750
11217
  [Service]
10751
11218
  Type=oneshot
10752
11219
  ExecStart=${bunPath} run ${workerPath}
10753
- Environment=HOME=${homedir5()}
11220
+ Environment=HOME=${homedir6()}
10754
11221
  Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
10755
11222
 
10756
11223
  [Install]
@@ -10773,8 +11240,8 @@ WantedBy=timers.target
10773
11240
  async function registerSystemd(intervalMinutes) {
10774
11241
  const dir = getSystemdDir();
10775
11242
  mkdirSync3(dir, { recursive: true });
10776
- writeFileSync2(join6(dir, `${SERVICE_NAME}.service`), createSystemdService());
10777
- writeFileSync2(join6(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
11243
+ writeFileSync2(join7(dir, `${SERVICE_NAME}.service`), createSystemdService());
11244
+ writeFileSync2(join7(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
10778
11245
  await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
10779
11246
  await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
10780
11247
  }
@@ -10784,10 +11251,10 @@ async function removeSystemd() {
10784
11251
  } catch {}
10785
11252
  const dir = getSystemdDir();
10786
11253
  try {
10787
- unlinkSync(join6(dir, `${SERVICE_NAME}.service`));
11254
+ unlinkSync(join7(dir, `${SERVICE_NAME}.service`));
10788
11255
  } catch {}
10789
11256
  try {
10790
- unlinkSync(join6(dir, `${SERVICE_NAME}.timer`));
11257
+ unlinkSync(join7(dir, `${SERVICE_NAME}.timer`));
10791
11258
  } catch {}
10792
11259
  try {
10793
11260
  await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
@@ -10798,7 +11265,7 @@ async function registerSyncSchedule(intervalMinutes) {
10798
11265
  throw new Error("Interval must be a positive number of minutes.");
10799
11266
  }
10800
11267
  mkdirSync3(CONFIG_DIR2, { recursive: true });
10801
- if (platform() === "darwin") {
11268
+ if (platform2() === "darwin") {
10802
11269
  await registerLaunchd(intervalMinutes);
10803
11270
  } else {
10804
11271
  await registerSystemd(intervalMinutes);
@@ -10808,7 +11275,7 @@ async function registerSyncSchedule(intervalMinutes) {
10808
11275
  saveCloudConfig(config);
10809
11276
  }
10810
11277
  async function removeSyncSchedule() {
10811
- if (platform() === "darwin") {
11278
+ if (platform2() === "darwin") {
10812
11279
  await removeLaunchd();
10813
11280
  } else {
10814
11281
  await removeSystemd();
@@ -10823,10 +11290,10 @@ function getSyncScheduleStatus() {
10823
11290
  const registered = minutes > 0;
10824
11291
  let mechanism = "none";
10825
11292
  if (registered) {
10826
- if (platform() === "darwin") {
10827
- mechanism = existsSync6(getLaunchdPlistPath()) ? "launchd" : "none";
11293
+ if (platform2() === "darwin") {
11294
+ mechanism = existsSync7(getLaunchdPlistPath()) ? "launchd" : "none";
10828
11295
  } else {
10829
- mechanism = existsSync6(join6(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
11296
+ mechanism = existsSync7(join7(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
10830
11297
  }
10831
11298
  }
10832
11299
  return {
@@ -10836,6 +11303,497 @@ function getSyncScheduleStatus() {
10836
11303
  mechanism
10837
11304
  };
10838
11305
  }
11306
+ // src/daemon-sync.ts
11307
+ init_adapter();
11308
+ init_config();
11309
+ init_discover();
11310
+ init_dotfile();
11311
+ import { spawn } from "child_process";
11312
+ import {
11313
+ existsSync as existsSync8,
11314
+ mkdirSync as mkdirSync4,
11315
+ readFileSync as readFileSync4,
11316
+ statSync as statSync4,
11317
+ writeFileSync as writeFileSync3
11318
+ } from "fs";
11319
+ import { homedir as homedir7 } from "os";
11320
+ import { join as join8 } from "path";
11321
+ var DAEMON_STATE_PATH = join8(homedir7(), ".hasna", "cloud", "daemon-state.json");
11322
+ var defaultDaemonAdapterFactory = {
11323
+ getLocalDbPath: (service) => getDbPath(service),
11324
+ openLocal: (service) => new SqliteAdapter(getDbPath(service)),
11325
+ openRemote: (service) => new PgAdapter(getConnectionString(service)),
11326
+ listRemoteTables: (remote) => remote.all(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename`).map((row) => row.tablename)
11327
+ };
11328
+ function nowIso() {
11329
+ return new Date().toISOString();
11330
+ }
11331
+ function sleepSync(ms) {
11332
+ if (ms <= 0)
11333
+ return;
11334
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
11335
+ Atomics.wait(sleeper, 0, 0, ms);
11336
+ }
11337
+ function cloneState(state) {
11338
+ return JSON.parse(JSON.stringify(state));
11339
+ }
11340
+ function getDefaultDaemonConfig() {
11341
+ return {
11342
+ enabled: false,
11343
+ paused: false,
11344
+ watch_interval_seconds: 5,
11345
+ pull_interval_seconds: 60,
11346
+ push_debounce_seconds: 5,
11347
+ conflict_strategy: "newest-wins",
11348
+ services: [],
11349
+ table_intervals: {},
11350
+ file_rules: []
11351
+ };
11352
+ }
11353
+ function getDaemonConfig(config = getCloudConfig()) {
11354
+ const defaults2 = getDefaultDaemonConfig();
11355
+ return {
11356
+ ...defaults2,
11357
+ ...config.daemon,
11358
+ services: [...config.daemon?.services ?? defaults2.services],
11359
+ table_intervals: { ...config.daemon?.table_intervals ?? defaults2.table_intervals },
11360
+ file_rules: [...config.daemon?.file_rules ?? defaults2.file_rules]
11361
+ };
11362
+ }
11363
+ function saveDaemonConfig(config) {
11364
+ const cloudConfig = getCloudConfig();
11365
+ cloudConfig.daemon = {
11366
+ ...getDefaultDaemonConfig(),
11367
+ ...config,
11368
+ services: [...config.services],
11369
+ table_intervals: { ...config.table_intervals },
11370
+ file_rules: [...config.file_rules]
11371
+ };
11372
+ saveCloudConfig(cloudConfig);
11373
+ return getDaemonConfig(cloudConfig);
11374
+ }
11375
+ function getDaemonStatePath() {
11376
+ return DAEMON_STATE_PATH;
11377
+ }
11378
+ function createDefaultDaemonState() {
11379
+ return {
11380
+ pid: null,
11381
+ status: "stopped",
11382
+ started_at: null,
11383
+ updated_at: null,
11384
+ last_push_at: null,
11385
+ last_pull_at: null,
11386
+ last_error: null,
11387
+ services: {},
11388
+ files: {}
11389
+ };
11390
+ }
11391
+ function readDaemonState() {
11392
+ if (!existsSync8(DAEMON_STATE_PATH)) {
11393
+ return createDefaultDaemonState();
11394
+ }
11395
+ try {
11396
+ const raw = JSON.parse(readFileSync4(DAEMON_STATE_PATH, "utf-8"));
11397
+ const defaults2 = createDefaultDaemonState();
11398
+ return {
11399
+ ...defaults2,
11400
+ ...raw,
11401
+ services: raw?.services ?? defaults2.services,
11402
+ files: raw?.files ?? defaults2.files
11403
+ };
11404
+ } catch {
11405
+ return createDefaultDaemonState();
11406
+ }
11407
+ }
11408
+ function writeDaemonState(state) {
11409
+ mkdirSync4(join8(homedir7(), ".hasna", "cloud"), { recursive: true });
11410
+ writeFileSync3(DAEMON_STATE_PATH, JSON.stringify(state, null, 2) + `
11411
+ `, "utf-8");
11412
+ }
11413
+ function getServiceState(state, service) {
11414
+ return state.services[service] ?? {
11415
+ last_local_db_mtime_ms: 0,
11416
+ last_push_at: null,
11417
+ last_pull_at: null,
11418
+ last_error: null,
11419
+ tables: {}
11420
+ };
11421
+ }
11422
+ function isProcessRunning(pid) {
11423
+ if (!pid || pid <= 0)
11424
+ return false;
11425
+ try {
11426
+ process.kill(pid, 0);
11427
+ return true;
11428
+ } catch {
11429
+ return false;
11430
+ }
11431
+ }
11432
+ function resolveDaemonServices(config) {
11433
+ if (config.services.length > 0) {
11434
+ return [...new Set(config.services)].sort();
11435
+ }
11436
+ return discoverSyncableServices();
11437
+ }
11438
+ function parseTableIntervalRule(raw) {
11439
+ const match = raw.match(/^([^.:=]+)[.:]([^=]+)=(\d+)$/);
11440
+ if (!match) {
11441
+ throw new Error(`Invalid table interval "${raw}". Use service.table=seconds or service:table=seconds.`);
11442
+ }
11443
+ const intervalSeconds = parseInt(match[3], 10);
11444
+ if (intervalSeconds <= 0) {
11445
+ throw new Error(`Invalid table interval "${raw}". Seconds must be > 0.`);
11446
+ }
11447
+ return {
11448
+ service: match[1],
11449
+ table: match[2],
11450
+ interval_seconds: intervalSeconds
11451
+ };
11452
+ }
11453
+ function applyTableIntervalRules(existing, rules) {
11454
+ const next = { ...existing };
11455
+ for (const raw of rules) {
11456
+ const parsed = parseTableIntervalRule(raw);
11457
+ next[parsed.service] = {
11458
+ ...next[parsed.service] ?? {},
11459
+ [parsed.table]: parsed.interval_seconds
11460
+ };
11461
+ }
11462
+ return next;
11463
+ }
11464
+ function parseFileRule(raw) {
11465
+ const [pathPart, intervalPart] = raw.split("=", 2);
11466
+ const path = pathPart?.trim();
11467
+ if (!path) {
11468
+ throw new Error(`Invalid file rule "${raw}". Use /path/to/file=seconds.`);
11469
+ }
11470
+ const intervalSeconds = intervalPart ? parseInt(intervalPart, 10) : 30;
11471
+ if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
11472
+ throw new Error(`Invalid file rule "${raw}". Seconds must be > 0.`);
11473
+ }
11474
+ return {
11475
+ path,
11476
+ interval_seconds: intervalSeconds,
11477
+ enabled: true
11478
+ };
11479
+ }
11480
+ function applyFileRules(existing, rules) {
11481
+ const next = [...existing];
11482
+ for (const raw of rules) {
11483
+ next.push(parseFileRule(raw));
11484
+ }
11485
+ return next;
11486
+ }
11487
+ function isIntervalDue(lastAt, intervalSeconds, nowMs) {
11488
+ if (!intervalSeconds)
11489
+ return true;
11490
+ if (!lastAt)
11491
+ return true;
11492
+ return nowMs - Date.parse(lastAt) >= intervalSeconds * 1000;
11493
+ }
11494
+ function scanConfiguredFiles(config, state) {
11495
+ for (const rule of config.file_rules) {
11496
+ const key = rule.path;
11497
+ let lastMtime = 0;
11498
+ let exists = false;
11499
+ try {
11500
+ const stats = statSync4(rule.path);
11501
+ lastMtime = stats.mtimeMs;
11502
+ exists = true;
11503
+ } catch {
11504
+ exists = false;
11505
+ }
11506
+ state.files[key] = {
11507
+ path: key,
11508
+ enabled: rule.enabled,
11509
+ interval_seconds: rule.interval_seconds,
11510
+ exists,
11511
+ last_mtime_ms: lastMtime
11512
+ };
11513
+ }
11514
+ }
11515
+ function filterDueTables(service, tables, direction, serviceState, config, nowMs) {
11516
+ const serviceIntervals = config.table_intervals[service] ?? {};
11517
+ return tables.filter((table) => {
11518
+ const tableState = serviceState.tables[table];
11519
+ const lastAt = direction === "push" ? tableState?.last_push_at ?? null : tableState?.last_pull_at ?? null;
11520
+ return isIntervalDue(lastAt, serviceIntervals[table], nowMs);
11521
+ });
11522
+ }
11523
+ function recordTableRuns(serviceState, tables, direction, at) {
11524
+ for (const table of tables) {
11525
+ const current = serviceState.tables[table] ?? {
11526
+ last_push_at: null,
11527
+ last_pull_at: null
11528
+ };
11529
+ if (direction === "push") {
11530
+ current.last_push_at = at;
11531
+ } else {
11532
+ current.last_pull_at = at;
11533
+ }
11534
+ serviceState.tables[table] = current;
11535
+ }
11536
+ }
11537
+ function summarizeStats(stats) {
11538
+ return {
11539
+ rows: stats.reduce((sum, stat) => sum + stat.synced_rows, 0),
11540
+ errors: stats.flatMap((stat) => stat.errors.map((error) => `${stat.table}: ${error}`))
11541
+ };
11542
+ }
11543
+ function runDaemonPass(config, state = createDefaultDaemonState(), options = {}) {
11544
+ const nextState = cloneState(state);
11545
+ const adapterFactory = options.adapterFactory ?? defaultDaemonAdapterFactory;
11546
+ const services = options.services ?? resolveDaemonServices(config);
11547
+ const now = nowIso();
11548
+ const nowMs = Date.parse(now);
11549
+ const summary = {
11550
+ services,
11551
+ pushed_services: 0,
11552
+ pulled_services: 0,
11553
+ pushed_rows: 0,
11554
+ pulled_rows: 0,
11555
+ errors: []
11556
+ };
11557
+ nextState.updated_at = now;
11558
+ nextState.status = config.paused ? "paused" : "running";
11559
+ if (config.paused) {
11560
+ scanConfiguredFiles(config, nextState);
11561
+ return { state: nextState, summary };
11562
+ }
11563
+ for (const service of services) {
11564
+ const dbPath = adapterFactory.getLocalDbPath(service);
11565
+ if (!existsSync8(dbPath))
11566
+ continue;
11567
+ const serviceState = getServiceState(nextState, service);
11568
+ let local = null;
11569
+ let remote = null;
11570
+ try {
11571
+ const localStats = statSync4(dbPath);
11572
+ local = adapterFactory.openLocal(service);
11573
+ remote = adapterFactory.openRemote(service);
11574
+ const localTables = listSqliteTables(local).filter((table) => !isSyncExcludedTable(table));
11575
+ const duePushTables = filterDueTables(service, localTables, "push", serviceState, config, nowMs);
11576
+ const pushDebounceDue = !serviceState.last_push_at || nowMs - Date.parse(serviceState.last_push_at) >= config.push_debounce_seconds * 1000;
11577
+ const localChanged = localStats.mtimeMs > serviceState.last_local_db_mtime_ms;
11578
+ const shouldPush = duePushTables.length > 0 && (options.forcePush || localChanged && pushDebounceDue);
11579
+ if (shouldPush) {
11580
+ const pushStats = incrementalSyncPush(local, remote, duePushTables, {
11581
+ conflictStrategy: config.conflict_strategy
11582
+ });
11583
+ const pushSummary = summarizeStats(pushStats);
11584
+ serviceState.last_local_db_mtime_ms = localStats.mtimeMs;
11585
+ serviceState.last_push_at = now;
11586
+ serviceState.last_error = pushSummary.errors[0] ?? null;
11587
+ recordTableRuns(serviceState, duePushTables, "push", now);
11588
+ nextState.last_push_at = now;
11589
+ summary.pushed_services++;
11590
+ summary.pushed_rows += pushSummary.rows;
11591
+ summary.errors.push(...pushSummary.errors.map((error) => `[${service}] ${error}`));
11592
+ } else if (serviceState.last_local_db_mtime_ms === 0) {
11593
+ serviceState.last_local_db_mtime_ms = localStats.mtimeMs;
11594
+ }
11595
+ const remoteTables = adapterFactory.listRemoteTables(remote).filter((table) => !isSyncExcludedTable(table));
11596
+ const duePullTables = filterDueTables(service, remoteTables, "pull", serviceState, config, nowMs);
11597
+ const shouldPull = duePullTables.length > 0 && (options.forcePull || isIntervalDue(serviceState.last_pull_at, config.pull_interval_seconds, nowMs));
11598
+ if (shouldPull) {
11599
+ const pullStats = incrementalSyncPull(remote, local, duePullTables, {
11600
+ conflictStrategy: config.conflict_strategy
11601
+ });
11602
+ const pullSummary = summarizeStats(pullStats);
11603
+ serviceState.last_pull_at = now;
11604
+ serviceState.last_error = pullSummary.errors[0] ?? null;
11605
+ recordTableRuns(serviceState, duePullTables, "pull", now);
11606
+ nextState.last_pull_at = now;
11607
+ summary.pulled_services++;
11608
+ summary.pulled_rows += pullSummary.rows;
11609
+ summary.errors.push(...pullSummary.errors.map((error) => `[${service}] ${error}`));
11610
+ }
11611
+ } catch (err) {
11612
+ const message = err?.message ?? String(err);
11613
+ serviceState.last_error = message;
11614
+ nextState.last_error = message;
11615
+ summary.errors.push(`[${service}] ${message}`);
11616
+ } finally {
11617
+ nextState.services[service] = serviceState;
11618
+ try {
11619
+ local?.close();
11620
+ } catch {}
11621
+ try {
11622
+ remote?.close();
11623
+ } catch {}
11624
+ }
11625
+ }
11626
+ if (summary.errors.length > 0) {
11627
+ nextState.last_error = summary.errors[0];
11628
+ }
11629
+ scanConfiguredFiles(config, nextState);
11630
+ return { state: nextState, summary };
11631
+ }
11632
+ function getDaemonStatus() {
11633
+ const config = getDaemonConfig();
11634
+ const state = readDaemonState();
11635
+ const running = isProcessRunning(state.pid);
11636
+ const status = running ? config.paused ? "paused" : state.status : "stopped";
11637
+ return {
11638
+ running,
11639
+ pid: running ? state.pid : null,
11640
+ status,
11641
+ config,
11642
+ state: {
11643
+ ...state,
11644
+ status,
11645
+ pid: running ? state.pid : null
11646
+ },
11647
+ services: resolveDaemonServices(config)
11648
+ };
11649
+ }
11650
+ function getDaemonArgs() {
11651
+ const script = process.argv[1];
11652
+ if (!script) {
11653
+ throw new Error("Unable to determine the current CLI entrypoint.");
11654
+ }
11655
+ return [script, "sync", "daemon", "--run"];
11656
+ }
11657
+ function startDaemon(overrides = {}) {
11658
+ const current = getDaemonConfig();
11659
+ const nextConfig = saveDaemonConfig({
11660
+ ...current,
11661
+ ...overrides,
11662
+ enabled: true,
11663
+ paused: false,
11664
+ services: overrides.services ?? current.services,
11665
+ table_intervals: overrides.table_intervals ?? current.table_intervals,
11666
+ file_rules: overrides.file_rules ?? current.file_rules
11667
+ });
11668
+ const currentStatus = getDaemonStatus();
11669
+ if (currentStatus.running) {
11670
+ return currentStatus;
11671
+ }
11672
+ const child = spawn(process.execPath, getDaemonArgs(), {
11673
+ detached: true,
11674
+ stdio: "ignore",
11675
+ env: {
11676
+ ...process.env,
11677
+ HASNA_CLOUD_DAEMON_CHILD: "1"
11678
+ }
11679
+ });
11680
+ child.unref();
11681
+ const state = readDaemonState();
11682
+ const startedAt = nowIso();
11683
+ state.pid = child.pid ?? null;
11684
+ state.status = "running";
11685
+ state.started_at = startedAt;
11686
+ state.updated_at = startedAt;
11687
+ writeDaemonState(state);
11688
+ return {
11689
+ running: true,
11690
+ pid: child.pid ?? null,
11691
+ status: "running",
11692
+ config: nextConfig,
11693
+ state,
11694
+ services: resolveDaemonServices(nextConfig)
11695
+ };
11696
+ }
11697
+ function stopDaemon() {
11698
+ const current = getDaemonConfig();
11699
+ saveDaemonConfig({ ...current, enabled: false });
11700
+ const status = getDaemonStatus();
11701
+ if (status.pid) {
11702
+ try {
11703
+ process.kill(status.pid, "SIGTERM");
11704
+ } catch {}
11705
+ }
11706
+ const state = readDaemonState();
11707
+ state.pid = null;
11708
+ state.status = "stopped";
11709
+ state.updated_at = nowIso();
11710
+ writeDaemonState(state);
11711
+ return getDaemonStatus();
11712
+ }
11713
+ function pauseDaemon() {
11714
+ const current = getDaemonConfig();
11715
+ saveDaemonConfig({ ...current, paused: true });
11716
+ const state = readDaemonState();
11717
+ if (state.pid) {
11718
+ state.status = "paused";
11719
+ state.updated_at = nowIso();
11720
+ writeDaemonState(state);
11721
+ }
11722
+ return getDaemonStatus();
11723
+ }
11724
+ function resumeDaemon() {
11725
+ const current = getDaemonConfig();
11726
+ saveDaemonConfig({ ...current, enabled: true, paused: false });
11727
+ const state = readDaemonState();
11728
+ if (state.pid) {
11729
+ state.status = "running";
11730
+ state.updated_at = nowIso();
11731
+ writeDaemonState(state);
11732
+ }
11733
+ return getDaemonStatus();
11734
+ }
11735
+ function runDaemonOnce(options = {}) {
11736
+ const config = options.config ?? getDaemonConfig();
11737
+ const state = options.state ?? readDaemonState();
11738
+ const result = runDaemonPass(config, state, {
11739
+ adapterFactory: options.adapterFactory,
11740
+ forcePull: options.forcePull,
11741
+ forcePush: options.forcePush,
11742
+ services: options.services
11743
+ });
11744
+ writeDaemonState(result.state);
11745
+ return result;
11746
+ }
11747
+ function runDaemonLoop(options = {}) {
11748
+ let stopping = false;
11749
+ const stop = () => {
11750
+ stopping = true;
11751
+ };
11752
+ process.on("SIGTERM", stop);
11753
+ process.on("SIGINT", stop);
11754
+ let passes = 0;
11755
+ const startedAt = nowIso();
11756
+ try {
11757
+ while (!stopping) {
11758
+ const config = getDaemonConfig();
11759
+ const state = readDaemonState();
11760
+ if (!config.enabled) {
11761
+ break;
11762
+ }
11763
+ state.pid = process.pid;
11764
+ state.started_at = state.started_at ?? startedAt;
11765
+ state.updated_at = nowIso();
11766
+ writeDaemonState(state);
11767
+ const result = runDaemonPass(config, state, {
11768
+ adapterFactory: options.adapterFactory
11769
+ });
11770
+ result.state.pid = process.pid;
11771
+ result.state.started_at = state.started_at ?? startedAt;
11772
+ result.state.status = config.paused ? "paused" : "running";
11773
+ result.state.updated_at = nowIso();
11774
+ writeDaemonState(result.state);
11775
+ passes++;
11776
+ if (options.max_passes && passes >= options.max_passes) {
11777
+ break;
11778
+ }
11779
+ sleepSync(config.watch_interval_seconds * 1000);
11780
+ }
11781
+ } finally {
11782
+ const finalState = readDaemonState();
11783
+ finalState.pid = null;
11784
+ finalState.status = "stopped";
11785
+ finalState.updated_at = nowIso();
11786
+ writeDaemonState(finalState);
11787
+ }
11788
+ }
11789
+ function normalizeConflictStrategy(strategy) {
11790
+ if (!strategy)
11791
+ return "newest-wins";
11792
+ if (strategy !== "newest-wins" && strategy !== "local-wins" && strategy !== "remote-wins") {
11793
+ throw new Error(`Invalid conflict strategy "${strategy}". Use newest-wins, local-wins, or remote-wins.`);
11794
+ }
11795
+ return strategy;
11796
+ }
10839
11797
  // src/pg-migrate.ts
10840
11798
  init_adapter();
10841
11799
  init_config();
@@ -11164,33 +12122,50 @@ function registerCloudCommands(program, serviceName) {
11164
12122
  });
11165
12123
  }
11166
12124
  export {
12125
+ writeDaemonState,
11167
12126
  translateSql,
11168
12127
  translateParams,
11169
12128
  translateDdl,
11170
12129
  syncPush,
11171
12130
  syncPull,
11172
12131
  storeConflicts,
12132
+ stopDaemon,
12133
+ startDaemon,
11173
12134
  setupAutoSync,
11174
12135
  sendFeedback,
11175
12136
  saveFeedback,
12137
+ saveDaemonConfig,
11176
12138
  saveCloudConfig,
11177
12139
  runScheduledSync,
12140
+ runDaemonPass,
12141
+ runDaemonOnce,
12142
+ runDaemonLoop,
12143
+ resumeDaemon,
12144
+ resolveDaemonServices,
11178
12145
  resolveConflicts,
11179
12146
  resolveConflict,
11180
12147
  resetSyncMeta,
11181
12148
  resetAllSyncMeta,
11182
12149
  removeSyncSchedule,
11183
12150
  registerSyncSchedule,
12151
+ registerMachine,
11184
12152
  registerCloudTools,
11185
12153
  registerCloudCommands,
12154
+ readDaemonState,
11186
12155
  purgeResolvedConflicts,
12156
+ pingMachine,
12157
+ pauseDaemon,
12158
+ parseTableIntervalRule,
11187
12159
  parseInterval,
12160
+ parseFileRule,
12161
+ normalizeConflictStrategy,
11188
12162
  minutesToCron,
11189
12163
  migrateService,
11190
12164
  migrateDotfile,
11191
12165
  migrateAllServices,
11192
12166
  listSqliteTables,
11193
12167
  listPgTables,
12168
+ listMachines,
11194
12169
  listFeedback,
11195
12170
  listConflicts,
11196
12171
  isSyncExcludedTable,
@@ -11202,9 +12177,16 @@ export {
11202
12177
  getSyncMetaForTable,
11203
12178
  getSyncMetaAll,
11204
12179
  getServiceDbPath,
12180
+ getMachineStatus,
12181
+ getMachineRecord,
11205
12182
  getHasnaDir,
12183
+ getDefaultDaemonConfig,
11206
12184
  getDbPath,
11207
12185
  getDataDir,
12186
+ getDaemonStatus,
12187
+ getDaemonStatePath,
12188
+ getDaemonConfig,
12189
+ getCurrentMachineId,
11208
12190
  getConnectionString,
11209
12191
  getConflict,
11210
12192
  getConfigPath,
@@ -11213,6 +12195,8 @@ export {
11213
12195
  getAutoSyncConfig,
11214
12196
  ensureSyncMetaTable,
11215
12197
  ensurePgDatabase,
12198
+ ensureMachinesTable,
12199
+ ensureMachineIdColumn,
11216
12200
  ensureFeedbackTable,
11217
12201
  ensureConflictsTable,
11218
12202
  ensureAllPgDatabases,
@@ -11220,9 +12204,15 @@ export {
11220
12204
  discoverSyncableServices as discoverSyncableServicesV2,
11221
12205
  discoverSyncableServices2 as discoverSyncableServices,
11222
12206
  discoverServices,
12207
+ detectCurrentMachine,
11223
12208
  detectConflicts,
12209
+ createMachineRegistry,
12210
+ createMachineAwareAdapter,
12211
+ createDefaultDaemonState,
11224
12212
  createDatabase,
12213
+ applyTableIntervalRules,
11225
12214
  applyPgMigrations,
12215
+ applyFileRules,
11226
12216
  SyncProgressTracker,
11227
12217
  SqliteAdapter,
11228
12218
  SYNC_EXCLUDED_TABLE_PATTERNS,