@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 +1405 -247
- package/bin/mcp.js +3 -2
- package/bin/serve.js +2 -1
- package/dist/db/database.test.d.ts +1 -0
- package/dist/lib/scheduler.d.ts +2 -0
- package/dist/lib/workflow-runner.test.d.ts +1 -0
- package/package.json +1 -1
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
|
-
)`,
|
|
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
|
|
12997
|
+
import { join as join7 } from "path";
|
|
11925
12998
|
import { homedir as homedir6 } from "os";
|
|
11926
|
-
import { mkdirSync as
|
|
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 =
|
|
11930
|
-
const oldDir =
|
|
11931
|
-
if (
|
|
11932
|
-
|
|
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
|
|
11935
|
-
const oldPath =
|
|
11936
|
-
const newPath =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
13151
|
+
DB_PATH = join7(DB_DIR, "connectors.db");
|
|
12079
13152
|
});
|
|
12080
13153
|
|
|
12081
13154
|
// src/lib/llm.ts
|
|
12082
|
-
import { existsSync as
|
|
12083
|
-
import { join as
|
|
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
|
|
13158
|
+
return join9(getConnectorsHome(), "llm.json");
|
|
12086
13159
|
}
|
|
12087
13160
|
function getLlmConfig() {
|
|
12088
13161
|
const path = getLlmConfigPath();
|
|
12089
|
-
if (!
|
|
13162
|
+
if (!existsSync9(path))
|
|
12090
13163
|
return null;
|
|
12091
13164
|
try {
|
|
12092
|
-
return JSON.parse(
|
|
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
|
-
|
|
12100
|
-
|
|
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
|
|
12614
|
-
import { join as
|
|
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
|
-
|
|
12776
|
-
|
|
13849
|
+
join10(thisDir, "..", "connectors"),
|
|
13850
|
+
join10(thisDir, "..", "..", "connectors")
|
|
12777
13851
|
];
|
|
12778
|
-
const connectorsDir = candidates.find((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 =
|
|
12784
|
-
if (
|
|
12785
|
-
const pkg = JSON.parse(
|
|
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
|
|
20360
|
-
import { join as
|
|
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 =
|
|
20364
|
-
if (
|
|
21437
|
+
const fromBin = join11(__dirname2, "..", "connectors");
|
|
21438
|
+
if (existsSync11(fromBin))
|
|
20365
21439
|
return fromBin;
|
|
20366
|
-
const fromSrc =
|
|
20367
|
-
if (
|
|
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
|
|
21447
|
+
return join11(CONNECTORS_DIR, connectorName);
|
|
20374
21448
|
}
|
|
20375
21449
|
function connectorExists(name) {
|
|
20376
|
-
return
|
|
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 =
|
|
20390
|
-
const destPath =
|
|
20391
|
-
if (!
|
|
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 (
|
|
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 (!
|
|
20408
|
-
|
|
21481
|
+
if (!existsSync11(destDir)) {
|
|
21482
|
+
mkdirSync7(destDir, { recursive: true });
|
|
20409
21483
|
}
|
|
20410
21484
|
cpSync(sourcePath, destPath, { recursive: true });
|
|
20411
|
-
const homeCredDir =
|
|
20412
|
-
if (
|
|
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 =
|
|
20416
|
-
if (
|
|
20417
|
-
cpSync(src,
|
|
21489
|
+
const src = join11(homeCredDir, file);
|
|
21490
|
+
if (existsSync11(src)) {
|
|
21491
|
+
cpSync(src, join11(destPath, file));
|
|
20418
21492
|
}
|
|
20419
21493
|
}
|
|
20420
|
-
const profilesDir =
|
|
20421
|
-
if (
|
|
20422
|
-
cpSync(profilesDir,
|
|
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 =
|
|
20441
|
-
const connectors =
|
|
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
|
-
|
|
21528
|
+
writeFileSync5(indexPath, content);
|
|
20455
21529
|
}
|
|
20456
21530
|
function getInstalledConnectors(targetDir = process.cwd()) {
|
|
20457
|
-
const connectorsDir =
|
|
20458
|
-
if (!
|
|
21531
|
+
const connectorsDir = join11(targetDir, ".connectors");
|
|
21532
|
+
if (!existsSync11(connectorsDir)) {
|
|
20459
21533
|
return [];
|
|
20460
21534
|
}
|
|
20461
|
-
return
|
|
20462
|
-
const fullPath =
|
|
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 =
|
|
20469
|
-
if (!
|
|
21542
|
+
const claudeMdPath = join11(connectorPath, "CLAUDE.md");
|
|
21543
|
+
if (!existsSync11(claudeMdPath))
|
|
20470
21544
|
return null;
|
|
20471
|
-
const raw =
|
|
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 =
|
|
20511
|
-
const connectorPath =
|
|
20512
|
-
if (!
|
|
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
|
|
20528
|
-
import { join as
|
|
20529
|
-
import { mkdirSync as
|
|
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 =
|
|
20532
|
-
|
|
20533
|
-
return
|
|
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 (
|
|
21618
|
+
if (existsSync12(path) && isStale(path)) {
|
|
20545
21619
|
try {
|
|
20546
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
21683
|
+
return join13(getConnectorsHome(), connectorName);
|
|
20610
21684
|
}
|
|
20611
21685
|
function getCurrentProfile(name) {
|
|
20612
21686
|
const configDir = getConnectorConfigDir(name);
|
|
20613
|
-
const currentProfileFile =
|
|
20614
|
-
if (
|
|
21687
|
+
const currentProfileFile = join13(configDir, "current_profile");
|
|
21688
|
+
if (existsSync13(currentProfileFile)) {
|
|
20615
21689
|
try {
|
|
20616
|
-
return
|
|
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 =
|
|
20629
|
-
if (
|
|
21702
|
+
const profileFile = join13(configDir, "profiles", `${profile}.json`);
|
|
21703
|
+
if (existsSync13(profileFile)) {
|
|
20630
21704
|
try {
|
|
20631
|
-
flatConfig = JSON.parse(
|
|
21705
|
+
flatConfig = JSON.parse(readFileSync7(profileFile, "utf-8"));
|
|
20632
21706
|
} catch {}
|
|
20633
21707
|
}
|
|
20634
|
-
const profileDirConfig =
|
|
20635
|
-
if (
|
|
21708
|
+
const profileDirConfig = join13(configDir, "profiles", profile, "config.json");
|
|
21709
|
+
if (existsSync13(profileDirConfig)) {
|
|
20636
21710
|
try {
|
|
20637
|
-
dirConfig = JSON.parse(
|
|
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 =
|
|
20649
|
-
if (
|
|
21722
|
+
const tokensFile = join13(configDir, "profiles", profile, "tokens.json");
|
|
21723
|
+
if (existsSync13(tokensFile)) {
|
|
20650
21724
|
try {
|
|
20651
|
-
return JSON.parse(
|
|
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 =
|
|
20719
|
-
|
|
21792
|
+
const credentialsFile = join13(configDir, "credentials.json");
|
|
21793
|
+
mkdirSync9(configDir, { recursive: true });
|
|
20720
21794
|
let creds = {};
|
|
20721
|
-
if (
|
|
21795
|
+
if (existsSync13(credentialsFile)) {
|
|
20722
21796
|
try {
|
|
20723
|
-
creds = JSON.parse(
|
|
21797
|
+
creds = JSON.parse(readFileSync7(credentialsFile, "utf-8"));
|
|
20724
21798
|
} catch {}
|
|
20725
21799
|
}
|
|
20726
21800
|
creds[keyField] = key;
|
|
20727
|
-
|
|
21801
|
+
writeFileSync6(credentialsFile, JSON.stringify(creds, null, 2));
|
|
20728
21802
|
return;
|
|
20729
21803
|
}
|
|
20730
|
-
const profileFile =
|
|
20731
|
-
const profileDir =
|
|
20732
|
-
if (
|
|
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(
|
|
21809
|
+
config = JSON.parse(readFileSync7(profileFile, "utf-8"));
|
|
20736
21810
|
} catch {}
|
|
20737
21811
|
config[keyField] = key;
|
|
20738
|
-
|
|
21812
|
+
writeFileSync6(profileFile, JSON.stringify(config, null, 2));
|
|
20739
21813
|
return;
|
|
20740
21814
|
}
|
|
20741
|
-
if (
|
|
20742
|
-
const configFile =
|
|
21815
|
+
if (existsSync13(profileDir)) {
|
|
21816
|
+
const configFile = join13(profileDir, "config.json");
|
|
20743
21817
|
let config = {};
|
|
20744
|
-
if (
|
|
21818
|
+
if (existsSync13(configFile)) {
|
|
20745
21819
|
try {
|
|
20746
|
-
config = JSON.parse(
|
|
21820
|
+
config = JSON.parse(readFileSync7(configFile, "utf-8"));
|
|
20747
21821
|
} catch {}
|
|
20748
21822
|
}
|
|
20749
21823
|
config[keyField] = key;
|
|
20750
|
-
|
|
21824
|
+
writeFileSync6(configFile, JSON.stringify(config, null, 2));
|
|
20751
21825
|
return;
|
|
20752
21826
|
}
|
|
20753
|
-
|
|
20754
|
-
|
|
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 =
|
|
20773
|
-
if (
|
|
21846
|
+
const credentialsFile = join13(configDir, "credentials.json");
|
|
21847
|
+
if (existsSync13(credentialsFile)) {
|
|
20774
21848
|
try {
|
|
20775
|
-
const creds = JSON.parse(
|
|
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 =
|
|
20855
|
-
|
|
20856
|
-
const tokensFile =
|
|
20857
|
-
|
|
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 =
|
|
20900
|
-
if (!
|
|
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 =
|
|
21978
|
+
const entries = readdirSync7(profilesDir);
|
|
20905
21979
|
for (const entry of entries) {
|
|
20906
|
-
const fullPath =
|
|
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
|
-
|
|
20921
|
-
|
|
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 =
|
|
20928
|
-
const profileFile =
|
|
20929
|
-
if (
|
|
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 =
|
|
20937
|
-
if (
|
|
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
|
|
21205
|
-
import { join as
|
|
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(
|
|
21218
|
-
candidates.push(
|
|
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(
|
|
21223
|
-
candidates.push(
|
|
22296
|
+
candidates.push(join15(mainDir, "..", "dashboard", "dist"));
|
|
22297
|
+
candidates.push(join15(mainDir, "..", "..", "dashboard", "dist"));
|
|
21224
22298
|
}
|
|
21225
|
-
candidates.push(
|
|
22299
|
+
candidates.push(join15(process.cwd(), "dashboard", "dist"));
|
|
21226
22300
|
for (const candidate of candidates) {
|
|
21227
|
-
if (
|
|
22301
|
+
if (existsSync15(candidate))
|
|
21228
22302
|
return candidate;
|
|
21229
22303
|
}
|
|
21230
|
-
return
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
21657
|
-
const currentProfileFile =
|
|
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 (
|
|
22733
|
+
if (existsSync15(currentProfileFile)) {
|
|
21660
22734
|
try {
|
|
21661
|
-
current =
|
|
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 (
|
|
21711
|
-
const entries =
|
|
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 =
|
|
21717
|
-
if (!
|
|
22790
|
+
const profilesDir = join15(connectDir, entry.name, "profiles");
|
|
22791
|
+
if (!existsSync15(profilesDir))
|
|
21718
22792
|
continue;
|
|
21719
22793
|
const profiles = {};
|
|
21720
|
-
const profileEntries =
|
|
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(
|
|
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 =
|
|
21731
|
-
if (
|
|
22804
|
+
const configPath = join15(profilesDir, pEntry.name, "config.json");
|
|
22805
|
+
if (existsSync15(configPath)) {
|
|
21732
22806
|
try {
|
|
21733
|
-
const config = JSON.parse(
|
|
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 =
|
|
21775
|
-
const profilesDir =
|
|
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
|
-
|
|
21780
|
-
const profileFile =
|
|
21781
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
23697
|
-
import { join as
|
|
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 =
|
|
23703
|
-
if (
|
|
24776
|
+
const fromBin = join14(__dirname3, "..", "connectors");
|
|
24777
|
+
if (existsSync14(fromBin))
|
|
23704
24778
|
return fromBin;
|
|
23705
|
-
const fromSrc =
|
|
23706
|
-
if (
|
|
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 =
|
|
23747
|
-
const cliPath =
|
|
23748
|
-
if (
|
|
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
|
|
23862
|
-
const fullPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
25523
|
+
const currentProfileFile = join16(configDir, connectorName, "current_profile");
|
|
24450
25524
|
let profile = "default";
|
|
24451
|
-
if (
|
|
25525
|
+
if (existsSync16(currentProfileFile)) {
|
|
24452
25526
|
try {
|
|
24453
|
-
profile =
|
|
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 (
|
|
25563
|
+
if (existsSync16(configDir)) {
|
|
24490
25564
|
try {
|
|
24491
|
-
const globalDirs =
|
|
25565
|
+
const globalDirs = readdirSync9(configDir).filter((f) => {
|
|
24492
25566
|
if (!f.startsWith("connect-"))
|
|
24493
25567
|
return false;
|
|
24494
25568
|
try {
|
|
24495
|
-
return statSync5(
|
|
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 (
|
|
24865
|
-
const entries =
|
|
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 =
|
|
24868
|
-
if (
|
|
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 (
|
|
24969
|
-
for (const entry of
|
|
24970
|
-
const entryPath =
|
|
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 =
|
|
24976
|
-
if (
|
|
26049
|
+
const credentialsPath = join16(entryPath, "credentials.json");
|
|
26050
|
+
if (existsSync16(credentialsPath)) {
|
|
24977
26051
|
try {
|
|
24978
|
-
credentials = JSON.parse(
|
|
26052
|
+
credentials = JSON.parse(readFileSync9(credentialsPath, "utf-8"));
|
|
24979
26053
|
} catch {}
|
|
24980
26054
|
}
|
|
24981
|
-
const profilesDir =
|
|
24982
|
-
if (!
|
|
26055
|
+
const profilesDir = join16(entryPath, "profiles");
|
|
26056
|
+
if (!existsSync16(profilesDir) && !credentials)
|
|
24983
26057
|
continue;
|
|
24984
26058
|
const profiles = {};
|
|
24985
|
-
if (
|
|
24986
|
-
for (const pEntry of
|
|
24987
|
-
const pPath =
|
|
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(
|
|
26064
|
+
profiles[pEntry.replace(/\.json$/, "")] = JSON.parse(readFileSync9(pPath, "utf-8"));
|
|
24991
26065
|
} catch {}
|
|
24992
26066
|
} else if (statSync5(pPath).isDirectory()) {
|
|
24993
|
-
const configPath =
|
|
24994
|
-
const tokensPath =
|
|
26067
|
+
const configPath = join16(pPath, "config.json");
|
|
26068
|
+
const tokensPath = join16(pPath, "tokens.json");
|
|
24995
26069
|
let merged = {};
|
|
24996
|
-
if (
|
|
26070
|
+
if (existsSync16(configPath)) {
|
|
24997
26071
|
try {
|
|
24998
|
-
merged = { ...merged, ...JSON.parse(
|
|
26072
|
+
merged = { ...merged, ...JSON.parse(readFileSync9(configPath, "utf-8")) };
|
|
24999
26073
|
} catch {}
|
|
25000
26074
|
}
|
|
25001
|
-
if (
|
|
26075
|
+
if (existsSync16(tokensPath)) {
|
|
25002
26076
|
try {
|
|
25003
|
-
merged = { ...merged, ...JSON.parse(
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
26149
|
+
const connectorDir = join16(connectDir, `connect-${connectorName}`);
|
|
25076
26150
|
if (connData.credentials && typeof connData.credentials === "object") {
|
|
25077
|
-
|
|
25078
|
-
|
|
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 =
|
|
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
|
-
|
|
25088
|
-
|
|
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 =
|
|
26173
|
+
const oldBase = join16(homedir8(), ".connect");
|
|
25100
26174
|
const newBase = getConnectorsHome();
|
|
25101
|
-
if (!
|
|
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 =
|
|
26183
|
+
const entries = readdirSync9(oldBase).filter((name) => {
|
|
25110
26184
|
if (!name.startsWith("connect-"))
|
|
25111
26185
|
return false;
|
|
25112
26186
|
try {
|
|
25113
|
-
return statSync5(
|
|
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 =
|
|
25130
|
-
const newDir =
|
|
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 =
|
|
25142
|
-
const destPath =
|
|
25143
|
-
if (
|
|
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 =
|
|
25149
|
-
|
|
25150
|
-
const content =
|
|
25151
|
-
|
|
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
|
-
|
|
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 =
|
|
25439
|
-
const currentProfileFile =
|
|
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 (
|
|
26515
|
+
if (existsSync16(currentProfileFile)) {
|
|
25442
26516
|
try {
|
|
25443
|
-
profile =
|
|
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 (
|
|
26522
|
+
if (existsSync16(configDir)) {
|
|
25449
26523
|
try {
|
|
25450
|
-
const globalDirs =
|
|
26524
|
+
const globalDirs = readdirSync9(configDir).filter((f) => {
|
|
25451
26525
|
if (!f.startsWith("connect-"))
|
|
25452
26526
|
return false;
|
|
25453
26527
|
try {
|
|
25454
|
-
return statSync5(
|
|
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 =
|
|
26542
|
+
const currentProfileFile = join16(configDir, dir, "current_profile");
|
|
25469
26543
|
let profile = "default";
|
|
25470
|
-
if (
|
|
26544
|
+
if (existsSync16(currentProfileFile)) {
|
|
25471
26545
|
try {
|
|
25472
|
-
profile =
|
|
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:
|
|
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}${
|
|
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 =
|
|
26659
|
+
const connectorConfigDir = join16(getConnectorsHome(), name.startsWith("connect-") ? name : `connect-${name}`);
|
|
25586
26660
|
let currentProfile = "default";
|
|
25587
|
-
const currentProfileFile =
|
|
25588
|
-
if (
|
|
26661
|
+
const currentProfileFile = join16(connectorConfigDir, "current_profile");
|
|
26662
|
+
if (existsSync16(currentProfileFile)) {
|
|
25589
26663
|
try {
|
|
25590
|
-
currentProfile =
|
|
26664
|
+
currentProfile = readFileSync9(currentProfileFile, "utf-8").trim() || "default";
|
|
25591
26665
|
} catch {}
|
|
25592
26666
|
}
|
|
25593
|
-
const tokensFile =
|
|
25594
|
-
if (
|
|
26667
|
+
const tokensFile = join16(connectorConfigDir, "profiles", currentProfile, "tokens.json");
|
|
26668
|
+
if (existsSync16(tokensFile)) {
|
|
25595
26669
|
try {
|
|
25596
|
-
const tokens = JSON.parse(
|
|
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 =
|
|
25615
|
-
if (
|
|
26688
|
+
const profileFile = join16(connectorConfigDir, "profiles", `${currentProfile}.json`);
|
|
26689
|
+
if (existsSync16(profileFile)) {
|
|
25616
26690
|
try {
|
|
25617
|
-
const config = JSON.parse(
|
|
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 =
|
|
25624
|
-
if (
|
|
26697
|
+
const profileDirConfig = join16(connectorConfigDir, "profiles", currentProfile, "config.json");
|
|
26698
|
+
if (existsSync16(profileDirConfig)) {
|
|
25625
26699
|
try {
|
|
25626
|
-
const config = JSON.parse(
|
|
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:
|
|
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();
|