@hasna/cloud 0.1.22 → 0.1.24

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 +104 -3
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -12794,7 +12794,21 @@ async function ensureAllPgDatabases() {
12794
12794
  }
12795
12795
 
12796
12796
  // src/cli/index.ts
12797
+ import { existsSync as existsSync9, statSync as statSync6 } from "fs";
12798
+ import { join as join9 } from "path";
12799
+ import { homedir as homedir8 } from "os";
12797
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
+ }
12798
12812
  program2.name("cloud").description("Shared cloud infrastructure \u2014 database adapter, sync engine, feedback, dotfile migration").version("0.1.8");
12799
12813
  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) => {
12800
12814
  const config = getCloudConfig();
@@ -12840,7 +12854,7 @@ Checking PostgreSQL connection...`);
12840
12854
  }
12841
12855
  });
12842
12856
  var syncCmd = program2.command("sync").description("Sync data between local and cloud");
12843
- 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) => {
12857
+ 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) => {
12844
12858
  const config = getCloudConfig();
12845
12859
  if (config.mode === "local") {
12846
12860
  console.error("Error: mode is 'local'. Run `cloud setup --mode hybrid` or `--mode cloud` first.");
@@ -12875,6 +12889,19 @@ syncCmd.command("push").description("Push local data to cloud").option("--servic
12875
12889
  local.close();
12876
12890
  continue;
12877
12891
  }
12892
+ if (opts.dryRun) {
12893
+ const rowCounts = tables.map((t) => {
12894
+ try {
12895
+ const r = local.get(`SELECT COUNT(*) as cnt FROM "${t}"`);
12896
+ return `${t}: ${r?.cnt ?? 0} rows`;
12897
+ } catch {
12898
+ return `${t}: ?`;
12899
+ }
12900
+ });
12901
+ console.log(`[${service}] Would push ${tables.length} table(s): ${rowCounts.join(", ")}`);
12902
+ local.close();
12903
+ continue;
12904
+ }
12878
12905
  console.log(`[${service}] Pushing ${tables.length} table(s) to cloud...`);
12879
12906
  let connStr;
12880
12907
  try {
@@ -12900,6 +12927,7 @@ syncCmd.command("push").description("Push local data to cloud").option("--servic
12900
12927
  const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
12901
12928
  grandTotalWritten += totalWritten;
12902
12929
  grandTotalErrors += totalErrors;
12930
+ logSync("push", service, totalWritten, totalErrors);
12903
12931
  if (opts.all) {
12904
12932
  console.log(` ${service}: ${totalWritten} rows pushed${totalErrors > 0 ? `, ${totalErrors} errors` : ""}`);
12905
12933
  } else {
@@ -12919,7 +12947,7 @@ Done. ${totalWritten} rows pushed, ${totalErrors} errors.`);
12919
12947
  Done. ${services.length} services, ${grandTotalWritten} rows pushed, ${grandTotalErrors} errors.`);
12920
12948
  }
12921
12949
  });
12922
- 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) => {
12950
+ 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) => {
12923
12951
  const config = getCloudConfig();
12924
12952
  if (config.mode === "local") {
12925
12953
  console.error("Error: mode is 'local'. Run `cloud setup --mode hybrid` or `--mode cloud` first.");
@@ -12978,6 +13006,12 @@ syncCmd.command("pull").description("Pull cloud data to local").option("--servic
12978
13006
  await cloud.close();
12979
13007
  continue;
12980
13008
  }
13009
+ if (opts.dryRun) {
13010
+ console.log(`[${service}] Would pull ${tables.length} table(s): ${tables.join(", ")}`);
13011
+ local.close();
13012
+ await cloud.close();
13013
+ continue;
13014
+ }
12981
13015
  if (!opts.all)
12982
13016
  console.log(`Pulling ${tables.length} table(s) from cloud...`);
12983
13017
  const results = await syncPull(cloud, local, {
@@ -12994,6 +13028,7 @@ syncCmd.command("pull").description("Pull cloud data to local").option("--servic
12994
13028
  const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
12995
13029
  grandTotalWritten += totalWritten;
12996
13030
  grandTotalErrors += totalErrors;
13031
+ logSync("pull", service, totalWritten, totalErrors);
12997
13032
  if (opts.all) {
12998
13033
  if (totalWritten > 0 || totalErrors > 0) {
12999
13034
  console.log(` ${service}: ${totalWritten} rows pulled${totalErrors > 0 ? `, ${totalErrors} errors` : ""}`);
@@ -13068,7 +13103,6 @@ Done. ${results.length} services, ${totalApplied} migrations applied, ${totalErr
13068
13103
  syncCmd.command("status").description("Show sync status for all discovered services").option("--service <name>", "Show status for a single service").option("--json", "Output as JSON").action(async (opts) => {
13069
13104
  const services = opts.service ? [opts.service] : discoverServices();
13070
13105
  const statuses = [];
13071
- const { existsSync: existsSync9, statSync: statSync6 } = __require("fs");
13072
13106
  for (const service of services) {
13073
13107
  const dbPath = getDbPath2(service);
13074
13108
  const localExists = existsSync9(dbPath);
@@ -13218,4 +13252,71 @@ program2.command("migrate").description("Migrate legacy dotfiles to ~/.hasna/").
13218
13252
  }
13219
13253
  }
13220
13254
  });
13255
+ program2.command("doctor").description("Comprehensive health check for cloud sync setup").action(async () => {
13256
+ const checks = [];
13257
+ const configPath = join9(homedir8(), ".hasna", "cloud", "config.json");
13258
+ if (existsSync9(configPath)) {
13259
+ checks.push({ name: "Config file", status: "pass", detail: configPath });
13260
+ } else {
13261
+ checks.push({ name: "Config file", status: "fail", detail: "Missing. Run `cloud setup`." });
13262
+ }
13263
+ const config = getCloudConfig();
13264
+ if (config.mode === "hybrid" || config.mode === "cloud") {
13265
+ checks.push({ name: "Sync mode", status: "pass", detail: config.mode });
13266
+ } else {
13267
+ checks.push({ name: "Sync mode", status: "fail", detail: `"${config.mode}" \u2014 sync disabled. Run \`cloud setup --mode hybrid\`.` });
13268
+ }
13269
+ if (config.rds.host) {
13270
+ checks.push({ name: "RDS host", status: "pass", detail: config.rds.host });
13271
+ } else {
13272
+ checks.push({ name: "RDS host", status: "fail", detail: "Not configured. Run `cloud setup`." });
13273
+ }
13274
+ const password = process.env[config.rds.password_env];
13275
+ if (password) {
13276
+ checks.push({ name: "RDS password", status: "pass", detail: `${config.rds.password_env} is set` });
13277
+ } else {
13278
+ checks.push({ name: "RDS password", status: "fail", detail: `${config.rds.password_env} not in environment. Add to ~/.secrets/hasna/rds/live.env` });
13279
+ }
13280
+ if (config.rds.host && password) {
13281
+ try {
13282
+ const connStr = getConnectionString("postgres");
13283
+ const pg2 = new PgAdapterAsync2(connStr);
13284
+ await pg2.all("SELECT 1");
13285
+ await pg2.close();
13286
+ checks.push({ name: "PG connection", status: "pass", detail: "Connected" });
13287
+ } catch (err) {
13288
+ checks.push({ name: "PG connection", status: "fail", detail: err?.message ?? String(err) });
13289
+ }
13290
+ } else {
13291
+ checks.push({ name: "PG connection", status: "fail", detail: "Skipped \u2014 missing host or password" });
13292
+ }
13293
+ const caPath = process.env.NODE_EXTRA_CA_CERTS;
13294
+ if (caPath && existsSync9(caPath)) {
13295
+ checks.push({ name: "SSL CA cert", status: "pass", detail: caPath });
13296
+ } else if (caPath) {
13297
+ checks.push({ name: "SSL CA cert", status: "warn", detail: `NODE_EXTRA_CA_CERTS set but file missing: ${caPath}` });
13298
+ } else {
13299
+ checks.push({ name: "SSL CA cert", status: "warn", detail: "NODE_EXTRA_CA_CERTS not set. May cause SSL errors on some systems." });
13300
+ }
13301
+ const services = discoverServices();
13302
+ checks.push({ name: "Local services", status: services.length > 0 ? "pass" : "warn", detail: `${services.length} found in ~/.hasna/` });
13303
+ const schedule = getSyncScheduleStatus();
13304
+ if (schedule.registered) {
13305
+ checks.push({ name: "Sync schedule", status: "pass", detail: `Every ${schedule.schedule_minutes}m (${schedule.mechanism})` });
13306
+ } else {
13307
+ checks.push({ name: "Sync schedule", status: "warn", detail: "Not configured. Run `cloud sync schedule --every 30m`." });
13308
+ }
13309
+ console.log(`Cloud Doctor
13310
+ `);
13311
+ for (const c of checks) {
13312
+ const icon = c.status === "pass" ? "\u2713" : c.status === "fail" ? "\u2717" : "\u26A0";
13313
+ console.log(` ${icon} ${c.name.padEnd(20)} ${c.detail}`);
13314
+ }
13315
+ const fails = checks.filter((c) => c.status === "fail").length;
13316
+ const warns = checks.filter((c) => c.status === "warn").length;
13317
+ console.log(`
13318
+ ${checks.length} checks: ${checks.length - fails - warns} passed, ${warns} warnings, ${fails} failed`);
13319
+ if (fails > 0)
13320
+ process.exit(1);
13321
+ });
13221
13322
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/cloud",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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",