@hasna/cloud 0.1.31 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -1
- package/README.md +17 -0
- package/dist/adapter.test.d.ts +2 -0
- package/dist/adapter.test.d.ts.map +1 -0
- package/dist/auto-sync.d.ts.map +1 -1
- package/dist/cli/cmd-doctor.d.ts +3 -0
- package/dist/cli/cmd-doctor.d.ts.map +1 -0
- package/dist/cli/cmd-feedback.d.ts +3 -0
- package/dist/cli/cmd-feedback.d.ts.map +1 -0
- package/dist/cli/cmd-migrate.d.ts +3 -0
- package/dist/cli/cmd-migrate.d.ts.map +1 -0
- package/dist/cli/cmd-setup.d.ts +3 -0
- package/dist/cli/cmd-setup.d.ts.map +1 -0
- package/dist/cli/cmd-sync.d.ts +4 -0
- package/dist/cli/cmd-sync.d.ts.map +1 -0
- package/dist/cli/index.js +2330 -1079
- package/dist/config.d.ts +138 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/daemon-sync.d.ts +108 -0
- package/dist/daemon-sync.d.ts.map +1 -0
- package/dist/dialect.test.d.ts +2 -0
- package/dist/dialect.test.d.ts.map +1 -0
- package/dist/discover.test.d.ts +2 -0
- package/dist/discover.test.d.ts.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1143 -153
- package/dist/machines.d.ts +63 -0
- package/dist/machines.d.ts.map +1 -0
- package/dist/mcp/bin.js +2 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +2 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +2125 -438
- package/dist/scheduled-sync.js +205 -44
- package/dist/sync-conflicts.test.d.ts +2 -0
- package/dist/sync-conflicts.test.d.ts.map +1 -0
- package/dist/sync-incremental.d.ts +5 -0
- package/dist/sync-incremental.d.ts.map +1 -1
- package/dist/sync-schedule.test.d.ts +2 -0
- package/dist/sync-schedule.test.d.ts.map +1 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.test.d.ts +2 -0
- package/dist/sync.test.d.ts.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -156,7 +156,7 @@ var require_arrayParser = __commonJS((exports, module) => {
|
|
|
156
156
|
};
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
// node_modules/
|
|
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/
|
|
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/
|
|
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
|
|
9364
|
-
import { join as
|
|
9365
|
-
import { homedir as
|
|
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 =
|
|
9371
|
-
if (!
|
|
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 =
|
|
9393
|
-
if (!
|
|
9309
|
+
const dataDir = join2(homedir2(), ".hasna", service);
|
|
9310
|
+
if (!existsSync2(dataDir))
|
|
9394
9311
|
return null;
|
|
9395
9312
|
const candidates = [
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
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(
|
|
9321
|
+
candidates.push(join2(dataDir, f));
|
|
9405
9322
|
}
|
|
9406
9323
|
}
|
|
9407
9324
|
} catch {}
|
|
9408
9325
|
for (const p of candidates) {
|
|
9409
|
-
if (
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
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,
|
|
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,
|
|
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
|
|
10411
|
-
import { homedir as
|
|
10412
|
-
import { join as
|
|
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 =
|
|
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 (!
|
|
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
|
-
|
|
10976
|
+
syncOnStart();
|
|
10514
10977
|
return origOnConnect.apply(server, args);
|
|
10515
10978
|
};
|
|
10516
10979
|
} else if (server && typeof server.on === "function") {
|
|
10517
|
-
server.on("connect", () =>
|
|
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
|
-
|
|
10987
|
+
syncOnStop();
|
|
10523
10988
|
return origOnDisconnect.apply(server, args);
|
|
10524
10989
|
};
|
|
10525
10990
|
} else if (server && typeof server.on === "function") {
|
|
10526
|
-
server.on("disconnect", () =>
|
|
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
|
|
10541
|
-
import { join as
|
|
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 =
|
|
10552
|
-
if (
|
|
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 =
|
|
10575
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
10619
|
-
import { existsSync as
|
|
10620
|
-
import { homedir as
|
|
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 =
|
|
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 :
|
|
10663
|
-
const tsPath =
|
|
10664
|
-
const jsPath =
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
11145
|
+
if (existsSync7(p))
|
|
10679
11146
|
return p;
|
|
10680
11147
|
}
|
|
10681
11148
|
return "bun";
|
|
10682
11149
|
}
|
|
10683
11150
|
function getLaunchdPlistPath() {
|
|
10684
|
-
return
|
|
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 =
|
|
10690
|
-
const errorLogPath =
|
|
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>${
|
|
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 =
|
|
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
|
|
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=${
|
|
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(
|
|
10777
|
-
writeFileSync2(
|
|
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(
|
|
11254
|
+
unlinkSync(join7(dir, `${SERVICE_NAME}.service`));
|
|
10788
11255
|
} catch {}
|
|
10789
11256
|
try {
|
|
10790
|
-
unlinkSync(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
10827
|
-
mechanism =
|
|
11293
|
+
if (platform2() === "darwin") {
|
|
11294
|
+
mechanism = existsSync7(getLaunchdPlistPath()) ? "launchd" : "none";
|
|
10828
11295
|
} else {
|
|
10829
|
-
mechanism =
|
|
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,
|