@hasna/connectors 1.3.11 → 1.3.12

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/bin/index.js CHANGED
@@ -1905,6 +1905,75 @@ var require_commander = __commonJS((exports) => {
1905
1905
  });
1906
1906
 
1907
1907
  // node_modules/@hasna/cloud/dist/index.js
1908
+ var exports_dist = {};
1909
+ __export(exports_dist, {
1910
+ translateSql: () => translateSql,
1911
+ translateParams: () => translateParams,
1912
+ translateDdl: () => translateDdl,
1913
+ syncPush: () => syncPush,
1914
+ syncPull: () => syncPull,
1915
+ storeConflicts: () => storeConflicts,
1916
+ setupAutoSync: () => setupAutoSync,
1917
+ sendFeedback: () => sendFeedback,
1918
+ saveFeedback: () => saveFeedback,
1919
+ saveCloudConfig: () => saveCloudConfig,
1920
+ runScheduledSync: () => runScheduledSync,
1921
+ resolveConflicts: () => resolveConflicts,
1922
+ resolveConflict: () => resolveConflict,
1923
+ resetSyncMeta: () => resetSyncMeta,
1924
+ resetAllSyncMeta: () => resetAllSyncMeta,
1925
+ removeSyncSchedule: () => removeSyncSchedule,
1926
+ registerSyncSchedule: () => registerSyncSchedule,
1927
+ registerCloudTools: () => registerCloudTools,
1928
+ registerCloudCommands: () => registerCloudCommands,
1929
+ purgeResolvedConflicts: () => purgeResolvedConflicts,
1930
+ parseInterval: () => parseInterval,
1931
+ minutesToCron: () => minutesToCron,
1932
+ migrateService: () => migrateService,
1933
+ migrateDotfile: () => migrateDotfile,
1934
+ migrateAllServices: () => migrateAllServices,
1935
+ listSqliteTables: () => listSqliteTables,
1936
+ listPgTables: () => listPgTables,
1937
+ listFeedback: () => listFeedback,
1938
+ listConflicts: () => listConflicts,
1939
+ isSyncExcludedTable: () => isSyncExcludedTable,
1940
+ incrementalSyncPush: () => incrementalSyncPush,
1941
+ incrementalSyncPull: () => incrementalSyncPull,
1942
+ hasLegacyDotfile: () => hasLegacyDotfile,
1943
+ getWinningData: () => getWinningData,
1944
+ getSyncScheduleStatus: () => getSyncScheduleStatus,
1945
+ getSyncMetaForTable: () => getSyncMetaForTable,
1946
+ getSyncMetaAll: () => getSyncMetaAll,
1947
+ getServiceDbPath: () => getServiceDbPath,
1948
+ getHasnaDir: () => getHasnaDir,
1949
+ getDbPath: () => getDbPath,
1950
+ getDataDir: () => getDataDir,
1951
+ getConnectionString: () => getConnectionString,
1952
+ getConflict: () => getConflict,
1953
+ getConfigPath: () => getConfigPath,
1954
+ getConfigDir: () => getConfigDir,
1955
+ getCloudConfig: () => getCloudConfig,
1956
+ getAutoSyncConfig: () => getAutoSyncConfig,
1957
+ ensureSyncMetaTable: () => ensureSyncMetaTable,
1958
+ ensurePgDatabase: () => ensurePgDatabase,
1959
+ ensureFeedbackTable: () => ensureFeedbackTable,
1960
+ ensureConflictsTable: () => ensureConflictsTable,
1961
+ ensureAllPgDatabases: () => ensureAllPgDatabases,
1962
+ enableAutoSync: () => enableAutoSync,
1963
+ discoverSyncableServicesV2: () => discoverSyncableServices,
1964
+ discoverSyncableServices: () => discoverSyncableServices2,
1965
+ discoverServices: () => discoverServices,
1966
+ detectConflicts: () => detectConflicts,
1967
+ createDatabase: () => createDatabase,
1968
+ applyPgMigrations: () => applyPgMigrations,
1969
+ SyncProgressTracker: () => SyncProgressTracker,
1970
+ SqliteAdapter: () => SqliteAdapter,
1971
+ SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
1972
+ PgAdapterAsync: () => PgAdapterAsync,
1973
+ PgAdapter: () => PgAdapter,
1974
+ KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES,
1975
+ CloudConfigSchema: () => CloudConfigSchema
1976
+ });
1908
1977
  import { createRequire } from "module";
1909
1978
  import { Database } from "bun:sqlite";
1910
1979
  import {
@@ -1922,9 +1991,13 @@ import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
1922
1991
  import { join as join3 } from "path";
1923
1992
  import { homedir as homedir3 } from "os";
1924
1993
  import { hostname } from "os";
1994
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1925
1995
  import { homedir as homedir4 } from "os";
1926
1996
  import { join as join4 } from "path";
1997
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
1998
+ import { join as join5 } from "path";
1927
1999
  import { join as join6, dirname } from "path";
2000
+ import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
1928
2001
  import { homedir as homedir5, platform } from "os";
1929
2002
  function __accessProp2(key) {
1930
2003
  return this[key];
@@ -1979,6 +2052,17 @@ function sqliteToPostgres(sql) {
1979
2052
  }
1980
2053
  return out;
1981
2054
  }
2055
+ function translateDdl(ddl, dialect) {
2056
+ if (dialect === "sqlite")
2057
+ return ddl;
2058
+ let out = ddl;
2059
+ out = out.replace(/\bINTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT\b/gi, "BIGSERIAL PRIMARY KEY");
2060
+ out = out.replace(/\bAUTOINCREMENT\b/gi, "");
2061
+ out = out.replace(/\bREAL\b/gi, "DOUBLE PRECISION");
2062
+ out = out.replace(/\bBLOB\b/gi, "BYTEA");
2063
+ out = sqliteToPostgres(out);
2064
+ return out;
2065
+ }
1982
2066
 
1983
2067
  class SqliteAdapter {
1984
2068
  db;
@@ -2804,6 +2888,39 @@ function getDbPath(serviceName) {
2804
2888
  const dir = getDataDir(serviceName);
2805
2889
  return join(dir, `${serviceName}.db`);
2806
2890
  }
2891
+ function migrateDotfile(serviceName) {
2892
+ const legacyDir = join(homedir(), `.${serviceName}`);
2893
+ const newDir = join(HASNA_DIR, serviceName);
2894
+ if (!existsSync(legacyDir))
2895
+ return [];
2896
+ if (existsSync(newDir))
2897
+ return [];
2898
+ mkdirSync(newDir, { recursive: true });
2899
+ const migrated = [];
2900
+ copyDirRecursive(legacyDir, newDir, legacyDir, migrated);
2901
+ return migrated;
2902
+ }
2903
+ function copyDirRecursive(src, dest, root, migrated) {
2904
+ const entries = readdirSync(src, { withFileTypes: true });
2905
+ for (const entry of entries) {
2906
+ const srcPath = join(src, entry.name);
2907
+ const destPath = join(dest, entry.name);
2908
+ if (entry.isDirectory()) {
2909
+ mkdirSync(destPath, { recursive: true });
2910
+ copyDirRecursive(srcPath, destPath, root, migrated);
2911
+ } else {
2912
+ copyFileSync(srcPath, destPath);
2913
+ migrated.push(relative(root, srcPath));
2914
+ }
2915
+ }
2916
+ }
2917
+ function hasLegacyDotfile(serviceName) {
2918
+ return existsSync(join(homedir(), `.${serviceName}`));
2919
+ }
2920
+ function getHasnaDir() {
2921
+ mkdirSync(HASNA_DIR, { recursive: true });
2922
+ return HASNA_DIR;
2923
+ }
2807
2924
  function getConfigDir() {
2808
2925
  return CONFIG_DIR;
2809
2926
  }
@@ -3376,6 +3493,10 @@ async function sendFeedback(feedback, db) {
3376
3493
  return { sent: false, id, error: errorMsg };
3377
3494
  }
3378
3495
  }
3496
+ function listFeedback(db) {
3497
+ ensureFeedbackTable(db);
3498
+ return db.all(`SELECT id, service, version, message, email, machine_id, created_at FROM feedback ORDER BY created_at DESC`);
3499
+ }
3379
3500
 
3380
3501
  class SyncProgressTracker {
3381
3502
  db;
@@ -3496,6 +3617,947 @@ class SyncProgressTracker {
3496
3617
  }
3497
3618
  }
3498
3619
  }
