@hasna/cloud 0.1.23 → 0.1.25
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/dist/cli/index.js +126 -8
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -12798,9 +12798,33 @@ import { existsSync as existsSync9, statSync as statSync6 } from "fs";
|
|
|
12798
12798
|
import { join as join9 } from "path";
|
|
12799
12799
|
import { homedir as homedir8 } from "os";
|
|
12800
12800
|
var program2 = new Command;
|
|
12801
|
+
function logSync(direction, service, rows, errors2) {
|
|
12802
|
+
try {
|
|
12803
|
+
const logDir = join9(homedir8(), ".hasna", "cloud");
|
|
12804
|
+
const logPath = join9(logDir, "sync.log");
|
|
12805
|
+
const { mkdirSync: mkdirSync6, appendFileSync } = __require("fs");
|
|
12806
|
+
mkdirSync6(logDir, { recursive: true });
|
|
12807
|
+
const ts = new Date().toISOString();
|
|
12808
|
+
appendFileSync(logPath, `${ts} ${direction.padEnd(4)} ${service.padEnd(20)} ${rows} rows, ${errors2} errors
|
|
12809
|
+
`);
|
|
12810
|
+
} catch {}
|
|
12811
|
+
}
|
|
12801
12812
|
program2.name("cloud").description("Shared cloud infrastructure \u2014 database adapter, sync engine, feedback, dotfile migration").version("0.1.8");
|
|
12802
|
-
program2.command("setup").description("Configure cloud settings").option("--host <host>", "RDS hostname").option("--port <port>", "RDS port", "5432").option("--username <user>", "RDS username").option("--password-env <env>", "Env var for RDS password", "HASNA_RDS_PASSWORD").option("--ssl", "Enable SSL", true).option("--no-ssl", "Disable SSL").option("--mode <mode>", "Mode: local, cloud, or hybrid", "
|
|
12813
|
+
program2.command("setup").description("Configure cloud settings \u2014 interactive wizard or flags").option("--host <host>", "RDS hostname").option("--port <port>", "RDS port", "5432").option("--username <user>", "RDS username").option("--password-env <env>", "Env var for RDS password", "HASNA_RDS_PASSWORD").option("--ssl", "Enable SSL", true).option("--no-ssl", "Disable SSL").option("--mode <mode>", "Mode: local, cloud, or hybrid").option("--schedule <interval>", "Sync schedule (e.g. 30m, 1h)").option("--migrate", "Run PG migrations after setup").option("--pull", "Pull data from cloud after setup").action(async (opts) => {
|
|
12803
12814
|
const config = getCloudConfig();
|
|
12815
|
+
const isAutoDetect = !opts.host && !opts.username;
|
|
12816
|
+
if (isAutoDetect) {
|
|
12817
|
+
const envHost = process.env.HASNA_RDS_HOST;
|
|
12818
|
+
const envUser = process.env.HASNA_RDS_USERNAME;
|
|
12819
|
+
if (envHost && !config.rds.host) {
|
|
12820
|
+
config.rds.host = envHost;
|
|
12821
|
+
console.log(`Auto-detected RDS host: ${envHost}`);
|
|
12822
|
+
}
|
|
12823
|
+
if (envUser && !config.rds.username) {
|
|
12824
|
+
config.rds.username = envUser;
|
|
12825
|
+
console.log(`Auto-detected RDS username: ${envUser}`);
|
|
12826
|
+
}
|
|
12827
|
+
}
|
|
12804
12828
|
if (opts.host)
|
|
12805
12829
|
config.rds.host = opts.host;
|
|
12806
12830
|
if (opts.port)
|
|
@@ -12810,13 +12834,86 @@ program2.command("setup").description("Configure cloud settings").option("--host
|
|
|
12810
12834
|
if (opts.passwordEnv)
|
|
12811
12835
|
config.rds.password_env = opts.passwordEnv;
|
|
12812
12836
|
config.rds.ssl = opts.ssl;
|
|
12813
|
-
if (opts.mode)
|
|
12837
|
+
if (opts.mode) {
|
|
12814
12838
|
config.mode = opts.mode;
|
|
12815
|
-
if (
|
|
12816
|
-
config.
|
|
12839
|
+
} else if (config.mode === "local" && config.rds.host) {
|
|
12840
|
+
config.mode = "hybrid";
|
|
12841
|
+
console.log("Mode set to: hybrid (auto-upgraded from local)");
|
|
12842
|
+
}
|
|
12817
12843
|
saveCloudConfig(config);
|
|
12818
|
-
console.log(
|
|
12819
|
-
|
|
12844
|
+
console.log(`
|
|
12845
|
+
\u2713 Configuration saved
|
|
12846
|
+
`);
|
|
12847
|
+
const password = process.env[config.rds.password_env];
|
|
12848
|
+
if (!password) {
|
|
12849
|
+
console.error(`\u2717 ${config.rds.password_env} not set in environment`);
|
|
12850
|
+
console.error(` Add it to ~/.secrets/hasna/rds/live.env and source it`);
|
|
12851
|
+
return;
|
|
12852
|
+
}
|
|
12853
|
+
if (config.rds.host) {
|
|
12854
|
+
process.stdout.write("Testing PG connection... ");
|
|
12855
|
+
try {
|
|
12856
|
+
const connStr = getConnectionString("postgres");
|
|
12857
|
+
const pg2 = new PgAdapterAsync2(connStr);
|
|
12858
|
+
await pg2.all("SELECT 1");
|
|
12859
|
+
await pg2.close();
|
|
12860
|
+
console.log(`\u2713 Connected
|
|
12861
|
+
`);
|
|
12862
|
+
} catch (err) {
|
|
12863
|
+
console.log(`\u2717 Failed: ${err?.message ?? String(err)}`);
|
|
12864
|
+
return;
|
|
12865
|
+
}
|
|
12866
|
+
if (opts.migrate !== false) {
|
|
12867
|
+
console.log("Creating databases & running migrations...");
|
|
12868
|
+
const dbResults = await ensureAllPgDatabases();
|
|
12869
|
+
const created = dbResults.filter((r) => r.created);
|
|
12870
|
+
if (created.length > 0) {
|
|
12871
|
+
console.log(` Created ${created.length} database(s): ${created.map((r) => r.service).join(", ")}`);
|
|
12872
|
+
}
|
|
12873
|
+
const migResults = await migrateAllServices();
|
|
12874
|
+
const applied = migResults.filter((r) => r.applied.length > 0);
|
|
12875
|
+
const totalApplied = migResults.reduce((s, r) => s + r.applied.length, 0);
|
|
12876
|
+
if (totalApplied > 0) {
|
|
12877
|
+
console.log(` Applied ${totalApplied} migration(s) across ${applied.length} service(s)`);
|
|
12878
|
+
} else {
|
|
12879
|
+
console.log(" All migrations up to date");
|
|
12880
|
+
}
|
|
12881
|
+
console.log("");
|
|
12882
|
+
}
|
|
12883
|
+
if (opts.schedule) {
|
|
12884
|
+
try {
|
|
12885
|
+
const minutes = parseInterval(opts.schedule);
|
|
12886
|
+
await registerSyncSchedule(minutes);
|
|
12887
|
+
console.log(`\u2713 Sync scheduled every ${minutes}m
|
|
12888
|
+
`);
|
|
12889
|
+
} catch (err) {
|
|
12890
|
+
console.error(`\u2717 Schedule failed: ${err?.message}`);
|
|
12891
|
+
}
|
|
12892
|
+
}
|
|
12893
|
+
if (opts.pull) {
|
|
12894
|
+
console.log("Pulling data from cloud...");
|
|
12895
|
+
const services = discoverServices();
|
|
12896
|
+
for (const service of services) {
|
|
12897
|
+
try {
|
|
12898
|
+
const dbPath = getDbPath2(service);
|
|
12899
|
+
const local = new SqliteAdapter2(dbPath);
|
|
12900
|
+
const connStr = getConnectionString(service);
|
|
12901
|
+
const cloud = new PgAdapterAsync2(connStr);
|
|
12902
|
+
const tables = (await listPgTables(cloud)).filter((t) => !isSyncExcludedTable(t));
|
|
12903
|
+
if (tables.length > 0) {
|
|
12904
|
+
const results = await syncPull(cloud, local, { tables });
|
|
12905
|
+
const written = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
12906
|
+
if (written > 0)
|
|
12907
|
+
console.log(` ${service}: ${written} rows`);
|
|
12908
|
+
}
|
|
12909
|
+
local.close();
|
|
12910
|
+
await cloud.close();
|
|
12911
|
+
} catch {}
|
|
12912
|
+
}
|
|
12913
|
+
console.log("");
|
|
12914
|
+
}
|
|
12915
|
+
}
|
|
12916
|
+
console.log("Setup complete. Run `cloud doctor` to verify everything.");
|
|
12820
12917
|
});
|
|
12821
12918
|
program2.command("status").description("Show current cloud configuration and connection health").action(async () => {
|
|
12822
12919
|
const config = getCloudConfig();
|
|
@@ -12843,7 +12940,7 @@ Checking PostgreSQL connection...`);
|
|
|
12843
12940
|
}
|
|
12844
12941
|
});
|
|
12845
12942
|
var syncCmd = program2.command("sync").description("Sync data between local and cloud");
|
|
12846
|
-
syncCmd.command("push").description("Push local data to cloud").option("--service <name>", "Service name").option("--all", "Push all discovered services").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
12943
|
+
syncCmd.command("push").description("Push local data to cloud").option("--service <name>", "Service name").option("--all", "Push all discovered services").option("--tables <tables>", "Comma-separated table names (default: all)").option("--dry-run", "Preview what would be synced without executing").action(async (opts) => {
|
|
12847
12944
|
const config = getCloudConfig();
|
|
12848
12945
|
if (config.mode === "local") {
|
|
12849
12946
|
console.error("Error: mode is 'local'. Run `cloud setup --mode hybrid` or `--mode cloud` first.");
|
|
@@ -12878,6 +12975,19 @@ syncCmd.command("push").description("Push local data to cloud").option("--servic
|
|
|
12878
12975
|
local.close();
|
|
12879
12976
|
continue;
|
|
12880
12977
|
}
|
|
12978
|
+
if (opts.dryRun) {
|
|
12979
|
+
const rowCounts = tables.map((t) => {
|
|
12980
|
+
try {
|
|
12981
|
+
const r = local.get(`SELECT COUNT(*) as cnt FROM "${t}"`);
|
|
12982
|
+
return `${t}: ${r?.cnt ?? 0} rows`;
|
|
12983
|
+
} catch {
|
|
12984
|
+
return `${t}: ?`;
|
|
12985
|
+
}
|
|
12986
|
+
});
|
|
12987
|
+
console.log(`[${service}] Would push ${tables.length} table(s): ${rowCounts.join(", ")}`);
|
|
12988
|
+
local.close();
|
|
12989
|
+
continue;
|
|
12990
|
+
}
|
|
12881
12991
|
console.log(`[${service}] Pushing ${tables.length} table(s) to cloud...`);
|
|
12882
12992
|
let connStr;
|
|
12883
12993
|
try {
|
|
@@ -12903,6 +13013,7 @@ syncCmd.command("push").description("Push local data to cloud").option("--servic
|
|
|
12903
13013
|
const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
|
|
12904
13014
|
grandTotalWritten += totalWritten;
|
|
12905
13015
|
grandTotalErrors += totalErrors;
|
|
13016
|
+
logSync("push", service, totalWritten, totalErrors);
|
|
12906
13017
|
if (opts.all) {
|
|
12907
13018
|
console.log(` ${service}: ${totalWritten} rows pushed${totalErrors > 0 ? `, ${totalErrors} errors` : ""}`);
|
|
12908
13019
|
} else {
|
|
@@ -12922,7 +13033,7 @@ Done. ${totalWritten} rows pushed, ${totalErrors} errors.`);
|
|
|
12922
13033
|
Done. ${services.length} services, ${grandTotalWritten} rows pushed, ${grandTotalErrors} errors.`);
|
|
12923
13034
|
}
|
|
12924
13035
|
});
|
|
12925
|
-
syncCmd.command("pull").description("Pull cloud data to local").option("--service <name>", "Service name").option("--all", "Pull all discovered services").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
13036
|
+
syncCmd.command("pull").description("Pull cloud data to local").option("--service <name>", "Service name").option("--all", "Pull all discovered services").option("--tables <tables>", "Comma-separated table names (default: all)").option("--dry-run", "Preview what would be synced without executing").action(async (opts) => {
|
|
12926
13037
|
const config = getCloudConfig();
|
|
12927
13038
|
if (config.mode === "local") {
|
|
12928
13039
|
console.error("Error: mode is 'local'. Run `cloud setup --mode hybrid` or `--mode cloud` first.");
|
|
@@ -12981,6 +13092,12 @@ syncCmd.command("pull").description("Pull cloud data to local").option("--servic
|
|
|
12981
13092
|
await cloud.close();
|
|
12982
13093
|
continue;
|
|
12983
13094
|
}
|
|
13095
|
+
if (opts.dryRun) {
|
|
13096
|
+
console.log(`[${service}] Would pull ${tables.length} table(s): ${tables.join(", ")}`);
|
|
13097
|
+
local.close();
|
|
13098
|
+
await cloud.close();
|
|
13099
|
+
continue;
|
|
13100
|
+
}
|
|
12984
13101
|
if (!opts.all)
|
|
12985
13102
|
console.log(`Pulling ${tables.length} table(s) from cloud...`);
|
|
12986
13103
|
const results = await syncPull(cloud, local, {
|
|
@@ -12997,6 +13114,7 @@ syncCmd.command("pull").description("Pull cloud data to local").option("--servic
|
|
|
12997
13114
|
const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
|
|
12998
13115
|
grandTotalWritten += totalWritten;
|
|
12999
13116
|
grandTotalErrors += totalErrors;
|
|
13117
|
+
logSync("pull", service, totalWritten, totalErrors);
|
|
13000
13118
|
if (opts.all) {
|
|
13001
13119
|
if (totalWritten > 0 || totalErrors > 0) {
|
|
13002
13120
|
console.log(` ${service}: ${totalWritten} rows pulled${totalErrors > 0 ? `, ${totalErrors} errors` : ""}`);
|
package/package.json
CHANGED