@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.
- package/dist/cli/index.js +104 -3
- 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