@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.
Files changed (2) hide show
  1. package/dist/cli/index.js +126 -8
  2. 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", "local").option("--sync-interval <minutes>", "Auto-sync interval in minutes", "0").action((opts) => {
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 (opts.syncInterval)
12816
- config.auto_sync_interval_minutes = parseInt(opts.syncInterval, 10);
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("Cloud configuration saved.");
12819
- console.log(JSON.stringify(config, null, 2));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/cloud",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Shared cloud infrastructure — database adapter (SQLite + PostgreSQL), sync engine, feedback system, unified dotfile config",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",