3620
+ function detectConflicts(local, remote, table, primaryKey = "id", conflictColumn = "updated_at") {
3621
+ const conflicts = [];
3622
+ const remoteMap = new Map;
3623
+ for (const row of remote) {
3624
+ const key = String(row[primaryKey]);
3625
+ remoteMap.set(key, row);
3626
+ }
3627
+ for (const localRow of local) {
3628
+ const key = String(localRow[primaryKey]);
3629
+ const remoteRow = remoteMap.get(key);
3630
+ if (!remoteRow)
3631
+ continue;
3632
+ const localTs = localRow[conflictColumn];
3633
+ const remoteTs = remoteRow[conflictColumn];
3634
+ if (localTs !== remoteTs) {
3635
+ conflicts.push({
3636
+ table,
3637
+ row_id: key,
3638
+ local_updated_at: String(localTs ?? ""),
3639
+ remote_updated_at: String(remoteTs ?? ""),
3640
+ local_data: { ...localRow },
3641
+ remote_data: { ...remoteRow },
3642
+ resolved: false
3643
+ });
3644
+ }
3645
+ }
3646
+ return conflicts;
3647
+ }
3648
+ function resolveConflicts(conflicts, strategy = "newest-wins") {
3649
+ return conflicts.map((conflict) => {
3650
+ const resolved = { ...conflict, resolved: true, resolution: strategy };
3651
+ switch (strategy) {
3652
+ case "local-wins":
3653
+ break;
3654
+ case "remote-wins":
3655
+ break;
3656
+ case "newest-wins": {
3657
+ const localTime = new Date(conflict.local_updated_at).getTime();
3658
+ const remoteTime = new Date(conflict.remote_updated_at).getTime();
3659
+ if (remoteTime > localTime) {
3660
+ resolved.resolution = "newest-wins";
3661
+ } else {
3662
+ resolved.resolution = "newest-wins";
3663
+ }
3664
+ break;
3665
+ }
3666
+ }
3667
+ return resolved;
3668
+ });
3669
+ }
3670
+ function getWinningData(conflict) {
3671
+ if (!conflict.resolved || !conflict.resolution) {
3672
+ throw new Error(`Conflict for row ${conflict.row_id} is not resolved`);
3673
+ }
3674
+ switch (conflict.resolution) {
3675
+ case "local-wins":
3676
+ return conflict.local_data;
3677
+ case "remote-wins":
3678
+ return conflict.remote_data;
3679
+ case "newest-wins": {
3680
+ const localTime = new Date(conflict.local_updated_at).getTime();
3681
+ const remoteTime = new Date(conflict.remote_updated_at).getTime();
3682
+ return remoteTime >= localTime ? conflict.remote_data : conflict.local_data;
3683
+ }
3684
+ case "manual":
3685
+ return conflict.local_data;
3686
+ default:
3687
+ return conflict.local_data;
3688
+ }
3689
+ }
3690
+ function ensureConflictsTable(db) {
3691
+ db.exec(`
3692
+ CREATE TABLE IF NOT EXISTS _sync_conflicts (
3693
+ id TEXT PRIMARY KEY,
3694
+ table_name TEXT,
3695
+ row_id TEXT,
3696
+ local_data TEXT,
3697
+ remote_data TEXT,
3698
+ local_updated_at TEXT,
3699
+ remote_updated_at TEXT,
3700
+ resolution TEXT,
3701
+ resolved_at TEXT,
3702
+ created_at TEXT DEFAULT (datetime('now'))
3703
+ )
3704
+ `);
3705
+ }
3706
+ function storeConflicts(db, conflicts) {
3707
+ ensureConflictsTable(db);
3708
+ for (const conflict of conflicts) {
3709
+ const id = `${conflict.table}:${conflict.row_id}:${Date.now()}`;
3710
+ db.run(`INSERT INTO _sync_conflicts (id, table_name, row_id, local_data, remote_data, local_updated_at, remote_updated_at, resolution, resolved_at)
3711
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, conflict.table, conflict.row_id, JSON.stringify(conflict.local_data), JSON.stringify(conflict.remote_data), conflict.local_updated_at, conflict.remote_updated_at, conflict.resolution ?? null, conflict.resolved ? new Date().toISOString() : null);
3712
+ }
3713
+ }
3714
+ function listConflicts(db, opts) {
3715
+ ensureConflictsTable(db);
3716
+ let sql = `SELECT * FROM _sync_conflicts WHERE 1=1`;
3717
+ const params = [];
3718
+ if (opts?.resolved !== undefined) {
3719
+ if (opts.resolved) {
3720
+ sql += ` AND resolution IS NOT NULL AND resolved_at IS NOT NULL`;
3721
+ } else {
3722
+ sql += ` AND (resolution IS NULL OR resolved_at IS NULL)`;
3723
+ }
3724
+ }
3725
+ if (opts?.table) {
3726
+ sql += ` AND table_name = ?`;
3727
+ params.push(opts.table);
3728
+ }
3729
+ sql += ` ORDER BY created_at DESC`;
3730
+ return db.all(sql, ...params);
3731
+ }
3732
+ function resolveConflict(db, conflictId, strategy) {
3733
+ ensureConflictsTable(db);
3734
+ const row = db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
3735
+ if (!row)
3736
+ return null;
3737
+ db.run(`UPDATE _sync_conflicts SET resolution = ?, resolved_at = datetime('now') WHERE id = ?`, strategy, conflictId);
3738
+ return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
3739
+ }
3740
+ function getConflict(db, conflictId) {
3741
+ ensureConflictsTable(db);
3742
+ return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
3743
+ }
3744
+ function purgeResolvedConflicts(db) {
3745
+ ensureConflictsTable(db);
3746
+ const result = db.run(`DELETE FROM _sync_conflicts WHERE resolution IS NOT NULL AND resolved_at IS NOT NULL`);
3747
+ return result.changes;
3748
+ }
3749
+ function ensureSyncMetaTable(db) {
3750
+ db.exec(SYNC_META_TABLE_SQL);
3751
+ }
3752
+ function getSyncMeta(db, table) {
3753
+ ensureSyncMetaTable(db);
3754
+ return db.get(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta WHERE table_name = ?`, table) ?? null;
3755
+ }
3756
+ function upsertSyncMeta(db, meta) {
3757
+ ensureSyncMetaTable(db);
3758
+ const existing = db.get(`SELECT table_name FROM _sync_meta WHERE table_name = ?`, meta.table_name);
3759
+ if (existing) {
3760
+ db.run(`UPDATE _sync_meta SET last_synced_at = ?, last_synced_row_count = ?, direction = ? WHERE table_name = ?`, meta.last_synced_at, meta.last_synced_row_count, meta.direction, meta.table_name);
3761
+ } else {
3762
+ db.run(`INSERT INTO _sync_meta (table_name, last_synced_at, last_synced_row_count, direction) VALUES (?, ?, ?, ?)`, meta.table_name, meta.last_synced_at, meta.last_synced_row_count, meta.direction);
3763
+ }
3764
+ }
3765
+ function transferRows(source, target, table, rows, options) {
3766
+ const { primaryKey = "id", conflictColumn = "updated_at" } = options;
3767
+ let written = 0;
3768
+ let skipped = 0;
3769
+ const errors2 = [];
3770
+ if (rows.length === 0)
3771
+ return { written, skipped, errors: errors2 };
3772
+ const columns = Object.keys(rows[0]);
3773
+ const hasConflictCol = columns.includes(conflictColumn);
3774
+ const hasPrimaryKey = columns.includes(primaryKey);
3775
+ if (!hasPrimaryKey) {
3776
+ errors2.push(`Table "${table}" has no "${primaryKey}" column -- skipping`);
3777
+ return { written, skipped, errors: errors2 };
3778
+ }
3779
+ for (const row of rows) {
3780
+ try {
3781
+ const existing = target.get(`SELECT "${primaryKey}"${hasConflictCol ? `, "${conflictColumn}"` : ""} FROM "${table}" WHERE "${primaryKey}" = ?`, row[primaryKey]);
3782
+ if (existing) {
3783
+ if (hasConflictCol && existing[conflictColumn] && row[conflictColumn]) {
3784
+ const existingTime = new Date(existing[conflictColumn]).getTime();
3785
+ const incomingTime = new Date(row[conflictColumn]).getTime();
3786
+ if (existingTime >= incomingTime) {
3787
+ skipped++;
3788
+ continue;
3789
+ }
3790
+ }
3791
+ const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
3792
+ const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
3793
+ values.push(row[primaryKey]);
3794
+ target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
3795
+ } else {
3796
+ const placeholders = columns.map(() => "?").join(", ");
3797
+ const colList = columns.map((c) => `"${c}"`).join(", ");
3798
+ const values = columns.map((c) => row[c]);
3799
+ target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
3800
+ }
3801
+ written++;
3802
+ } catch (err) {
3803
+ errors2.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
3804
+ }
3805
+ }
3806
+ return { written, skipped, errors: errors2 };
3807
+ }
3808
+ function incrementalSyncPush(local, remote, tables, options = {}) {
3809
+ const { conflictColumn = "updated_at", batchSize = 500 } = options;
3810
+ const results = [];
3811
+ ensureSyncMetaTable(local);
3812
+ for (const table of tables) {
3813
+ const stat = {
3814
+ table,
3815
+ total_rows: 0,
3816
+ synced_rows: 0,
3817
+ skipped_rows: 0,
3818
+ errors: [],
3819
+ first_sync: false
3820
+ };
3821
+ try {
3822
+ const countResult = local.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
3823
+ stat.total_rows = countResult?.cnt ?? 0;
3824
+ const meta = getSyncMeta(local, table);
3825
+ let rows;
3826
+ if (meta?.last_synced_at) {
3827
+ try {
3828
+ rows = local.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
3829
+ } catch {
3830
+ rows = local.all(`SELECT * FROM "${table}"`);
3831
+ stat.first_sync = true;
3832
+ }
3833
+ } else {
3834
+ rows = local.all(`SELECT * FROM "${table}"`);
3835
+ stat.first_sync = true;
3836
+ }
3837
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
3838
+ const batch = rows.slice(offset, offset + batchSize);
3839
+ const result = transferRows(local, remote, table, batch, options);
3840
+ stat.synced_rows += result.written;
3841
+ stat.skipped_rows += result.skipped;
3842
+ stat.errors.push(...result.errors);
3843
+ }
3844
+ if (rows.length === 0) {
3845
+ stat.skipped_rows = stat.total_rows;
3846
+ }
3847
+ const now = new Date().toISOString();
3848
+ upsertSyncMeta(local, {
3849
+ table_name: table,
3850
+ last_synced_at: now,
3851
+ last_synced_row_count: stat.synced_rows,
3852
+ direction: "push"
3853
+ });
3854
+ } catch (err) {
3855
+ stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
3856
+ }
3857
+ results.push(stat);
3858
+ }
3859
+ return results;
3860
+ }
3861
+ function incrementalSyncPull(remote, local, tables, options = {}) {
3862
+ const { conflictColumn = "updated_at", batchSize = 500 } = options;
3863
+ const results = [];
3864
+ ensureSyncMetaTable(local);
3865
+ for (const table of tables) {
3866
+ const stat = {
3867
+ table,
3868
+ total_rows: 0,
3869
+ synced_rows: 0,
3870
+ skipped_rows: 0,
3871
+ errors: [],
3872
+ first_sync: false
3873
+ };
3874
+ try {
3875
+ const countResult = remote.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
3876
+ stat.total_rows = countResult?.cnt ?? 0;
3877
+ const meta = getSyncMeta(local, table);
3878
+ let rows;
3879
+ if (meta?.last_synced_at) {
3880
+ try {
3881
+ rows = remote.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
3882
+ } catch {
3883
+ rows = remote.all(`SELECT * FROM "${table}"`);
3884
+ stat.first_sync = true;
3885
+ }
3886
+ } else {
3887
+ rows = remote.all(`SELECT * FROM "${table}"`);
3888
+ stat.first_sync = true;
3889
+ }
3890
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
3891
+ const batch = rows.slice(offset, offset + batchSize);
3892
+ const result = transferRows(remote, local, table, batch, options);
3893
+ stat.synced_rows += result.written;
3894
+ stat.skipped_rows += result.skipped;
3895
+ stat.errors.push(...result.errors);
3896
+ }
3897
+ if (rows.length === 0) {
3898
+ stat.skipped_rows = stat.total_rows;
3899
+ }
3900
+ const now = new Date().toISOString();
3901
+ upsertSyncMeta(local, {
3902
+ table_name: table,
3903
+ last_synced_at: now,
3904
+ last_synced_row_count: stat.synced_rows,
3905
+ direction: "pull"
3906
+ });
3907
+ } catch (err) {
3908
+ stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
3909
+ }
3910
+ results.push(stat);
3911
+ }
3912
+ return results;
3913
+ }
3914
+ function getSyncMetaAll(db) {
3915
+ ensureSyncMetaTable(db);
3916
+ return db.all(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta ORDER BY table_name`);
3917
+ }
3918
+ function getSyncMetaForTable(db, table) {
3919
+ return getSyncMeta(db, table);
3920
+ }
3921
+ function resetSyncMeta(db, table) {
3922
+ ensureSyncMetaTable(db);
3923
+ db.run(`DELETE FROM _sync_meta WHERE table_name = ?`, table);
3924
+ }
3925
+ function resetAllSyncMeta(db) {
3926
+ ensureSyncMetaTable(db);
3927
+ db.run(`DELETE FROM _sync_meta`);
3928
+ }
3929
+ function getAutoSyncConfig() {
3930
+ try {
3931
+ if (!existsSync4(AUTO_SYNC_CONFIG_PATH)) {
3932
+ return { ...DEFAULT_AUTO_SYNC_CONFIG };
3933
+ }
3934
+ const raw = JSON.parse(readFileSync2(AUTO_SYNC_CONFIG_PATH, "utf-8"));
3935
+ return {
3936
+ auto_sync_on_start: typeof raw.auto_sync_on_start === "boolean" ? raw.auto_sync_on_start : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_start,
3937
+ auto_sync_on_stop: typeof raw.auto_sync_on_stop === "boolean" ? raw.auto_sync_on_stop : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_stop
3938
+ };
3939
+ } catch {
3940
+ return { ...DEFAULT_AUTO_SYNC_CONFIG };
3941
+ }
3942
+ }
3943
+ async function executeAutoSync(event, serviceName, local, tables) {
3944
+ const direction = event === "start" ? "pull" : "push";
3945
+ const result = {
3946
+ event,
3947
+ direction,
3948
+ success: false,
3949
+ tables_synced: 0,
3950
+ total_rows_synced: 0,
3951
+ errors: []
3952
+ };
3953
+ let remote = null;
3954
+ try {
3955
+ const connStr = getConnectionString(serviceName);
3956
+ remote = new PgAdapterAsync(connStr);
3957
+ const syncTables = tables.length > 0 ? tables.filter((t) => !isSyncExcludedTable(t)) : direction === "push" ? listSqliteTables(local).filter((t) => !isSyncExcludedTable(t)) : (await listPgTables(remote)).filter((t) => !isSyncExcludedTable(t));
3958
+ if (syncTables.length === 0) {
3959
+ result.success = true;
3960
+ return result;
3961
+ }
3962
+ const results = direction === "pull" ? await syncPull(remote, local, { tables: syncTables }) : await syncPush(local, remote, { tables: syncTables });
3963
+ for (const r of results) {
3964
+ if (r.errors.length === 0)
3965
+ result.tables_synced++;
3966
+ result.total_rows_synced += r.rowsWritten;
3967
+ result.errors.push(...r.errors);
3968
+ }
3969
+ result.success = result.errors.length === 0;
3970
+ } catch (err) {
3971
+ result.errors.push(err?.message ?? String(err));
3972
+ } finally {
3973
+ if (remote) {
3974
+ try {
3975
+ await remote.close();
3976
+ } catch {}
3977
+ }
3978
+ }
3979
+ return result;
3980
+ }
3981
+ function installSignalHandlers() {
3982
+ if (signalHandlersInstalled)
3983
+ return;
3984
+ signalHandlersInstalled = true;
3985
+ const handleExit = async () => {
3986
+ for (const fn of cleanupHandlers) {
3987
+ try {
3988
+ await fn();
3989
+ } catch {}
3990
+ }
3991
+ };
3992
+ process.on("SIGTERM", async () => {
3993
+ await handleExit();
3994
+ process.exit(0);
3995
+ });
3996
+ process.on("SIGINT", async () => {
3997
+ await handleExit();
3998
+ process.exit(0);
3999
+ });
4000
+ process.on("beforeExit", async () => {
4001
+ await handleExit();
4002
+ });
4003
+ }
4004
+ function setupAutoSync(serviceName, server, local, remote, tables) {
4005
+ const config = getAutoSyncConfig();
4006
+ const cloudConfig = getCloudConfig();
4007
+ const isSyncEnabled = cloudConfig.mode === "hybrid" || cloudConfig.mode === "cloud";
4008
+ const syncOnStart = async () => {
4009
+ if (!config.auto_sync_on_start || !isSyncEnabled)
4010
+ return null;
4011
+ return executeAutoSync("start", serviceName, local, tables);
4012
+ };
4013
+ const syncOnStop = async () => {
4014
+ if (!config.auto_sync_on_stop || !isSyncEnabled)
4015
+ return null;
4016
+ return executeAutoSync("stop", serviceName, local, tables);
4017
+ };
4018
+ if (server && typeof server.onconnect === "function") {
4019
+ const origOnConnect = server.onconnect;
4020
+ server.onconnect = async (...args) => {
4021
+ await syncOnStart();
4022
+ return origOnConnect.apply(server, args);
4023
+ };
4024
+ } else if (server && typeof server.on === "function") {
4025
+ server.on("connect", () => syncOnStart());
4026
+ }
4027
+ if (server && typeof server.ondisconnect === "function") {
4028
+ const origOnDisconnect = server.ondisconnect;
4029
+ server.ondisconnect = async (...args) => {
4030
+ await syncOnStop();
4031
+ return origOnDisconnect.apply(server, args);
4032
+ };
4033
+ } else if (server && typeof server.on === "function") {
4034
+ server.on("disconnect", () => syncOnStop());
4035
+ }
4036
+ installSignalHandlers();
4037
+ cleanupHandlers.push(async () => {
4038
+ await syncOnStop();
4039
+ });
4040
+ return { syncOnStart, syncOnStop, config };
4041
+ }
4042
+ function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
4043
+ setupAutoSync(serviceName, mcpServer, local, remote, tables);
4044
+ }
4045
+ function discoverSyncableServices2() {
4046
+ const hasnaDir = getHasnaDir();
4047
+ const services = [];
4048
+ try {
4049
+ const entries = readdirSync3(hasnaDir, { withFileTypes: true });
4050
+ for (const entry of entries) {
4051
+ if (!entry.isDirectory())
4052
+ continue;
4053
+ const dbPath = join5(hasnaDir, entry.name, `${entry.name}.db`);
4054
+ if (existsSync5(dbPath)) {
4055
+ services.push(entry.name);
4056
+ }
4057
+ }
4058
+ } catch {}
4059
+ return services;
4060
+ }
4061
+ async function runScheduledSync() {
4062
+ const config = getCloudConfig();
4063
+ if (config.mode === "local")
4064
+ return [];
4065
+ const services = discoverSyncableServices2();
4066
+ const results = [];
4067
+ let remote = null;
4068
+ for (const service of services) {
4069
+ const result = {
4070
+ service,
4071
+ tables_synced: 0,
4072
+ total_rows_synced: 0,
4073
+ errors: []
4074
+ };
4075
+ try {
4076
+ const dbPath = join5(getDataDir(service), `${service}.db`);
4077
+ if (!existsSync5(dbPath)) {
4078
+ continue;
4079
+ }
4080
+ const local = new SqliteAdapter(dbPath);
4081
+ const tables = listSqliteTables(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
4082
+ if (tables.length === 0) {
4083
+ local.close();
4084
+ continue;
4085
+ }
4086
+ try {
4087
+ const connStr = getConnectionString(service);
4088
+ remote = new PgAdapterAsync(connStr);
4089
+ } catch (err) {
4090
+ result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
4091
+ local.close();
4092
+ results.push(result);
4093
+ continue;
4094
+ }
4095
+ const stats = incrementalSyncPush(local, remote, tables);
4096
+ for (const s of stats) {
4097
+ if (s.errors.length === 0) {
4098
+ result.tables_synced++;
4099
+ }
4100
+ result.total_rows_synced += s.synced_rows;
4101
+ result.errors.push(...s.errors);
4102
+ }
4103
+ local.close();
4104
+ await remote.close();
4105
+ remote = null;
4106
+ } catch (err) {
4107
+ result.errors.push(err?.message ?? String(err));
4108
+ }
4109
+ results.push(result);
4110
+ }
4111
+ if (remote) {
4112
+ try {
4113
+ await remote.close();
4114
+ } catch {}
4115
+ }
4116
+ return results;
4117
+ }
4118
+ function parseInterval(input) {
4119
+ const trimmed = input.trim().toLowerCase();
4120
+ const hourMatch = trimmed.match(/^(\d+)\s*h$/);
4121
+ if (hourMatch) {
4122
+ const hours = parseInt(hourMatch[1], 10);
4123
+ if (hours <= 0) {
4124
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
4125
+ }
4126
+ return hours * 60;
4127
+ }
4128
+ const minMatch = trimmed.match(/^(\d+)\s*m$/);
4129
+ if (minMatch) {
4130
+ const mins = parseInt(minMatch[1], 10);
4131
+ if (mins <= 0) {
4132
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
4133
+ }
4134
+ return mins;
4135
+ }
4136
+ const plain = parseInt(trimmed, 10);
4137
+ if (!isNaN(plain) && plain > 0) {
4138
+ return plain;
4139
+ }
4140
+ throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
4141
+ }
4142
+ function minutesToCron(minutes) {
4143
+ if (minutes <= 0) {
4144
+ throw new Error("Interval must be greater than 0 minutes.");
4145
+ }
4146
+ if (minutes < 60) {
4147
+ return `*/${minutes} * * * *`;
4148
+ }
4149
+ const hours = Math.floor(minutes / 60);
4150
+ const remainderMins = minutes % 60;
4151
+ if (remainderMins === 0 && hours <= 24) {
4152
+ return `0 */${hours} * * *`;
4153
+ }
4154
+ return `*/${minutes} * * * *`;
4155
+ }
4156
+ function getWorkerPath() {
4157
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
4158
+ const tsPath = join6(dir, "scheduled-sync.ts");
4159
+ const jsPath = join6(dir, "scheduled-sync.js");
4160
+ try {
4161
+ if (existsSync6(tsPath))
4162
+ return tsPath;
4163
+ } catch {}
4164
+ return jsPath;
4165
+ }
4166
+ function getBunPath() {
4167
+ const candidates = [
4168
+ join6(homedir5(), ".bun", "bin", "bun"),
4169
+ "/usr/local/bin/bun",
4170
+ "/usr/bin/bun"
4171
+ ];
4172
+ for (const p of candidates) {
4173
+ if (existsSync6(p))
4174
+ return p;
4175
+ }
4176
+ return "bun";
4177
+ }
4178
+ function getLaunchdPlistPath() {
4179
+ return join6(homedir5(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
4180
+ }
4181
+ function createLaunchdPlist(intervalMinutes) {
4182
+ const workerPath = getWorkerPath();
4183
+ const bunPath = getBunPath();
4184
+ const logPath = join6(CONFIG_DIR2, "sync.log");
4185
+ const errorLogPath = join6(CONFIG_DIR2, "sync-error.log");
4186
+ return `<?xml version="1.0" encoding="UTF-8"?>
4187
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4188
+ <plist version="1.0">
4189
+ <dict>
4190
+ <key>Label</key>
4191
+ <string>com.hasna.cloud-sync</string>
4192
+ <key>ProgramArguments</key>
4193
+ <array>
4194
+ <string>${bunPath}</string>
4195
+ <string>run</string>
4196
+ <string>${workerPath}</string>
4197
+ </array>
4198
+ <key>StartInterval</key>
4199
+ <integer>${intervalMinutes * 60}</integer>
4200
+ <key>RunAtLoad</key>
4201
+ <true/>
4202
+ <key>StandardOutPath</key>
4203
+ <string>${logPath}</string>
4204
+ <key>StandardErrorPath</key>
4205
+ <string>${errorLogPath}</string>
4206
+ <key>EnvironmentVariables</key>
4207
+ <dict>
4208
+ <key>PATH</key>
4209
+ <string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
4210
+ <key>HOME</key>
4211
+ <string>${homedir5()}</string>
4212
+ </dict>
4213
+ </dict>
4214
+ </plist>`;
4215
+ }
4216
+ async function registerLaunchd(intervalMinutes) {
4217
+ const plistPath = getLaunchdPlistPath();
4218
+ const plistDir = dirname(plistPath);
4219
+ mkdirSync3(plistDir, { recursive: true });
4220
+ try {
4221
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
4222
+ } catch {}
4223
+ writeFileSync2(plistPath, createLaunchdPlist(intervalMinutes));
4224
+ await Bun.spawn(["launchctl", "load", plistPath]).exited;
4225
+ }
4226
+ async function removeLaunchd() {
4227
+ const plistPath = getLaunchdPlistPath();
4228
+ try {
4229
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
4230
+ } catch {}
4231
+ try {
4232
+ unlinkSync(plistPath);
4233
+ } catch {}
4234
+ }
4235
+ function getSystemdDir() {
4236
+ return join6(homedir5(), ".config", "systemd", "user");
4237
+ }
4238
+ function createSystemdService() {
4239
+ const workerPath = getWorkerPath();
4240
+ const bunPath = getBunPath();
4241
+ return `[Unit]
4242
+ Description=Hasna Cloud Sync
4243
+ After=network.target
4244
+
4245
+ [Service]
4246
+ Type=oneshot
4247
+ ExecStart=${bunPath} run ${workerPath}
4248
+ Environment=HOME=${homedir5()}
4249
+ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
4250
+
4251
+ [Install]
4252
+ WantedBy=default.target
4253
+ `;
4254
+ }
4255
+ function createSystemdTimer(intervalMinutes) {
4256
+ return `[Unit]
4257
+ Description=Hasna Cloud Sync Timer
4258
+
4259
+ [Timer]
4260
+ OnBootSec=${intervalMinutes}min
4261
+ OnUnitActiveSec=${intervalMinutes}min
4262
+ Persistent=true
4263
+
4264
+ [Install]
4265
+ WantedBy=timers.target
4266
+ `;
4267
+ }
4268
+ async function registerSystemd(intervalMinutes) {
4269
+ const dir = getSystemdDir();
4270
+ mkdirSync3(dir, { recursive: true });
4271
+ writeFileSync2(join6(dir, `${SERVICE_NAME}.service`), createSystemdService());
4272
+ writeFileSync2(join6(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
4273
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
4274
+ await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
4275
+ }
4276
+ async function removeSystemd() {
4277
+ try {
4278
+ await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SERVICE_NAME}.timer`]).exited;
4279
+ } catch {}
4280
+ const dir = getSystemdDir();
4281
+ try {
4282
+ unlinkSync(join6(dir, `${SERVICE_NAME}.service`));
4283
+ } catch {}
4284
+ try {
4285
+ unlinkSync(join6(dir, `${SERVICE_NAME}.timer`));
4286
+ } catch {}
4287
+ try {
4288
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
4289
+ } catch {}
4290
+ }
4291
+ async function registerSyncSchedule(intervalMinutes) {
4292
+ if (intervalMinutes <= 0) {
4293
+ throw new Error("Interval must be a positive number of minutes.");
4294
+ }
4295
+ mkdirSync3(CONFIG_DIR2, { recursive: true });
4296
+ if (platform() === "darwin") {
4297
+ await registerLaunchd(intervalMinutes);
4298
+ } else {
4299
+ await registerSystemd(intervalMinutes);
4300
+ }
4301
+ const config = getCloudConfig();
4302
+ config.sync.schedule_minutes = intervalMinutes;
4303
+ saveCloudConfig(config);
4304
+ }
4305
+ async function removeSyncSchedule() {
4306
+ if (platform() === "darwin") {
4307
+ await removeLaunchd();
4308
+ } else {
4309
+ await removeSystemd();
4310
+ }
4311
+ const config = getCloudConfig();
4312
+ config.sync.schedule_minutes = 0;
4313
+ saveCloudConfig(config);
4314
+ }
4315
+ function getSyncScheduleStatus() {
4316
+ const config = getCloudConfig();
4317
+ const minutes = config.sync.schedule_minutes;
4318
+ const registered = minutes > 0;
4319
+ let mechanism = "none";
4320
+ if (registered) {
4321
+ if (platform() === "darwin") {
4322
+ mechanism = existsSync6(getLaunchdPlistPath()) ? "launchd" : "none";
4323
+ } else {
4324
+ mechanism = existsSync6(join6(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
4325
+ }
4326
+ }
4327
+ return {
4328
+ registered,
4329
+ schedule_minutes: minutes,
4330
+ cron_expression: registered ? minutesToCron(minutes) : null,
4331
+ mechanism
4332
+ };
4333
+ }
4334
+ async function applyPgMigrations(connectionString, migrations, service = "unknown") {
4335
+ const pg2 = new PgAdapterAsync(connectionString);
4336
+ const result = {
4337
+ service,
4338
+ applied: [],
4339
+ alreadyApplied: [],
4340
+ errors: [],
4341
+ totalMigrations: migrations.length
4342
+ };
4343
+ try {
4344
+ await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (
4345
+ id SERIAL PRIMARY KEY,
4346
+ version INT UNIQUE NOT NULL,
4347
+ applied_at TIMESTAMPTZ DEFAULT NOW()
4348
+ )`);
4349
+ const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
4350
+ const appliedSet = new Set(applied.map((r) => r.version));
4351
+ for (let i = 0;i < migrations.length; i++) {
4352
+ if (appliedSet.has(i)) {
4353
+ result.alreadyApplied.push(i);
4354
+ continue;
4355
+ }
4356
+ try {
4357
+ await pg2.exec(migrations[i]);
4358
+ await pg2.run("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", i);
4359
+ result.applied.push(i);
4360
+ } catch (err) {
4361
+ result.errors.push(`Migration ${i}: ${err?.message ?? String(err)}`);
4362
+ break;
4363
+ }
4364
+ }
4365
+ } finally {
4366
+ await pg2.close();
4367
+ }
4368
+ return result;
4369
+ }
4370
+ function getServicePackage(service) {
4371
+ return `@hasna/${service}`;
4372
+ }
4373
+ async function loadServiceMigrations(service) {
4374
+ const pkg = getServicePackage(service);
4375
+ const paths = [
4376
+ `${pkg}/pg-migrations`,
4377
+ `${pkg}/dist/db/pg-migrations.js`,
4378
+ `${pkg}/dist/db/pg-migrations`
4379
+ ];
4380
+ for (const path of paths) {
4381
+ try {
4382
+ const mod = await import(path);
4383
+ if (Array.isArray(mod.PG_MIGRATIONS)) {
4384
+ return mod.PG_MIGRATIONS;
4385
+ }
4386
+ if (mod.default && Array.isArray(mod.default.PG_MIGRATIONS)) {
4387
+ return mod.default.PG_MIGRATIONS;
4388
+ }
4389
+ } catch {}
4390
+ }
4391
+ return null;
4392
+ }
4393
+ async function migrateService(service, connectionString) {
4394
+ const connStr = connectionString ?? getConnectionString(service);
4395
+ const migrations = await loadServiceMigrations(service);
4396
+ if (!migrations) {
4397
+ return {
4398
+ service,
4399
+ applied: [],
4400
+ alreadyApplied: [],
4401
+ errors: [`No PG migrations found for service "${service}"`],
4402
+ totalMigrations: 0
4403
+ };
4404
+ }
4405
+ return applyPgMigrations(connStr, migrations, service);
4406
+ }
4407
+ async function migrateAllServices() {
4408
+ const { discoverServices: discoverServices2 } = await Promise.resolve().then(() => (init_discover(), exports_discover));
4409
+ const services = discoverServices2();
4410
+ const results = [];
4411
+ for (const service of services) {
4412
+ try {
4413
+ const result = await migrateService(service);
4414
+ results.push(result);
4415
+ } catch (err) {
4416
+ results.push({
4417
+ service,
4418
+ applied: [],
4419
+ alreadyApplied: [],
4420
+ errors: [err?.message ?? String(err)],
4421
+ totalMigrations: 0
4422
+ });
4423
+ }
4424
+ }
4425
+ return results;
4426
+ }
4427
+ async function ensurePgDatabase(service) {
4428
+ const config = (await Promise.resolve().then(() => (init_config(), exports_config))).getCloudConfig();
4429
+ const { host, port, username, password_env, ssl } = config.rds;
4430
+ if (!host || !username)
4431
+ return false;
4432
+ const password = process.env[password_env] ?? "";
4433
+ const sslParam = ssl ? "?sslmode=require" : "";
4434
+ const adminConnStr = `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/postgres${sslParam}`;
4435
+ const pg2 = new PgAdapterAsync(adminConnStr);
4436
+ try {
4437
+ const existing = await pg2.all(`SELECT 1 FROM pg_database WHERE datname = $1`, service);
4438
+ if (existing.length === 0) {
4439
+ await pg2.exec(`CREATE DATABASE "${service}"`);
4440
+ return true;
4441
+ }
4442
+ return false;
4443
+ } finally {
4444
+ await pg2.close();
4445
+ }
4446
+ }
4447
+ async function ensureAllPgDatabases() {
4448
+ const { discoverServices: discoverServices2 } = await Promise.resolve().then(() => (init_discover(), exports_discover));
4449
+ const services = discoverServices2();
4450
+ const results = [];
4451
+ for (const service of services) {
4452
+ try {
4453
+ const created = await ensurePgDatabase(service);
4454
+ results.push({ service, created });
4455
+ } catch (err) {
4456
+ results.push({ service, created: false, error: err?.message ?? String(err) });
4457
+ }
4458
+ }
4459
+ return results;
4460
+ }
4461
+ function registerCloudTools(server, serviceName) {
4462
+ server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
4463
+ const config = getCloudConfig();
4464
+ const lines = [
4465
+ `Mode: ${config.mode}`,
4466
+ `Service: ${serviceName}`,
4467
+ `RDS Host: ${config.rds.host || "(not configured)"}`
4468
+ ];
4469
+ if (config.rds.host && config.rds.username) {
4470
+ try {
4471
+ const pg2 = new PgAdapterAsync(getConnectionString("postgres"));
4472
+ await pg2.get("SELECT 1 as ok");
4473
+ lines.push("PostgreSQL: connected");
4474
+ await pg2.close();
4475
+ } catch (err) {
4476
+ lines.push(`PostgreSQL: failed \u2014 ${err?.message}`);
4477
+ }
4478
+ }
4479
+ return { content: [{ type: "text", text: lines.join(`
4480
+ `) }] };
4481
+ });
4482
+ server.tool(`${serviceName}_cloud_push`, "Push local data to cloud PostgreSQL", {
4483
+ tables: exports_external.string().optional().describe("Comma-separated table names (default: all)")
4484
+ }, async ({ tables: tablesStr }) => {
4485
+ const config = getCloudConfig();
4486
+ if (config.mode === "local") {
4487
+ return {
4488
+ content: [
4489
+ { type: "text", text: "Error: cloud mode not configured." }
4490
+ ],
4491
+ isError: true
4492
+ };
4493
+ }
4494
+ const local = new SqliteAdapter(getDbPath(serviceName));
4495
+ const cloud = new PgAdapterAsync(getConnectionString(serviceName));
4496
+ const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
4497
+ const results = await syncPush(local, cloud, { tables: tableList });
4498
+ local.close();
4499
+ await cloud.close();
4500
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
4501
+ return {
4502
+ content: [{ type: "text", text: `Pushed ${total} rows across ${tableList.length} table(s).` }]
4503
+ };
4504
+ });
4505
+ server.tool(`${serviceName}_cloud_pull`, "Pull cloud PostgreSQL data to local", {
4506
+ tables: exports_external.string().optional().describe("Comma-separated table names (default: all)")
4507
+ }, async ({ tables: tablesStr }) => {
4508
+ const config = getCloudConfig();
4509
+ if (config.mode === "local") {
4510
+ return {
4511
+ content: [
4512
+ { type: "text", text: "Error: cloud mode not configured." }
4513
+ ],
4514
+ isError: true
4515
+ };
4516
+ }
4517
+ const local = new SqliteAdapter(getDbPath(serviceName));
4518
+ const cloud = new PgAdapterAsync(getConnectionString(serviceName));
4519
+ let tableList;
4520
+ if (tablesStr) {
4521
+ tableList = tablesStr.split(",").map((t) => t.trim());
4522
+ } else {
4523
+ try {
4524
+ tableList = await listPgTables(cloud);
4525
+ } catch {
4526
+ local.close();
4527
+ await cloud.close();
4528
+ return {
4529
+ content: [
4530
+ { type: "text", text: "Error: failed to list cloud tables." }
4531
+ ],
4532
+ isError: true
4533
+ };
4534
+ }
4535
+ }
4536
+ const results = await syncPull(cloud, local, { tables: tableList });
4537
+ local.close();
4538
+ await cloud.close();
4539
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
4540
+ return {
4541
+ content: [{ type: "text", text: `Pulled ${total} rows across ${tableList.length} table(s).` }]
4542
+ };
4543
+ });
4544
+ server.tool(`${serviceName}_cloud_feedback`, "Send feedback for this service", {
4545
+ message: exports_external.string().describe("Feedback message"),
4546
+ email: exports_external.string().optional().describe("Contact email")
4547
+ }, async ({ message, email }) => {
4548
+ const db = createDatabase({ service: "cloud" });
4549
+ const result = await sendFeedback({ service: serviceName, message, email }, db);
4550
+ db.close();
4551
+ return {
4552
+ content: [
4553
+ {
4554
+ type: "text",
4555
+ text: result.sent ? `Feedback sent (id: ${result.id})` : `Saved locally (id: ${result.id}): ${result.error}`
4556
+ }
4557
+ ]
4558
+ };
4559
+ });
4560
+ }
3499
4561
  function registerCloudCommands(program2, serviceName) {
3500
4562
  const cloudCmd = program2.command("cloud").description("Cloud sync and feedback commands");
3501
4563
  cloudCmd.command("status").description("Show cloud config and connection health").action(async () => {
@@ -3827,7 +4889,13 @@ CREATE TABLE IF NOT EXISTS feedback (
3827
4889
  email TEXT DEFAULT '',
3828
4890
  machine_id TEXT DEFAULT '',
3829
4891
  created_at TEXT DEFAULT (datetime('now'))
3830
- )`, AUTO_SYNC_CONFIG_PATH, CONFIG_DIR2;
4892
+ )`, SYNC_META_TABLE_SQL = `
4893
+ CREATE TABLE IF NOT EXISTS _sync_meta (
4894
+ table_name TEXT PRIMARY KEY,
4895
+ last_synced_at TEXT,
4896
+ last_synced_row_count INTEGER DEFAULT 0,
4897
+ direction TEXT DEFAULT 'push'
4898
+ )`, AUTO_SYNC_CONFIG_PATH, DEFAULT_AUTO_SYNC_CONFIG, cleanupHandlers, signalHandlersInstalled = false, SERVICE_NAME = "hasna-cloud-sync", CONFIG_DIR2;
3831
4899
  var init_dist = __esm(() => {
3832
4900
  __create2 = Object.create;
3833
4901
  __getProtoOf2 = Object.getPrototypeOf;
@@ -11895,6 +12963,11 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
11895
12963
  init_config();
11896
12964
  init_discover();
11897
12965
  AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
12966
+ DEFAULT_AUTO_SYNC_CONFIG = {
12967
+ auto_sync_on_start: true,
12968
+ auto_sync_on_stop: true
12969
+ };
12970
+ cleanupHandlers = [];
11898
12971
  init_config();
11899
12972
  init_adapter();
11900
12973
  init_dotfile();
@@ -11921,19 +12994,19 @@ __export(exports_database, {
11921
12994
  getConnectorsHome: () => getConnectorsHome,
11922
12995
  closeDatabase: () => closeDatabase
11923
12996
  });
11924
- import { join as join5 } from "path";
12997
+ import { join as join7 } from "path";
11925
12998
  import { homedir as homedir6 } from "os";
11926
- import { mkdirSync as mkdirSync3, existsSync as existsSync4, readdirSync as readdirSync3, copyFileSync as copyFileSync2, statSync } from "fs";
12999
+ import { mkdirSync as mkdirSync4, existsSync as existsSync7, readdirSync as readdirSync4, copyFileSync as copyFileSync2, statSync } from "fs";
11927
13000
  function getConnectorsHome() {
11928
13001
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir6();
11929
- const newDir = join5(home, ".hasna", "connectors");
11930
- const oldDir = join5(home, ".connectors");
11931
- if (existsSync4(oldDir) && !existsSync4(newDir)) {
11932
- mkdirSync3(newDir, { recursive: true });
13002
+ const newDir = join7(home, ".hasna", "connectors");
13003
+ const oldDir = join7(home, ".connectors");
13004
+ if (existsSync7(oldDir) && !existsSync7(newDir)) {
13005
+ mkdirSync4(newDir, { recursive: true });
11933
13006
  try {
11934
- for (const file of readdirSync3(oldDir)) {
11935
- const oldPath = join5(oldDir, file);
11936
- const newPath = join5(newDir, file);
13007
+ for (const file of readdirSync4(oldDir)) {
13008
+ const oldPath = join7(oldDir, file);
13009
+ const newPath = join7(newDir, file);
11937
13010
  try {
11938
13011
  if (statSync(oldPath).isFile()) {
11939
13012
  copyFileSync2(oldPath, newPath);
@@ -11942,14 +13015,14 @@ function getConnectorsHome() {
11942
13015
  }
11943
13016
  } catch {}
11944
13017
  }
11945
- mkdirSync3(newDir, { recursive: true });
13018
+ mkdirSync4(newDir, { recursive: true });
11946
13019
  return newDir;
11947
13020
  }
11948
13021
  function getDatabase(path) {
11949
13022
  if (_db)
11950
13023
  return _db;
11951
13024
  const dbPath = path ?? DB_PATH;
11952
- mkdirSync3(join5(dbPath, ".."), { recursive: true });
13025
+ mkdirSync4(join7(dbPath, ".."), { recursive: true });
11953
13026
  _db = new SqliteAdapter(dbPath);
11954
13027
  _db.run("PRAGMA journal_mode = WAL");
11955
13028
  migrate(_db);
@@ -12075,29 +13148,29 @@ var DB_DIR, DB_PATH, _db = null;
12075
13148
  var init_database = __esm(() => {
12076
13149
  init_dist();
12077
13150
  DB_DIR = getConnectorsHome();
12078
- DB_PATH = join5(DB_DIR, "connectors.db");
13151
+ DB_PATH = join7(DB_DIR, "connectors.db");
12079
13152
  });
12080
13153
 
12081
13154
  // src/lib/llm.ts
12082
- import { existsSync as existsSync6, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync5 } from "fs";
12083
- import { join as join7 } from "path";
13155
+ import { existsSync as existsSync9, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync6 } from "fs";
13156
+ import { join as join9 } from "path";
12084
13157
  function getLlmConfigPath() {
12085
- return join7(getConnectorsHome(), "llm.json");
13158
+ return join9(getConnectorsHome(), "llm.json");
12086
13159
  }
12087
13160
  function getLlmConfig() {
12088
13161
  const path = getLlmConfigPath();
12089
- if (!existsSync6(path))
13162
+ if (!existsSync9(path))
12090
13163
  return null;
12091
13164
  try {
12092
- return JSON.parse(readFileSync2(path, "utf-8"));
13165
+ return JSON.parse(readFileSync3(path, "utf-8"));
12093
13166
  } catch {
12094
13167
  return null;
12095
13168
  }
12096
13169
  }
12097
13170
  function saveLlmConfig(config) {
12098
13171
  const dir = getConnectorsHome();
12099
- mkdirSync5(dir, { recursive: true });
12100
- writeFileSync2(getLlmConfigPath(), JSON.stringify(config, null, 2));
13172
+ mkdirSync6(dir, { recursive: true });
13173
+ writeFileSync3(getLlmConfigPath(), JSON.stringify(config, null, 2));
12101
13174
  }
12102
13175
  function setLlmStrip(enabled) {
12103
13176
  const config = getLlmConfig();
@@ -12372,7 +13445,8 @@ var exports_scheduler = {};
12372
13445
  __export(exports_scheduler, {
12373
13446
  triggerJob: () => triggerJob,
12374
13447
  stopScheduler: () => stopScheduler,
12375
- startScheduler: () => startScheduler
13448
+ startScheduler: () => startScheduler,
13449
+ cronMatches: () => cronMatches
12376
13450
  });
12377
13451
  import { spawn } from "child_process";
12378
13452
  function cronMatches(cron, d) {
@@ -12610,8 +13684,8 @@ var init_synonyms = __esm(() => {
12610
13684
  });
12611
13685
 
12612
13686
  // src/lib/registry.ts
12613
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
12614
- import { join as join8, dirname as dirname2 } from "path";
13687
+ import { existsSync as existsSync10, readFileSync as readFileSync5 } from "fs";
13688
+ import { join as join10, dirname as dirname2 } from "path";
12615
13689
  import { fileURLToPath } from "url";
12616
13690
  function getConnectorsByCategory(category) {
12617
13691
  return CONNECTORS.filter((c) => c.category === category);
@@ -12772,17 +13846,17 @@ function loadConnectorVersions() {
12772
13846
  versionsLoaded = true;
12773
13847
  const thisDir = dirname2(fileURLToPath(import.meta.url));
12774
13848
  const candidates = [
12775
- join8(thisDir, "..", "connectors"),
12776
- join8(thisDir, "..", "..", "connectors")
13849
+ join10(thisDir, "..", "connectors"),
13850
+ join10(thisDir, "..", "..", "connectors")
12777
13851
  ];
12778
- const connectorsDir = candidates.find((d) => existsSync7(d));
13852
+ const connectorsDir = candidates.find((d) => existsSync10(d));
12779
13853
  if (!connectorsDir)
12780
13854
  return;
12781
13855
  for (const connector of CONNECTORS) {
12782
13856
  try {
12783
- const pkgPath = join8(connectorsDir, `connect-${connector.name}`, "package.json");
12784
- if (existsSync7(pkgPath)) {
12785
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
13857
+ const pkgPath = join10(connectorsDir, `connect-${connector.name}`, "package.json");
13858
+ if (existsSync10(pkgPath)) {
13859
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
12786
13860
  connector.version = pkg.version || "0.0.0";
12787
13861
  }
12788
13862
  } catch {}
@@ -20356,24 +21430,24 @@ var require_cli_spinners = __commonJS((exports, module) => {
20356
21430
  });
20357
21431
 
20358
21432
  // src/lib/installer.ts
20359
- import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3, readdirSync as readdirSync5, statSync as statSync2, rmSync } from "fs";
20360
- import { join as join9, dirname as dirname3 } from "path";
21433
+ import { existsSync as existsSync11, cpSync, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync6, statSync as statSync2, rmSync } from "fs";
21434
+ import { join as join11, dirname as dirname3 } from "path";
20361
21435
  import { fileURLToPath as fileURLToPath2 } from "url";
20362
21436
  function resolveConnectorsDir() {
20363
- const fromBin = join9(__dirname2, "..", "connectors");
20364
- if (existsSync8(fromBin))
21437
+ const fromBin = join11(__dirname2, "..", "connectors");
21438
+ if (existsSync11(fromBin))
20365
21439
  return fromBin;
20366
- const fromSrc = join9(__dirname2, "..", "..", "connectors");
20367
- if (existsSync8(fromSrc))
21440
+ const fromSrc = join11(__dirname2, "..", "..", "connectors");
21441
+ if (existsSync11(fromSrc))
20368
21442
  return fromSrc;
20369
21443
  return fromBin;
20370
21444
  }
20371
21445
  function getConnectorPath(name) {
20372
21446
  const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
20373
- return join9(CONNECTORS_DIR, connectorName);
21447
+ return join11(CONNECTORS_DIR, connectorName);
20374
21448
  }
20375
21449
  function connectorExists(name) {
20376
- return existsSync8(getConnectorPath(name));
21450
+ return existsSync11(getConnectorPath(name));
20377
21451
  }
20378
21452
  function installConnector(name, options = {}) {
20379
21453
  const { targetDir = process.cwd(), overwrite = false } = options;
@@ -20386,16 +21460,16 @@ function installConnector(name, options = {}) {
20386
21460
  }
20387
21461
  const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
20388
21462
  const sourcePath = getConnectorPath(name);
20389
- const destDir = join9(targetDir, ".connectors");
20390
- const destPath = join9(destDir, connectorName);
20391
- if (!existsSync8(sourcePath)) {
21463
+ const destDir = join11(targetDir, ".connectors");
21464
+ const destPath = join11(destDir, connectorName);
21465
+ if (!existsSync11(sourcePath)) {
20392
21466
  return {
20393
21467
  connector: name,
20394
21468
  success: false,
20395
21469
  error: `Connector '${name}' not found`
20396
21470
  };
20397
21471
  }
20398
- if (existsSync8(destPath) && !overwrite) {
21472
+ if (existsSync11(destPath) && !overwrite) {
20399
21473
  return {
20400
21474
  connector: name,
20401
21475
  success: false,
@@ -20404,22 +21478,22 @@ function installConnector(name, options = {}) {
20404
21478
  };
20405
21479
  }
20406
21480
  try {
20407
- if (!existsSync8(destDir)) {
20408
- mkdirSync6(destDir, { recursive: true });
21481
+ if (!existsSync11(destDir)) {
21482
+ mkdirSync7(destDir, { recursive: true });
20409
21483
  }
20410
21484
  cpSync(sourcePath, destPath, { recursive: true });
20411
- const homeCredDir = join9(getConnectorsHome(), connectorName);
20412
- if (existsSync8(homeCredDir)) {
21485
+ const homeCredDir = join11(getConnectorsHome(), connectorName);
21486
+ if (existsSync11(homeCredDir)) {
20413
21487
  const filesToCopy = ["credentials.json", "current_profile"];
20414
21488
  for (const file of filesToCopy) {
20415
- const src = join9(homeCredDir, file);
20416
- if (existsSync8(src)) {
20417
- cpSync(src, join9(destPath, file));
21489
+ const src = join11(homeCredDir, file);
21490
+ if (existsSync11(src)) {
21491
+ cpSync(src, join11(destPath, file));
20418
21492
  }
20419
21493
  }
20420
- const profilesDir = join9(homeCredDir, "profiles");
20421
- if (existsSync8(profilesDir)) {
20422
- cpSync(profilesDir, join9(destPath, "profiles"), { recursive: true });
21494
+ const profilesDir = join11(homeCredDir, "profiles");
21495
+ if (existsSync11(profilesDir)) {
21496
+ cpSync(profilesDir, join11(destPath, "profiles"), { recursive: true });
20423
21497
  }
20424
21498
  }
20425
21499
  updateConnectorsIndex(destDir);
@@ -20437,8 +21511,8 @@ function installConnector(name, options = {}) {
20437
21511
  }
20438
21512
  }
20439
21513
  function updateConnectorsIndex(connectorsDir) {
20440
- const indexPath = join9(connectorsDir, "index.ts");
20441
- const connectors = readdirSync5(connectorsDir).filter((f) => f.startsWith("connect-") && !f.includes("."));
21514
+ const indexPath = join11(connectorsDir, "index.ts");
21515
+ const connectors = readdirSync6(connectorsDir).filter((f) => f.startsWith("connect-") && !f.includes("."));
20442
21516
  const exports = connectors.map((c) => {
20443
21517
  const name = c.replace("connect-", "");
20444
21518
  return `export * as ${name} from './${c}/src/index.js';`;
@@ -20451,24 +21525,24 @@ function updateConnectorsIndex(connectorsDir) {
20451
21525
 
20452
21526
  ${exports}
20453
21527
  `;
20454
- writeFileSync3(indexPath, content);
21528
+ writeFileSync5(indexPath, content);
20455
21529
  }
20456
21530
  function getInstalledConnectors(targetDir = process.cwd()) {
20457
- const connectorsDir = join9(targetDir, ".connectors");
20458
- if (!existsSync8(connectorsDir)) {
21531
+ const connectorsDir = join11(targetDir, ".connectors");
21532
+ if (!existsSync11(connectorsDir)) {
20459
21533
  return [];
20460
21534
  }
20461
- return readdirSync5(connectorsDir).filter((f) => {
20462
- const fullPath = join9(connectorsDir, f);
21535
+ return readdirSync6(connectorsDir).filter((f) => {
21536
+ const fullPath = join11(connectorsDir, f);
20463
21537
  return f.startsWith("connect-") && statSync2(fullPath).isDirectory();
20464
21538
  }).map((f) => f.replace("connect-", ""));
20465
21539
  }
20466
21540
  function getConnectorDocs(name) {
20467
21541
  const connectorPath = getConnectorPath(name);
20468
- const claudeMdPath = join9(connectorPath, "CLAUDE.md");
20469
- if (!existsSync8(claudeMdPath))
21542
+ const claudeMdPath = join11(connectorPath, "CLAUDE.md");
21543
+ if (!existsSync11(claudeMdPath))
20470
21544
  return null;
20471
- const raw = readFileSync4(claudeMdPath, "utf-8");
21545
+ const raw = readFileSync6(claudeMdPath, "utf-8");
20472
21546
  return {
20473
21547
  overview: extractSection(raw, "Project Overview"),
20474
21548
  auth: extractSection(raw, "Authentication"),
@@ -20507,9 +21581,9 @@ function parseEnvVarsTable(section) {
20507
21581
  }
20508
21582
  function removeConnector(name, targetDir = process.cwd()) {
20509
21583
  const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
20510
- const connectorsDir = join9(targetDir, ".connectors");
20511
- const connectorPath = join9(connectorsDir, connectorName);
20512
- if (!existsSync8(connectorPath)) {
21584
+ const connectorsDir = join11(targetDir, ".connectors");
21585
+ const connectorPath = join11(connectorsDir, connectorName);
21586
+ if (!existsSync11(connectorPath)) {
20513
21587
  return false;
20514
21588
  }
20515
21589
  rmSync(connectorPath, { recursive: true });
@@ -20524,13 +21598,13 @@ var init_installer = __esm(() => {
20524
21598
  });
20525
21599
 
20526
21600
  // src/lib/lock.ts
20527
- import { openSync, closeSync, unlinkSync, existsSync as existsSync9, statSync as statSync3 } from "fs";
20528
- import { join as join10 } from "path";
20529
- import { mkdirSync as mkdirSync7 } from "fs";
21601
+ import { openSync, closeSync, unlinkSync as unlinkSync2, existsSync as existsSync12, statSync as statSync3 } from "fs";
21602
+ import { join as join12 } from "path";
21603
+ import { mkdirSync as mkdirSync8 } from "fs";
20530
21604
  function lockPath(connector) {
20531
- const dir = join10(getConnectorsHome(), `connect-${connector}`);
20532
- mkdirSync7(dir, { recursive: true });
20533
- return join10(dir, ".write.lock");
21605
+ const dir = join12(getConnectorsHome(), `connect-${connector}`);
21606
+ mkdirSync8(dir, { recursive: true });
21607
+ return join12(dir, ".write.lock");
20534
21608
  }
20535
21609
  function isStale(path) {
20536
21610
  try {
@@ -20541,9 +21615,9 @@ function isStale(path) {
20541
21615
  }
20542
21616
  }
20543
21617
  function tryAcquire(path) {
20544
- if (existsSync9(path) && isStale(path)) {
21618
+ if (existsSync12(path) && isStale(path)) {
20545
21619
  try {
20546
- unlinkSync(path);
21620
+ unlinkSync2(path);
20547
21621
  } catch {}
20548
21622
  }
20549
21623
  try {
@@ -20558,7 +21632,7 @@ function tryAcquire(path) {
20558
21632
  }
20559
21633
  function release(path) {
20560
21634
  try {
20561
- unlinkSync(path);
21635
+ unlinkSync2(path);
20562
21636
  } catch {}
20563
21637
  }
20564
21638
  async function withWriteLock(connector, fn) {
@@ -20590,9 +21664,9 @@ var init_lock = __esm(() => {
20590
21664
  });
20591
21665
 
20592
21666
  // src/server/auth.ts
20593
- import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync8, readdirSync as readdirSync6, rmSync as rmSync2, statSync as statSync4 } from "fs";
21667
+ import { existsSync as existsSync13, readFileSync as readFileSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync9, readdirSync as readdirSync7, rmSync as rmSync2, statSync as statSync4 } from "fs";
20594
21668
  import { randomBytes } from "crypto";
20595
- import { join as join11 } from "path";
21669
+ import { join as join13 } from "path";
20596
21670
  function getAuthType(name) {
20597
21671
  const docs = getConnectorDocs(name);
20598
21672
  if (!docs?.auth)
@@ -20606,14 +21680,14 @@ function getAuthType(name) {
20606
21680
  }
20607
21681
  function getConnectorConfigDir(name) {
20608
21682
  const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
20609
- return join11(getConnectorsHome(), connectorName);
21683
+ return join13(getConnectorsHome(), connectorName);
20610
21684
  }
20611
21685
  function getCurrentProfile(name) {
20612
21686
  const configDir = getConnectorConfigDir(name);
20613
- const currentProfileFile = join11(configDir, "current_profile");
20614
- if (existsSync10(currentProfileFile)) {
21687
+ const currentProfileFile = join13(configDir, "current_profile");
21688
+ if (existsSync13(currentProfileFile)) {
20615
21689
  try {
20616
- return readFileSync5(currentProfileFile, "utf-8").trim() || "default";
21690
+ return readFileSync7(currentProfileFile, "utf-8").trim() || "default";
20617
21691
  } catch {
20618
21692
  return "default";
20619
21693
  }
@@ -20625,16 +21699,16 @@ function loadProfileConfig(name) {
20625
21699
  const profile = getCurrentProfile(name);
20626
21700
  let flatConfig = {};
20627
21701
  let dirConfig = {};
20628
- const profileFile = join11(configDir, "profiles", `${profile}.json`);
20629
- if (existsSync10(profileFile)) {
21702
+ const profileFile = join13(configDir, "profiles", `${profile}.json`);
21703
+ if (existsSync13(profileFile)) {
20630
21704
  try {
20631
- flatConfig = JSON.parse(readFileSync5(profileFile, "utf-8"));
21705
+ flatConfig = JSON.parse(readFileSync7(profileFile, "utf-8"));
20632
21706
  } catch {}
20633
21707
  }
20634
- const profileDirConfig = join11(configDir, "profiles", profile, "config.json");
20635
- if (existsSync10(profileDirConfig)) {
21708
+ const profileDirConfig = join13(configDir, "profiles", profile, "config.json");
21709
+ if (existsSync13(profileDirConfig)) {
20636
21710
  try {
20637
- dirConfig = JSON.parse(readFileSync5(profileDirConfig, "utf-8"));
21711
+ dirConfig = JSON.parse(readFileSync7(profileDirConfig, "utf-8"));
20638
21712
  } catch {}
20639
21713
  }
20640
21714
  if (Object.keys(flatConfig).length === 0 && Object.keys(dirConfig).length === 0) {
@@ -20645,10 +21719,10 @@ function loadProfileConfig(name) {
20645
21719
  function loadTokens(name) {
20646
21720
  const configDir = getConnectorConfigDir(name);
20647
21721
  const profile = getCurrentProfile(name);
20648
- const tokensFile = join11(configDir, "profiles", profile, "tokens.json");
20649
- if (existsSync10(tokensFile)) {
21722
+ const tokensFile = join13(configDir, "profiles", profile, "tokens.json");
21723
+ if (existsSync13(tokensFile)) {
20650
21724
  try {
20651
- return JSON.parse(readFileSync5(tokensFile, "utf-8"));
21725
+ return JSON.parse(readFileSync7(tokensFile, "utf-8"));
20652
21726
  } catch {
20653
21727
  return null;
20654
21728
  }
@@ -20715,43 +21789,43 @@ function _saveApiKey(name, key, field) {
20715
21789
  const profile = getCurrentProfile(name);
20716
21790
  const keyField = field || guessKeyField(name);
20717
21791
  if (keyField === "clientId" || keyField === "clientSecret") {
20718
- const credentialsFile = join11(configDir, "credentials.json");
20719
- mkdirSync8(configDir, { recursive: true });
21792
+ const credentialsFile = join13(configDir, "credentials.json");
21793
+ mkdirSync9(configDir, { recursive: true });
20720
21794
  let creds = {};
20721
- if (existsSync10(credentialsFile)) {
21795
+ if (existsSync13(credentialsFile)) {
20722
21796
  try {
20723
- creds = JSON.parse(readFileSync5(credentialsFile, "utf-8"));
21797
+ creds = JSON.parse(readFileSync7(credentialsFile, "utf-8"));
20724
21798
  } catch {}
20725
21799
  }
20726
21800
  creds[keyField] = key;
20727
- writeFileSync4(credentialsFile, JSON.stringify(creds, null, 2));
21801
+ writeFileSync6(credentialsFile, JSON.stringify(creds, null, 2));
20728
21802
  return;
20729
21803
  }
20730
- const profileFile = join11(configDir, "profiles", `${profile}.json`);
20731
- const profileDir = join11(configDir, "profiles", profile);
20732
- if (existsSync10(profileFile)) {
21804
+ const profileFile = join13(configDir, "profiles", `${profile}.json`);
21805
+ const profileDir = join13(configDir, "profiles", profile);
21806
+ if (existsSync13(profileFile)) {
20733
21807
  let config = {};
20734
21808
  try {
20735
- config = JSON.parse(readFileSync5(profileFile, "utf-8"));
21809
+ config = JSON.parse(readFileSync7(profileFile, "utf-8"));
20736
21810
  } catch {}
20737
21811
  config[keyField] = key;
20738
- writeFileSync4(profileFile, JSON.stringify(config, null, 2));
21812
+ writeFileSync6(profileFile, JSON.stringify(config, null, 2));
20739
21813
  return;
20740
21814
  }
20741
- if (existsSync10(profileDir)) {
20742
- const configFile = join11(profileDir, "config.json");
21815
+ if (existsSync13(profileDir)) {
21816
+ const configFile = join13(profileDir, "config.json");
20743
21817
  let config = {};
20744
- if (existsSync10(configFile)) {
21818
+ if (existsSync13(configFile)) {
20745
21819
  try {
20746
- config = JSON.parse(readFileSync5(configFile, "utf-8"));
21820
+ config = JSON.parse(readFileSync7(configFile, "utf-8"));
20747
21821
  } catch {}
20748
21822
  }
20749
21823
  config[keyField] = key;
20750
- writeFileSync4(configFile, JSON.stringify(config, null, 2));
21824
+ writeFileSync6(configFile, JSON.stringify(config, null, 2));
20751
21825
  return;
20752
21826
  }
20753
- mkdirSync8(profileDir, { recursive: true });
20754
- writeFileSync4(join11(profileDir, "config.json"), JSON.stringify({ [keyField]: key }, null, 2));
21827
+ mkdirSync9(profileDir, { recursive: true });
21828
+ writeFileSync6(join13(profileDir, "config.json"), JSON.stringify({ [keyField]: key }, null, 2));
20755
21829
  }
20756
21830
  function guessKeyField(name) {
20757
21831
  const docs = getConnectorDocs(name);
@@ -20769,10 +21843,10 @@ function guessKeyField(name) {
20769
21843
  }
20770
21844
  function getOAuthConfig(name) {
20771
21845
  const configDir = getConnectorConfigDir(name);
20772
- const credentialsFile = join11(configDir, "credentials.json");
20773
- if (existsSync10(credentialsFile)) {
21846
+ const credentialsFile = join13(configDir, "credentials.json");
21847
+ if (existsSync13(credentialsFile)) {
20774
21848
  try {
20775
- const creds = JSON.parse(readFileSync5(credentialsFile, "utf-8"));
21849
+ const creds = JSON.parse(readFileSync7(credentialsFile, "utf-8"));
20776
21850
  return { clientId: creds.clientId, clientSecret: creds.clientSecret };
20777
21851
  } catch {}
20778
21852
  }
@@ -20851,10 +21925,10 @@ async function exchangeOAuthCode(name, code, redirectUri) {
20851
21925
  function saveOAuthTokens(name, tokens) {
20852
21926
  const configDir = getConnectorConfigDir(name);
20853
21927
  const profile = getCurrentProfile(name);
20854
- const profileDir = join11(configDir, "profiles", profile);
20855
- mkdirSync8(profileDir, { recursive: true });
20856
- const tokensFile = join11(profileDir, "tokens.json");
20857
- writeFileSync4(tokensFile, JSON.stringify(tokens, null, 2), { mode: 384 });
21928
+ const profileDir = join13(configDir, "profiles", profile);
21929
+ mkdirSync9(profileDir, { recursive: true });
21930
+ const tokensFile = join13(profileDir, "tokens.json");
21931
+ writeFileSync6(tokensFile, JSON.stringify(tokens, null, 2), { mode: 384 });
20858
21932
  }
20859
21933
  async function refreshOAuthToken(name) {
20860
21934
  return withWriteLock(name, () => _refreshOAuthToken(name));
@@ -20896,14 +21970,14 @@ async function _refreshOAuthToken(name) {
20896
21970
  }
20897
21971
  function listProfiles(name) {
20898
21972
  const configDir = getConnectorConfigDir(name);
20899
- const profilesDir = join11(configDir, "profiles");
20900
- if (!existsSync10(profilesDir))
21973
+ const profilesDir = join13(configDir, "profiles");
21974
+ if (!existsSync13(profilesDir))
20901
21975
  return ["default"];
20902
21976
  const seen = new Set;
20903
21977
  try {
20904
- const entries = readdirSync6(profilesDir);
21978
+ const entries = readdirSync7(profilesDir);
20905
21979
  for (const entry of entries) {
20906
- const fullPath = join11(profilesDir, entry);
21980
+ const fullPath = join13(profilesDir, entry);
20907
21981
  const stat = statSync4(fullPath);
20908
21982
  if (stat.isDirectory()) {
20909
21983
  seen.add(entry);
@@ -20917,24 +21991,24 @@ function listProfiles(name) {
20917
21991
  }
20918
21992
  function switchProfile(name, profile) {
20919
21993
  const configDir = getConnectorConfigDir(name);
20920
- mkdirSync8(configDir, { recursive: true });
20921
- writeFileSync4(join11(configDir, "current_profile"), profile);
21994
+ mkdirSync9(configDir, { recursive: true });
21995
+ writeFileSync6(join13(configDir, "current_profile"), profile);
20922
21996
  }
20923
21997
  function deleteProfile(name, profile) {
20924
21998
  if (profile === "default")
20925
21999
  return false;
20926
22000
  const configDir = getConnectorConfigDir(name);
20927
- const profilesDir = join11(configDir, "profiles");
20928
- const profileFile = join11(profilesDir, `${profile}.json`);
20929
- if (existsSync10(profileFile)) {
22001
+ const profilesDir = join13(configDir, "profiles");
22002
+ const profileFile = join13(profilesDir, `${profile}.json`);
22003
+ if (existsSync13(profileFile)) {
20930
22004
  rmSync2(profileFile);
20931
22005
  if (getCurrentProfile(name) === profile) {
20932
22006
  switchProfile(name, "default");
20933
22007
  }
20934
22008
  return true;
20935
22009
  }
20936
- const profileDir = join11(profilesDir, profile);
20937
- if (existsSync10(profileDir)) {
22010
+ const profileDir = join13(profilesDir, profile);
22011
+ if (existsSync13(profileDir)) {
20938
22012
  rmSync2(profileDir, { recursive: true });
20939
22013
  if (getCurrentProfile(name) === profile) {
20940
22014
  switchProfile(name, "default");
@@ -21201,8 +22275,8 @@ var exports_serve = {};
21201
22275
  __export(exports_serve, {
21202
22276
  startServer: () => startServer
21203
22277
  });
21204
- import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync9 } from "fs";
21205
- import { join as join13, dirname as dirname5, extname, basename } from "path";
22278
+ import { existsSync as existsSync15, readdirSync as readdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync10 } from "fs";
22279
+ import { join as join15, dirname as dirname5, extname, basename } from "path";
21206
22280
  import { fileURLToPath as fileURLToPath4 } from "url";
21207
22281
  function logActivity(action, connector, detail) {
21208
22282
  activityLog.unshift({ action, connector, timestamp: Date.now(), detail });
@@ -21214,20 +22288,20 @@ function resolveDashboardDir() {
21214
22288
  const candidates = [];
21215
22289
  try {
21216
22290
  const scriptDir = dirname5(fileURLToPath4(import.meta.url));
21217
- candidates.push(join13(scriptDir, "..", "dashboard", "dist"));
21218
- candidates.push(join13(scriptDir, "..", "..", "dashboard", "dist"));
22291
+ candidates.push(join15(scriptDir, "..", "dashboard", "dist"));
22292
+ candidates.push(join15(scriptDir, "..", "..", "dashboard", "dist"));
21219
22293
  } catch {}
21220
22294
  if (process.argv[1]) {
21221
22295
  const mainDir = dirname5(process.argv[1]);
21222
- candidates.push(join13(mainDir, "..", "dashboard", "dist"));
21223
- candidates.push(join13(mainDir, "..", "..", "dashboard", "dist"));
22296
+ candidates.push(join15(mainDir, "..", "dashboard", "dist"));
22297
+ candidates.push(join15(mainDir, "..", "..", "dashboard", "dist"));
21224
22298
  }
21225
- candidates.push(join13(process.cwd(), "dashboard", "dist"));
22299
+ candidates.push(join15(process.cwd(), "dashboard", "dist"));
21226
22300
  for (const candidate of candidates) {
21227
- if (existsSync12(candidate))
22301
+ if (existsSync15(candidate))
21228
22302
  return candidate;
21229
22303
  }
21230
- return join13(process.cwd(), "dashboard", "dist");
22304
+ return join15(process.cwd(), "dashboard", "dist");
21231
22305
  }
21232
22306
  function json(data, status = 200, port) {
21233
22307
  return new Response(JSON.stringify(data), {
@@ -21312,7 +22386,7 @@ function oauthPage(type, title, message, hint, extra) {
21312
22386
  </body></html>`;
21313
22387
  }
21314
22388
  function serveStaticFile(filePath) {
21315
- if (!existsSync12(filePath))
22389
+ if (!existsSync15(filePath))
21316
22390
  return null;
21317
22391
  const ext = extname(filePath);
21318
22392
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -21336,7 +22410,7 @@ async function startServer(requestedPort, options) {
21336
22410
  const shouldOpen = options?.open ?? true;
21337
22411
  loadConnectorVersions();
21338
22412
  const dashboardDir = resolveDashboardDir();
21339
- const dashboardExists = existsSync12(dashboardDir);
22413
+ const dashboardExists = existsSync15(dashboardDir);
21340
22414
  if (!dashboardExists) {
21341
22415
  console.error(`
21342
22416
  Dashboard not found at: ${dashboardDir}`);
@@ -21653,12 +22727,12 @@ Dashboard not found at: ${dashboardDir}`);
21653
22727
  return json({ error: "Invalid connector name" }, 400, port);
21654
22728
  try {
21655
22729
  const profiles = listProfiles(name);
21656
- const configDir = join13(getConnectorsHome(), name.startsWith("connect-") ? name : `connect-${name}`);
21657
- const currentProfileFile = join13(configDir, "current_profile");
22730
+ const configDir = join15(getConnectorsHome(), name.startsWith("connect-") ? name : `connect-${name}`);
22731
+ const currentProfileFile = join15(configDir, "current_profile");
21658
22732
  let current = "default";
21659
- if (existsSync12(currentProfileFile)) {
22733
+ if (existsSync15(currentProfileFile)) {
21660
22734
  try {
21661
- current = readFileSync6(currentProfileFile, "utf-8").trim() || "default";
22735
+ current = readFileSync8(currentProfileFile, "utf-8").trim() || "default";
21662
22736
  } catch {}
21663
22737
  }
21664
22738
  return json({ current, profiles }, 200, port);
@@ -21707,30 +22781,30 @@ Dashboard not found at: ${dashboardDir}`);
21707
22781
  try {
21708
22782
  const connectDir = getConnectorsHome();
21709
22783
  const result = {};
21710
- if (existsSync12(connectDir)) {
21711
- const entries = readdirSync7(connectDir, { withFileTypes: true });
22784
+ if (existsSync15(connectDir)) {
22785
+ const entries = readdirSync8(connectDir, { withFileTypes: true });
21712
22786
  for (const entry of entries) {
21713
22787
  if (!entry.isDirectory() || !entry.name.startsWith("connect-"))
21714
22788
  continue;
21715
22789
  const connectorName = entry.name.replace(/^connect-/, "");
21716
- const profilesDir = join13(connectDir, entry.name, "profiles");
21717
- if (!existsSync12(profilesDir))
22790
+ const profilesDir = join15(connectDir, entry.name, "profiles");
22791
+ if (!existsSync15(profilesDir))
21718
22792
  continue;
21719
22793
  const profiles = {};
21720
- const profileEntries = readdirSync7(profilesDir, { withFileTypes: true });
22794
+ const profileEntries = readdirSync8(profilesDir, { withFileTypes: true });
21721
22795
  for (const pEntry of profileEntries) {
21722
22796
  if (pEntry.isFile() && pEntry.name.endsWith(".json")) {
21723
22797
  const profileName = basename(pEntry.name, ".json");
21724
22798
  try {
21725
- const config = JSON.parse(readFileSync6(join13(profilesDir, pEntry.name), "utf-8"));
22799
+ const config = JSON.parse(readFileSync8(join15(profilesDir, pEntry.name), "utf-8"));
21726
22800
  profiles[profileName] = config;
21727
22801
  } catch {}
21728
22802
  }
21729
22803
  if (pEntry.isDirectory()) {
21730
- const configPath = join13(profilesDir, pEntry.name, "config.json");
21731
- if (existsSync12(configPath)) {
22804
+ const configPath = join15(profilesDir, pEntry.name, "config.json");
22805
+ if (existsSync15(configPath)) {
21732
22806
  try {
21733
- const config = JSON.parse(readFileSync6(configPath, "utf-8"));
22807
+ const config = JSON.parse(readFileSync8(configPath, "utf-8"));
21734
22808
  profiles[pEntry.name] = config;
21735
22809
  } catch {}
21736
22810
  }
@@ -21771,14 +22845,14 @@ Dashboard not found at: ${dashboardDir}`);
21771
22845
  continue;
21772
22846
  if (!data.profiles || typeof data.profiles !== "object")
21773
22847
  continue;
21774
- const connectorDir = join13(connectDir, `connect-${connectorName}`);
21775
- const profilesDir = join13(connectorDir, "profiles");
22848
+ const connectorDir = join15(connectDir, `connect-${connectorName}`);
22849
+ const profilesDir = join15(connectorDir, "profiles");
21776
22850
  for (const [profileName, config] of Object.entries(data.profiles)) {
21777
22851
  if (!config || typeof config !== "object")
21778
22852
  continue;
21779
- mkdirSync9(profilesDir, { recursive: true });
21780
- const profileFile = join13(profilesDir, `${profileName}.json`);
21781
- writeFileSync5(profileFile, JSON.stringify(config, null, 2));
22853
+ mkdirSync10(profilesDir, { recursive: true });
22854
+ const profileFile = join15(profilesDir, `${profileName}.json`);
22855
+ writeFileSync7(profileFile, JSON.stringify(config, null, 2));
21782
22856
  imported++;
21783
22857
  }
21784
22858
  }
@@ -21833,12 +22907,12 @@ Dashboard not found at: ${dashboardDir}`);
21833
22907
  }
21834
22908
  if (dashboardExists && (method === "GET" || method === "HEAD")) {
21835
22909
  if (path !== "/") {
21836
- const filePath = join13(dashboardDir, path);
22910
+ const filePath = join15(dashboardDir, path);
21837
22911
  const res2 = serveStaticFile(filePath);
21838
22912
  if (res2)
21839
22913
  return res2;
21840
22914
  }
21841
- const indexPath = join13(dashboardDir, "index.html");
22915
+ const indexPath = join15(dashboardDir, "index.html");
21842
22916
  const res = serveStaticFile(indexPath);
21843
22917
  if (res)
21844
22918
  return res;
@@ -21923,7 +22997,7 @@ import chalk2 from "chalk";
21923
22997
  // package.json
21924
22998
  var package_default = {
21925
22999
  name: "@hasna/connectors",
21926
- version: "1.3.11",
23000
+ version: "1.3.12",
21927
23001
  description: "Open source connector library - Install API connectors with a single command",
21928
23002
  type: "module",
21929
23003
  bin: {
@@ -23490,9 +24564,9 @@ init_registry();
23490
24564
  init_installer();
23491
24565
  init_auth();
23492
24566
  init_database();
23493
- import { readdirSync as readdirSync8, existsSync as existsSync13, statSync as statSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync10 } from "fs";
24567
+ import { readdirSync as readdirSync9, existsSync as existsSync16, statSync as statSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync8, mkdirSync as mkdirSync11 } from "fs";
23494
24568
  import { homedir as homedir8 } from "os";
23495
- import { join as join14, relative as relative2 } from "path";
24569
+ import { join as join16, relative as relative2 } from "path";
23496
24570
 
23497
24571
  // src/lib/test-endpoints.ts
23498
24572
  var TEST_ENDPOINTS = {
@@ -23693,17 +24767,17 @@ var TEST_ENDPOINTS = {
23693
24767
  import { createInterface } from "readline";
23694
24768
 
23695
24769
  // src/lib/runner.ts
23696
- import { existsSync as existsSync11 } from "fs";
23697
- import { join as join12, dirname as dirname4 } from "path";
24770
+ import { existsSync as existsSync14 } from "fs";
24771
+ import { join as join14, dirname as dirname4 } from "path";
23698
24772
  import { fileURLToPath as fileURLToPath3 } from "url";
23699
24773
  import { spawn as spawn3 } from "child_process";
23700
24774
  var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
23701
24775
  function resolveConnectorsDir2() {
23702
- const fromBin = join12(__dirname3, "..", "connectors");
23703
- if (existsSync11(fromBin))
24776
+ const fromBin = join14(__dirname3, "..", "connectors");
24777
+ if (existsSync14(fromBin))
23704
24778
  return fromBin;
23705
- const fromSrc = join12(__dirname3, "..", "..", "connectors");
23706
- if (existsSync11(fromSrc))
24779
+ const fromSrc = join14(__dirname3, "..", "..", "connectors");
24780
+ if (existsSync14(fromSrc))
23707
24781
  return fromSrc;
23708
24782
  return fromBin;
23709
24783
  }
@@ -23743,9 +24817,9 @@ function buildEnvWithCredentials(connectorName, baseEnv) {
23743
24817
  }
23744
24818
  function getConnectorCliPath(name) {
23745
24819
  const safeName = name.replace(/[^a-z0-9-]/g, "");
23746
- const connectorDir = join12(CONNECTORS_DIR2, `connect-${safeName}`);
23747
- const cliPath = join12(connectorDir, "src", "cli", "index.ts");
23748
- if (existsSync11(cliPath))
24820
+ const connectorDir = join14(CONNECTORS_DIR2, `connect-${safeName}`);
24821
+ const cliPath = join14(connectorDir, "src", "cli", "index.ts");
24822
+ if (existsSync14(cliPath))
23749
24823
  return cliPath;
23750
24824
  return null;
23751
24825
  }
@@ -23858,8 +24932,8 @@ Run 'connectors --help' for full usage.`);
23858
24932
  });
23859
24933
  function listFilesRecursive(dir, base = dir) {
23860
24934
  const files = [];
23861
- for (const entry of readdirSync8(dir)) {
23862
- const fullPath = join14(dir, entry);
24935
+ for (const entry of readdirSync9(dir)) {
24936
+ const fullPath = join16(dir, entry);
23863
24937
  if (statSync5(fullPath).isDirectory()) {
23864
24938
  files.push(...listFilesRecursive(fullPath, base));
23865
24939
  } else {
@@ -23908,7 +24982,7 @@ program2.command("install").alias("add").argument("[connectors...]", "Connectors
23908
24982
  }
23909
24983
  if (options.dryRun) {
23910
24984
  const installed = getInstalledConnectors();
23911
- const destDir = join14(process.cwd(), ".connectors");
24985
+ const destDir = join16(process.cwd(), ".connectors");
23912
24986
  const actions = [];
23913
24987
  for (const name of connectors) {
23914
24988
  if (!/^[a-z0-9-]+$/.test(name)) {
@@ -23926,7 +25000,7 @@ program2.command("install").alias("add").argument("[connectors...]", "Connectors
23926
25000
  }
23927
25001
  const connectorDirName = name.startsWith("connect-") ? name : `connect-${name}`;
23928
25002
  const sourcePath = getConnectorPath(name);
23929
- const destPath = join14(destDir, connectorDirName);
25003
+ const destPath = join16(destDir, connectorDirName);
23930
25004
  const alreadyInstalled = installed.includes(name);
23931
25005
  const files = listFilesRecursive(sourcePath);
23932
25006
  const importLine = `export * as ${name} from './${connectorDirName}/src/index.js';`;
@@ -24446,11 +25520,11 @@ program2.command("status").option("--json", "Output as JSON", false).description
24446
25520
  function buildStatusEntry(name, source) {
24447
25521
  const auth = getAuthStatus(name);
24448
25522
  const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
24449
- const currentProfileFile = join14(configDir, connectorName, "current_profile");
25523
+ const currentProfileFile = join16(configDir, connectorName, "current_profile");
24450
25524
  let profile = "default";
24451
- if (existsSync13(currentProfileFile)) {
25525
+ if (existsSync16(currentProfileFile)) {
24452
25526
  try {
24453
- profile = readFileSync7(currentProfileFile, "utf-8").trim() || "default";
25527
+ profile = readFileSync9(currentProfileFile, "utf-8").trim() || "default";
24454
25528
  } catch {}
24455
25529
  }
24456
25530
  let expiryLabel = null;
@@ -24486,13 +25560,13 @@ program2.command("status").option("--json", "Output as JSON", false).description
24486
25560
  seen.add(name);
24487
25561
  allStatuses.push(buildStatusEntry(name, "project"));
24488
25562
  }
24489
- if (existsSync13(configDir)) {
25563
+ if (existsSync16(configDir)) {
24490
25564
  try {
24491
- const globalDirs = readdirSync8(configDir).filter((f) => {
25565
+ const globalDirs = readdirSync9(configDir).filter((f) => {
24492
25566
  if (!f.startsWith("connect-"))
24493
25567
  return false;
24494
25568
  try {
24495
- return statSync5(join14(configDir, f)).isDirectory();
25569
+ return statSync5(join16(configDir, f)).isDirectory();
24496
25570
  } catch {
24497
25571
  return false;
24498
25572
  }
@@ -24861,11 +25935,11 @@ program2.command("init").option("--json", "Output presets and suggestions as JSO
24861
25935
  let configuredCount = 0;
24862
25936
  const configuredNames = [];
24863
25937
  try {
24864
- if (existsSync13(connectorsHome)) {
24865
- const entries = readdirSync8(connectorsHome).filter((e) => e.startsWith("connect-") && statSync5(join14(connectorsHome, e)).isDirectory());
25938
+ if (existsSync16(connectorsHome)) {
25939
+ const entries = readdirSync9(connectorsHome).filter((e) => e.startsWith("connect-") && statSync5(join16(connectorsHome, e)).isDirectory());
24866
25940
  for (const entry of entries) {
24867
- const profilesDir = join14(connectorsHome, entry, "profiles");
24868
- if (existsSync13(profilesDir)) {
25941
+ const profilesDir = join16(connectorsHome, entry, "profiles");
25942
+ if (existsSync16(profilesDir)) {
24869
25943
  configuredCount++;
24870
25944
  configuredNames.push(entry.replace(/^connect-/, ""));
24871
25945
  }
@@ -24965,42 +26039,42 @@ function redactSecrets(obj) {
24965
26039
  program2.command("export").option("-o, --output <file>", "Write to file instead of stdout").option("--include-secrets", "Include secrets in plaintext (dangerous \u2014 use only for backup/restore)").description("Export all connector credentials as JSON backup").action((options) => {
24966
26040
  const connectDir = getConnectorsHome();
24967
26041
  const result = {};
24968
- if (existsSync13(connectDir)) {
24969
- for (const entry of readdirSync8(connectDir)) {
24970
- const entryPath = join14(connectDir, entry);
26042
+ if (existsSync16(connectDir)) {
26043
+ for (const entry of readdirSync9(connectDir)) {
26044
+ const entryPath = join16(connectDir, entry);
24971
26045
  if (!statSync5(entryPath).isDirectory() || !entry.startsWith("connect-"))
24972
26046
  continue;
24973
26047
  const connectorName = entry.replace(/^connect-/, "");
24974
26048
  let credentials = undefined;
24975
- const credentialsPath = join14(entryPath, "credentials.json");
24976
- if (existsSync13(credentialsPath)) {
26049
+ const credentialsPath = join16(entryPath, "credentials.json");
26050
+ if (existsSync16(credentialsPath)) {
24977
26051
  try {
24978
- credentials = JSON.parse(readFileSync7(credentialsPath, "utf-8"));
26052
+ credentials = JSON.parse(readFileSync9(credentialsPath, "utf-8"));
24979
26053
  } catch {}
24980
26054
  }
24981
- const profilesDir = join14(entryPath, "profiles");
24982
- if (!existsSync13(profilesDir) && !credentials)
26055
+ const profilesDir = join16(entryPath, "profiles");
26056
+ if (!existsSync16(profilesDir) && !credentials)
24983
26057
  continue;
24984
26058
  const profiles = {};
24985
- if (existsSync13(profilesDir)) {
24986
- for (const pEntry of readdirSync8(profilesDir)) {
24987
- const pPath = join14(profilesDir, pEntry);
26059
+ if (existsSync16(profilesDir)) {
26060
+ for (const pEntry of readdirSync9(profilesDir)) {
26061
+ const pPath = join16(profilesDir, pEntry);
24988
26062
  if (statSync5(pPath).isFile() && pEntry.endsWith(".json")) {
24989
26063
  try {
24990
- profiles[pEntry.replace(/\.json$/, "")] = JSON.parse(readFileSync7(pPath, "utf-8"));
26064
+ profiles[pEntry.replace(/\.json$/, "")] = JSON.parse(readFileSync9(pPath, "utf-8"));
24991
26065
  } catch {}
24992
26066
  } else if (statSync5(pPath).isDirectory()) {
24993
- const configPath = join14(pPath, "config.json");
24994
- const tokensPath = join14(pPath, "tokens.json");
26067
+ const configPath = join16(pPath, "config.json");
26068
+ const tokensPath = join16(pPath, "tokens.json");
24995
26069
  let merged = {};
24996
- if (existsSync13(configPath)) {
26070
+ if (existsSync16(configPath)) {
24997
26071
  try {
24998
- merged = { ...merged, ...JSON.parse(readFileSync7(configPath, "utf-8")) };
26072
+ merged = { ...merged, ...JSON.parse(readFileSync9(configPath, "utf-8")) };
24999
26073
  } catch {}
25000
26074
  }
25001
- if (existsSync13(tokensPath)) {
26075
+ if (existsSync16(tokensPath)) {
25002
26076
  try {
25003
- merged = { ...merged, ...JSON.parse(readFileSync7(tokensPath, "utf-8")) };
26077
+ merged = { ...merged, ...JSON.parse(readFileSync9(tokensPath, "utf-8")) };
25004
26078
  } catch {}
25005
26079
  }
25006
26080
  if (Object.keys(merged).length > 0)
@@ -25021,7 +26095,7 @@ program2.command("export").option("-o, --output <file>", "Write to file instead
25021
26095
  }
25022
26096
  const exportData = JSON.stringify(exportPayload, null, 2);
25023
26097
  if (options.output) {
25024
- writeFileSync6(options.output, exportData);
26098
+ writeFileSync8(options.output, exportData);
25025
26099
  console.log(chalk2.green(`\u2713 Exported to ${options.output}`));
25026
26100
  } else {
25027
26101
  console.log(exportData);
@@ -25035,7 +26109,7 @@ program2.command("import").argument("<file>", "JSON backup file to import (use -
25035
26109
  chunks.push(chunk.toString());
25036
26110
  raw = chunks.join("");
25037
26111
  } else {
25038
- if (!existsSync13(file)) {
26112
+ if (!existsSync16(file)) {
25039
26113
  if (options.json) {
25040
26114
  console.log(JSON.stringify({ error: `File not found: ${file}` }));
25041
26115
  } else {
@@ -25044,7 +26118,7 @@ program2.command("import").argument("<file>", "JSON backup file to import (use -
25044
26118
  process.exit(1);
25045
26119
  return;
25046
26120
  }
25047
- raw = readFileSync7(file, "utf-8");
26121
+ raw = readFileSync9(file, "utf-8");
25048
26122
  }
25049
26123
  let data;
25050
26124
  try {
@@ -25072,20 +26146,20 @@ program2.command("import").argument("<file>", "JSON backup file to import (use -
25072
26146
  for (const [connectorName, connData] of Object.entries(data.connectors)) {
25073
26147
  if (!/^[a-z0-9-]+$/.test(connectorName))
25074
26148
  continue;
25075
- const connectorDir = join14(connectDir, `connect-${connectorName}`);
26149
+ const connectorDir = join16(connectDir, `connect-${connectorName}`);
25076
26150
  if (connData.credentials && typeof connData.credentials === "object") {
25077
- mkdirSync10(connectorDir, { recursive: true });
25078
- writeFileSync6(join14(connectorDir, "credentials.json"), JSON.stringify(connData.credentials, null, 2));
26151
+ mkdirSync11(connectorDir, { recursive: true });
26152
+ writeFileSync8(join16(connectorDir, "credentials.json"), JSON.stringify(connData.credentials, null, 2));
25079
26153
  imported++;
25080
26154
  }
25081
26155
  if (!connData.profiles || typeof connData.profiles !== "object")
25082
26156
  continue;
25083
- const profilesDir = join14(connectorDir, "profiles");
26157
+ const profilesDir = join16(connectorDir, "profiles");
25084
26158
  for (const [profileName, config] of Object.entries(connData.profiles)) {
25085
26159
  if (!config || typeof config !== "object")
25086
26160
  continue;
25087
- mkdirSync10(profilesDir, { recursive: true });
25088
- writeFileSync6(join14(profilesDir, `${profileName}.json`), JSON.stringify(config, null, 2));
26161
+ mkdirSync11(profilesDir, { recursive: true });
26162
+ writeFileSync8(join16(profilesDir, `${profileName}.json`), JSON.stringify(config, null, 2));
25089
26163
  imported++;
25090
26164
  }
25091
26165
  }
@@ -25096,9 +26170,9 @@ program2.command("import").argument("<file>", "JSON backup file to import (use -
25096
26170
  }
25097
26171
  });
25098
26172
  program2.command("auth-import").option("--json", "Output as JSON", false).option("-d, --dry-run", "Preview what would be imported without copying", false).option("--force", "Overwrite existing files in ~/.hasna/connectors/", false).description("Migrate auth tokens from ~/.connect/ to ~/.hasna/connectors/").action((options) => {
25099
- const oldBase = join14(homedir8(), ".connect");
26173
+ const oldBase = join16(homedir8(), ".connect");
25100
26174
  const newBase = getConnectorsHome();
25101
- if (!existsSync13(oldBase)) {
26175
+ if (!existsSync16(oldBase)) {
25102
26176
  if (options.json) {
25103
26177
  console.log(JSON.stringify({ imported: [], skipped: [], error: null, message: "No ~/.connect/ directory found" }));
25104
26178
  } else {
@@ -25106,11 +26180,11 @@ program2.command("auth-import").option("--json", "Output as JSON", false).option
25106
26180
  }
25107
26181
  return;
25108
26182
  }
25109
- const entries = readdirSync8(oldBase).filter((name) => {
26183
+ const entries = readdirSync9(oldBase).filter((name) => {
25110
26184
  if (!name.startsWith("connect-"))
25111
26185
  return false;
25112
26186
  try {
25113
- return statSync5(join14(oldBase, name)).isDirectory();
26187
+ return statSync5(join16(oldBase, name)).isDirectory();
25114
26188
  } catch {
25115
26189
  return false;
25116
26190
  }
@@ -25126,8 +26200,8 @@ program2.command("auth-import").option("--json", "Output as JSON", false).option
25126
26200
  const imported = [];
25127
26201
  const skipped = [];
25128
26202
  for (const dirName of entries) {
25129
- const oldDir = join14(oldBase, dirName);
25130
- const newDir = join14(newBase, dirName);
26203
+ const oldDir = join16(oldBase, dirName);
26204
+ const newDir = join16(newBase, dirName);
25131
26205
  const connectorName = dirName.replace(/^connect-/, "");
25132
26206
  const allFiles = listFilesRecursive(oldDir);
25133
26207
  const authFiles = allFiles.filter((f) => {
@@ -25138,17 +26212,17 @@ program2.command("auth-import").option("--json", "Output as JSON", false).option
25138
26212
  const copiedFiles = [];
25139
26213
  const skippedFiles = [];
25140
26214
  for (const relFile of authFiles) {
25141
- const srcPath = join14(oldDir, relFile);
25142
- const destPath = join14(newDir, relFile);
25143
- if (existsSync13(destPath) && !options.force) {
26215
+ const srcPath = join16(oldDir, relFile);
26216
+ const destPath = join16(newDir, relFile);
26217
+ if (existsSync16(destPath) && !options.force) {
25144
26218
  skippedFiles.push(relFile);
25145
26219
  continue;
25146
26220
  }
25147
26221
  if (!options.dryRun) {
25148
- const parentDir = join14(destPath, "..");
25149
- mkdirSync10(parentDir, { recursive: true });
25150
- const content = readFileSync7(srcPath);
25151
- writeFileSync6(destPath, content);
26222
+ const parentDir = join16(destPath, "..");
26223
+ mkdirSync11(parentDir, { recursive: true });
26224
+ const content = readFileSync9(srcPath);
26225
+ writeFileSync8(destPath, content);
25152
26226
  }
25153
26227
  copiedFiles.push(relFile);
25154
26228
  }
@@ -25393,7 +26467,7 @@ program2.command("env").option("-o, --output <file>", "Write to file instead of
25393
26467
  `) + `
25394
26468
  `;
25395
26469
  if (options.output) {
25396
- writeFileSync6(options.output, output);
26470
+ writeFileSync8(options.output, output);
25397
26471
  console.log(chalk2.green(`\u2713 Written to ${options.output} (${vars.length} variables)`));
25398
26472
  } else {
25399
26473
  console.log(output);
@@ -25435,23 +26509,23 @@ program2.command("whoami").option("--json", "Output as JSON", false).description
25435
26509
  configured++;
25436
26510
  else
25437
26511
  unconfigured++;
25438
- const connectorConfigDir = join14(configDir, name.startsWith("connect-") ? name : `connect-${name}`);
25439
- const currentProfileFile = join14(connectorConfigDir, "current_profile");
26512
+ const connectorConfigDir = join16(configDir, name.startsWith("connect-") ? name : `connect-${name}`);
26513
+ const currentProfileFile = join16(connectorConfigDir, "current_profile");
25440
26514
  let profile = "default";
25441
- if (existsSync13(currentProfileFile)) {
26515
+ if (existsSync16(currentProfileFile)) {
25442
26516
  try {
25443
- profile = readFileSync7(currentProfileFile, "utf-8").trim() || "default";
26517
+ profile = readFileSync9(currentProfileFile, "utf-8").trim() || "default";
25444
26518
  } catch {}
25445
26519
  }
25446
26520
  connectorDetails.push({ name, configured: auth.configured, authType: auth.type, profile, source: "project" });
25447
26521
  }
25448
- if (existsSync13(configDir)) {
26522
+ if (existsSync16(configDir)) {
25449
26523
  try {
25450
- const globalDirs = readdirSync8(configDir).filter((f) => {
26524
+ const globalDirs = readdirSync9(configDir).filter((f) => {
25451
26525
  if (!f.startsWith("connect-"))
25452
26526
  return false;
25453
26527
  try {
25454
- return statSync5(join14(configDir, f)).isDirectory();
26528
+ return statSync5(join16(configDir, f)).isDirectory();
25455
26529
  } catch {
25456
26530
  return false;
25457
26531
  }
@@ -25465,11 +26539,11 @@ program2.command("whoami").option("--json", "Output as JSON", false).description
25465
26539
  continue;
25466
26540
  seen.add(name);
25467
26541
  configured++;
25468
- const currentProfileFile = join14(configDir, dir, "current_profile");
26542
+ const currentProfileFile = join16(configDir, dir, "current_profile");
25469
26543
  let profile = "default";
25470
- if (existsSync13(currentProfileFile)) {
26544
+ if (existsSync16(currentProfileFile)) {
25471
26545
  try {
25472
- profile = readFileSync7(currentProfileFile, "utf-8").trim() || "default";
26546
+ profile = readFileSync9(currentProfileFile, "utf-8").trim() || "default";
25473
26547
  } catch {}
25474
26548
  }
25475
26549
  connectorDetails.push({ name, configured: true, authType: auth.type, profile, source: "global" });
@@ -25480,7 +26554,7 @@ program2.command("whoami").option("--json", "Output as JSON", false).description
25480
26554
  console.log(JSON.stringify({
25481
26555
  version,
25482
26556
  configDir,
25483
- configDirExists: existsSync13(configDir),
26557
+ configDirExists: existsSync16(configDir),
25484
26558
  installed: installed.length,
25485
26559
  configured,
25486
26560
  unconfigured,
@@ -25492,7 +26566,7 @@ program2.command("whoami").option("--json", "Output as JSON", false).description
25492
26566
  Connectors Setup
25493
26567
  `));
25494
26568
  console.log(` Version: ${chalk2.cyan(version)}`);
25495
- console.log(` Config: ${configDir}${existsSync13(configDir) ? "" : chalk2.dim(" (not created yet)")}`);
26569
+ console.log(` Config: ${configDir}${existsSync16(configDir) ? "" : chalk2.dim(" (not created yet)")}`);
25496
26570
  console.log(` Installed: ${installed.length} connector${installed.length !== 1 ? "s" : ""}`);
25497
26571
  console.log(` Configured: ${chalk2.green(String(configured))} ready, ${unconfigured > 0 ? chalk2.red(String(unconfigured)) : chalk2.dim("0")} need auth`);
25498
26572
  const projectConnectors = connectorDetails.filter((c) => c.source === "project");
@@ -25582,18 +26656,18 @@ Testing connector credentials...
25582
26656
  }
25583
26657
  }
25584
26658
  if (!apiKey) {
25585
- const connectorConfigDir = join14(getConnectorsHome(), name.startsWith("connect-") ? name : `connect-${name}`);
26659
+ const connectorConfigDir = join16(getConnectorsHome(), name.startsWith("connect-") ? name : `connect-${name}`);
25586
26660
  let currentProfile = "default";
25587
- const currentProfileFile = join14(connectorConfigDir, "current_profile");
25588
- if (existsSync13(currentProfileFile)) {
26661
+ const currentProfileFile = join16(connectorConfigDir, "current_profile");
26662
+ if (existsSync16(currentProfileFile)) {
25589
26663
  try {
25590
- currentProfile = readFileSync7(currentProfileFile, "utf-8").trim() || "default";
26664
+ currentProfile = readFileSync9(currentProfileFile, "utf-8").trim() || "default";
25591
26665
  } catch {}
25592
26666
  }
25593
- const tokensFile = join14(connectorConfigDir, "profiles", currentProfile, "tokens.json");
25594
- if (existsSync13(tokensFile)) {
26667
+ const tokensFile = join16(connectorConfigDir, "profiles", currentProfile, "tokens.json");
26668
+ if (existsSync16(tokensFile)) {
25595
26669
  try {
25596
- const tokens = JSON.parse(readFileSync7(tokensFile, "utf-8"));
26670
+ const tokens = JSON.parse(readFileSync9(tokensFile, "utf-8"));
25597
26671
  const isExpired = tokens.expiresAt && Date.now() >= tokens.expiresAt - 60000;
25598
26672
  if (isExpired && tokens.refreshToken) {
25599
26673
  try {
@@ -25611,19 +26685,19 @@ Testing connector credentials...
25611
26685
  } catch {}
25612
26686
  }
25613
26687
  if (!apiKey) {
25614
- const profileFile = join14(connectorConfigDir, "profiles", `${currentProfile}.json`);
25615
- if (existsSync13(profileFile)) {
26688
+ const profileFile = join16(connectorConfigDir, "profiles", `${currentProfile}.json`);
26689
+ if (existsSync16(profileFile)) {
25616
26690
  try {
25617
- const config = JSON.parse(readFileSync7(profileFile, "utf-8"));
26691
+ const config = JSON.parse(readFileSync9(profileFile, "utf-8"));
25618
26692
  apiKey = Object.values(config).find((v) => typeof v === "string" && v.length > 0);
25619
26693
  } catch {}
25620
26694
  }
25621
26695
  }
25622
26696
  if (!apiKey) {
25623
- const profileDirConfig = join14(connectorConfigDir, "profiles", currentProfile, "config.json");
25624
- if (existsSync13(profileDirConfig)) {
26697
+ const profileDirConfig = join16(connectorConfigDir, "profiles", currentProfile, "config.json");
26698
+ if (existsSync16(profileDirConfig)) {
25625
26699
  try {
25626
- const config = JSON.parse(readFileSync7(profileDirConfig, "utf-8"));
26700
+ const config = JSON.parse(readFileSync9(profileDirConfig, "utf-8"));
25627
26701
  apiKey = Object.values(config).find((v) => typeof v === "string" && v.length > 0);
25628
26702
  } catch {}
25629
26703
  }
@@ -25790,7 +26864,7 @@ Setting up ${meta.displayName}...
25790
26864
  const alreadyInstalled = installed.includes(meta.name);
25791
26865
  let installResult;
25792
26866
  if (alreadyInstalled && !options.overwrite) {
25793
- installResult = { success: true, path: join14(process.cwd(), ".connectors", `connect-${meta.name}`) };
26867
+ installResult = { success: true, path: join16(process.cwd(), ".connectors", `connect-${meta.name}`) };
25794
26868
  if (!options.json) {
25795
26869
  console.log(` ${chalk2.green("\u2713")} Already installed`);
25796
26870
  }
@@ -26212,4 +27286,88 @@ llmCmd.command("providers").description("List supported LLM providers").option("
26212
27286
  }
26213
27287
  });
26214
27288
  registerCloudCommands(program2, "connectors");
27289
+ {
27290
+ const cloudCmd = program2.commands.find((c) => c.name() === "cloud");
27291
+ if (cloudCmd) {
27292
+ const syncCmd = cloudCmd.command("sync").description("Incremental (delta) sync \u2014 only rows changed since last sync");
27293
+ syncCmd.command("push").description("Push local changes to cloud (incremental by default)").option("--tables <tables>", "Comma-separated table names").option("--full", "Full resync instead of incremental delta", false).action(async (opts) => {
27294
+ const local = new SqliteAdapter(getDbPath("connectors"));
27295
+ const tables = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : listSqliteTables(local);
27296
+ if (opts.full) {
27297
+ const cloud = new PgAdapterAsync(getConnectionString("connectors"));
27298
+ const { syncPush: syncPush2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
27299
+ const results = await syncPush2(local, cloud, {
27300
+ tables,
27301
+ onProgress: (p) => {
27302
+ if (p.phase === "done")
27303
+ console.log(` ${p.table}: ${p.rowsWritten} rows pushed (full)`);
27304
+ }
27305
+ });
27306
+ local.close();
27307
+ await cloud.close();
27308
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
27309
+ console.log(`Done. ${total} rows pushed (full sync).`);
27310
+ } else {
27311
+ const cloud = new PgAdapter(getConnectionString("connectors"));
27312
+ const results = incrementalSyncPush(local, cloud, tables);
27313
+ local.close();
27314
+ cloud.close();
27315
+ const total = results.reduce((s, r) => s + r.synced_rows, 0);
27316
+ const firstSync = results.filter((r) => r.first_sync).length;
27317
+ console.log(`Done. ${total} rows pushed (incremental).${firstSync ? ` ${firstSync} table(s) had first-sync.` : ""}`);
27318
+ for (const r of results) {
27319
+ if (r.errors.length)
27320
+ console.warn(` ${r.table}: ${r.errors.join(", ")}`);
27321
+ }
27322
+ }
27323
+ });
27324
+ syncCmd.command("pull").description("Pull cloud changes to local (incremental by default)").option("--tables <tables>", "Comma-separated table names").option("--full", "Full resync instead of incremental delta", false).action(async (opts) => {
27325
+ const local = new SqliteAdapter(getDbPath("connectors"));
27326
+ if (opts.full) {
27327
+ const cloud = new PgAdapterAsync(getConnectionString("connectors"));
27328
+ const tables = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : await listPgTables(cloud);
27329
+ const { syncPull: syncPull2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
27330
+ const results = await syncPull2(cloud, local, {
27331
+ tables,
27332
+ onProgress: (p) => {
27333
+ if (p.phase === "done")
27334
+ console.log(` ${p.table}: ${p.rowsWritten} rows pulled (full)`);
27335
+ }
27336
+ });
27337
+ local.close();
27338
+ await cloud.close();
27339
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
27340
+ console.log(`Done. ${total} rows pulled (full sync).`);
27341
+ } else {
27342
+ const cloud = new PgAdapter(getConnectionString("connectors"));
27343
+ const tables = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : listSqliteTables(local);
27344
+ const results = incrementalSyncPull(cloud, local, tables);
27345
+ local.close();
27346
+ cloud.close();
27347
+ const total = results.reduce((s, r) => s + r.synced_rows, 0);
27348
+ console.log(`Done. ${total} rows pulled (incremental).`);
27349
+ for (const r of results) {
27350
+ if (r.errors.length)
27351
+ console.warn(` ${r.table}: ${r.errors.join(", ")}`);
27352
+ }
27353
+ }
27354
+ });
27355
+ syncCmd.command("status").description("Show last-synced timestamps per table").action(() => {
27356
+ const local = new SqliteAdapter(getDbPath("connectors"));
27357
+ const meta = getSyncMetaAll(local);
27358
+ local.close();
27359
+ if (!meta.length) {
27360
+ console.log("No sync history found. Run: connectors cloud sync push");
27361
+ return;
27362
+ }
27363
+ console.log(chalk2.bold(`
27364
+ Sync status:
27365
+ `));
27366
+ for (const m of meta) {
27367
+ console.log(` ${chalk2.cyan(m.table_name.padEnd(32))} last synced: ${m.last_synced_at ?? "never"} (${m.direction})`);
27368
+ }
27369
+ console.log();
27370
+ });
27371
+ }
27372
+ }
26215
27373
  program2.parse();