@envsync-cloud/deploy-cli 0.8.13 → 0.8.14

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/index.js +304 -63
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/index.ts
9
- import { createHash, randomBytes } from "crypto";
9
+ import { createHash, randomBytes, randomInt } from "crypto";
10
10
  import { spawnSync } from "child_process";
11
11
  import fs3 from "fs";
12
12
  import path3 from "path";
@@ -4400,7 +4400,6 @@ function buildRuntimeEnv2(config, generated) {
4400
4400
  const oss = isOssConfig(config);
4401
4401
  const enterprise = !oss;
4402
4402
  const license = config.license ?? {};
4403
- const accessProxy = config.access_proxy;
4404
4403
  return {
4405
4404
  NODE_ENV: "production",
4406
4405
  ENVSYNC_EDITION: oss ? "oss" : "enterprise",
@@ -4429,13 +4428,6 @@ function buildRuntimeEnv2(config, generated) {
4429
4428
  POSTGRES_USER: "postgres",
4430
4429
  POSTGRES_PASSWORD: "envsync-postgres",
4431
4430
  POSTGRES_DB: "envsync",
4432
- ENVSYNC_ACCESS_PROXY_ENABLED: `${accessProxy?.enabled ?? false}`,
4433
- ENVSYNC_ACCESS_PROXY_PROVIDER: accessProxy?.provider ?? "",
4434
- ENVSYNC_ACCESS_PROXY_DOMAIN: accessProxy?.domain ?? "",
4435
- ENVSYNC_ACCESS_PROXY_SERVICE_SCOPE: accessProxy?.service_scope ?? "all",
4436
- ENVSYNC_ACCESS_PROXY_SERVICES: Array.isArray(accessProxy?.services) ? accessProxy.services.join(",") : "",
4437
- ENVSYNC_ACCESS_PROXY_ADVERTISE_ROUTES: `${accessProxy?.advertise_routes ?? false}`,
4438
- ENVSYNC_ACCESS_PROXY_INCLUDE_ADMIN_SERVICES: `${accessProxy?.include_admin_services ?? false}`,
4439
4431
  S3_BUCKET: bucketName,
4440
4432
  S3_REGION: "us-east-1",
4441
4433
  S3_ACCESS_KEY: "envsync-rustfs",
@@ -4745,16 +4737,11 @@ function renderFrontendRuntimeConfig(config, generated) {
4745
4737
  const otelEndpoint = publicHttpsUrl(config, hosts.obs);
4746
4738
  const managementApiEnabled = !isOssConfig(config);
4747
4739
  const activeReleaseVersion = generated.deployment.slots[generated.deployment.active_slot].release_version || config.release.version;
4748
- const accessProxy = config.access_proxy;
4749
4740
  const managementApiUrl = managementApiEnabled ? publicHttpsUrl(config, hosts.manage_api) : "";
4750
4741
  return `window.__ENVSYNC_RUNTIME_CONFIG__ = ${JSON.stringify({
4751
4742
  apiBaseUrl: publicHttpsUrl(config, hosts.api),
4752
4743
  appBaseUrl: publicHttpsUrl(config, hosts.app),
4753
4744
  authBaseUrl: publicHttpsUrl(config, hosts.auth),
4754
- accessProxyEnabled: accessProxy?.enabled ?? false,
4755
- accessProxyDomain: accessProxy?.domain ?? "",
4756
- accessProxyServiceScope: accessProxy?.service_scope ?? "all",
4757
- accessProxyServices: accessProxy?.services ?? [],
4758
4745
  managementApiUrl,
4759
4746
  keycloakRealm: config.auth.keycloak_realm,
4760
4747
  webClientId: config.auth.web_client_id,
@@ -5169,6 +5156,7 @@ var BACKUPS_ROOT = path3.join(HOST_ROOT, "backups");
5169
5156
  var REPO_ROOT = process.env.ENVSYNC_REPO_ROOT ?? path3.join(HOST_ROOT, "repo");
5170
5157
  var DEPLOY_ENV = path3.join(ETC_ROOT, "deploy.env");
5171
5158
  var DEPLOY_YAML = path3.join(ETC_ROOT, "deploy.yaml");
5159
+ var WIREGUARD_ROOT = path3.join(ETC_ROOT, "wireguard");
5172
5160
  var LICENSE_ROOT = path3.join(ETC_ROOT, "license");
5173
5161
  var LICENSE_BUNDLE_FILE = path3.join(LICENSE_ROOT, "enterprise-license-bundle.json");
5174
5162
  var LICENSE_CERT_FILE = path3.join(LICENSE_ROOT, "enterprise-cert.pem");
@@ -5189,6 +5177,10 @@ var OTEL_AGENT_CONF = path3.join(DEPLOY_ROOT, "otel-agent.yaml");
5189
5177
  var CLICKSTACK_CLICKHOUSE_CONF = path3.join(DEPLOY_ROOT, "clickhouse-listen.xml");
5190
5178
  var INTERNAL_CONFIG_JSON = path3.join(DEPLOY_ROOT, "config.json");
5191
5179
  var UPGRADE_BACKUPS_ROOT = path3.join(BACKUPS_ROOT, "upgrade");
5180
+ var WIREGUARD_STATE_FILE = path3.join(WIREGUARD_ROOT, "state.json");
5181
+ var WIREGUARD_PRIVATE_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-private.key");
5182
+ var WIREGUARD_PUBLIC_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-public.key");
5183
+ var WIREGUARD_CONFIG_FILE = path3.join(WIREGUARD_ROOT, "envsync-wg.conf");
5192
5184
  var DEPLOY_RENDER_PATHS = {
5193
5185
  traefikStateRoot: TRAEFIK_STATE_ROOT,
5194
5186
  deployRoot: DEPLOY_ROOT,
@@ -5215,6 +5207,11 @@ var REMOVE_TARGETS = [
5215
5207
  LICENSE_ROOT_CA_FILE,
5216
5208
  INTERNAL_CONFIG_JSON,
5217
5209
  VERSIONS_LOCK,
5210
+ WIREGUARD_ROOT,
5211
+ WIREGUARD_STATE_FILE,
5212
+ WIREGUARD_PRIVATE_KEY_FILE,
5213
+ WIREGUARD_PUBLIC_KEY_FILE,
5214
+ WIREGUARD_CONFIG_FILE,
5218
5215
  STACK_FILE,
5219
5216
  BOOTSTRAP_BASE_STACK_FILE,
5220
5217
  BOOTSTRAP_STACK_FILE,
@@ -5340,6 +5337,28 @@ ${stderr}` : ""}`);
5340
5337
  }
5341
5338
  return result.stdout?.toString() ?? "";
5342
5339
  }
5340
+ function runCapture(cmd, args, opts = {}) {
5341
+ const result = spawnSync(cmd, args, {
5342
+ cwd: opts.cwd,
5343
+ env: { ...process.env, ...opts.env },
5344
+ input: opts.input,
5345
+ stdio: "pipe",
5346
+ encoding: "utf8"
5347
+ });
5348
+ if (result.status !== 0) {
5349
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
5350
+ throw new Error(`Command failed: ${cmd} ${args.join(" ")}${stderr ? `
5351
+ ${stderr}` : ""}`);
5352
+ }
5353
+ return (result.stdout ?? "").toString().trim();
5354
+ }
5355
+ function parseJsonOrDefault(value, fallback) {
5356
+ try {
5357
+ return JSON.parse(value);
5358
+ } catch {
5359
+ return fallback;
5360
+ }
5361
+ }
5343
5362
  function tryRun(cmd, args, opts = {}) {
5344
5363
  try {
5345
5364
  return run(cmd, args, opts);
@@ -5426,6 +5445,93 @@ function deterministicInstallFingerprint(rootDomain, stackName) {
5426
5445
  function randomStrongPassword() {
5427
5446
  return `EnvSync!${randomBytes(8).toString("hex")}Aa1`;
5428
5447
  }
5448
+ function randomWireguardPort() {
5449
+ return randomInt(5e4, 65535);
5450
+ }
5451
+ function loadWireguardState() {
5452
+ if (!exists2(WIREGUARD_STATE_FILE)) return null;
5453
+ const raw = JSON.parse(fs3.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
5454
+ return {
5455
+ private_key: raw.private_key,
5456
+ public_key: raw.public_key,
5457
+ listen_port: raw.listen_port,
5458
+ network: raw.network,
5459
+ server_ip: raw.server_ip,
5460
+ peers: Array.isArray(raw.peers) ? raw.peers : []
5461
+ };
5462
+ }
5463
+ function saveWireguardState(state) {
5464
+ if (currentOptions.dryRun) {
5465
+ logDryRun(`Would save WireGuard state to ${WIREGUARD_STATE_FILE}`);
5466
+ return;
5467
+ }
5468
+ writeFile(WIREGUARD_STATE_FILE, JSON.stringify(state, null, 2), 384);
5469
+ }
5470
+ function ensureWireguardState() {
5471
+ ensureDir(WIREGUARD_ROOT);
5472
+ let state = loadWireguardState();
5473
+ if (state) {
5474
+ state.private_key = state.private_key?.trim();
5475
+ state.public_key = state.public_key?.trim();
5476
+ state.network = state.network || "10.66.0.0/24";
5477
+ state.server_ip = state.server_ip || "10.66.0.1";
5478
+ state.listen_port = state.listen_port || randomWireguardPort();
5479
+ state.peers = Array.isArray(state.peers) ? state.peers : [];
5480
+ if (state.private_key && !state.public_key) {
5481
+ state.public_key = runCapture("wg", ["pubkey"], { input: `${state.private_key}
5482
+ ` });
5483
+ }
5484
+ if (!state.private_key || !state.public_key) {
5485
+ state = null;
5486
+ }
5487
+ }
5488
+ if (!state) {
5489
+ const privateKey = runCapture("wg", ["genkey"]);
5490
+ const publicKey = runCapture("wg", ["pubkey"], { input: `${privateKey}
5491
+ ` });
5492
+ state = {
5493
+ private_key: privateKey,
5494
+ public_key: publicKey,
5495
+ listen_port: randomWireguardPort(),
5496
+ network: "10.66.0.0/24",
5497
+ server_ip: "10.66.0.1",
5498
+ peers: []
5499
+ };
5500
+ saveWireguardState(state);
5501
+ }
5502
+ writeFileMaybe(WIREGUARD_PRIVATE_KEY_FILE, `${state.private_key}
5503
+ `, 384);
5504
+ writeFileMaybe(WIREGUARD_PUBLIC_KEY_FILE, `${state.public_key}
5505
+ `);
5506
+ return state;
5507
+ }
5508
+ function wireguardPeerAllowedIp(state) {
5509
+ const usedIps = new Set(state.peers.map((peer) => peer.allowed_ip));
5510
+ for (let i = 2; i <= 254; i++) {
5511
+ const candidate = `${state.server_ip.split(".").slice(0, 3).join(".")}.${i}/32`;
5512
+ if (!usedIps.has(candidate)) {
5513
+ return candidate;
5514
+ }
5515
+ }
5516
+ throw new Error("WireGuard client IP pool exhausted in subnet 10.66.0.0/24.");
5517
+ }
5518
+ function writeWireguardConfig(state) {
5519
+ const peersSection = state.peers.map((peer) => `
5520
+ [Peer]
5521
+ PublicKey = ${peer.public_key}
5522
+ AllowedIPs = ${peer.allowed_ip}`).join("\n");
5523
+ const content = `[Interface]
5524
+ Address = ${state.server_ip}/24
5525
+ ListenPort = ${state.listen_port}
5526
+ PrivateKey = ${state.private_key}
5527
+ PostUp = iptables -A FORWARD -i envsync-wg -j ACCEPT
5528
+ PostDown = iptables -D FORWARD -i envsync-wg -j ACCEPT
5529
+ PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
5530
+ PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
5531
+ ${peersSection}
5532
+ `;
5533
+ writeFileMaybe(WIREGUARD_CONFIG_FILE, content);
5534
+ }
5429
5535
  function emptyApiSlotState() {
5430
5536
  return {
5431
5537
  api_image: "",
@@ -5475,12 +5581,6 @@ function parseEnvFile(content) {
5475
5581
  }
5476
5582
  return out;
5477
5583
  }
5478
- function asBool(value) {
5479
- return value.trim().toLowerCase() === "true" || value.trim().toLowerCase() === "y" || value.trim().toLowerCase() === "yes";
5480
- }
5481
- function asStringList(value) {
5482
- return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
5483
- }
5484
5584
  async function ask(question, fallback = "") {
5485
5585
  if (!process.stdin.isTTY) return fallback;
5486
5586
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -5567,6 +5667,9 @@ function renderHelpBlock() {
5567
5667
  " validate-topology Validate Enterprise edition topology rules",
5568
5668
  " upgrade [version] Pin a target release and deploy it",
5569
5669
  " upgrade-deps Refresh dependency images and redeploy",
5670
+ " vpn pubkey Generate or print WireGuard server public key",
5671
+ " vpn whitelist <key> Add a client public key to allowed peers",
5672
+ " vpn status Show WireGuard server/peer status",
5570
5673
  " license issue-cert Issue and install an Enterprise certificate bundle",
5571
5674
  " license renew-cert Renew and install the Enterprise certificate bundle",
5572
5675
  " license validate-cert Validate the installed Enterprise certificate bundle files",
@@ -5822,30 +5925,6 @@ function normalizeConfig(raw) {
5822
5925
  } : raw.edition === "oss" ? void 0 : {
5823
5926
  install_fingerprint: deterministicInstallFingerprint(rootDomain, stackName),
5824
5927
  certificate_validity_days: 1095
5825
- },
5826
- access_proxy: {
5827
- enabled: raw.access_proxy?.enabled ?? false,
5828
- provider: "tsdproxy",
5829
- auth_token: raw.access_proxy?.auth_token ?? "",
5830
- domain: raw.access_proxy?.domain ?? `tsdproxy.${rootDomain}`,
5831
- service_scope: raw.access_proxy?.service_scope === "selected" ? "selected" : "all",
5832
- services: Array.isArray(raw.access_proxy?.services) ? raw.access_proxy.services.filter((s) => typeof s === "string" && s.trim().length > 0) : [
5833
- "api",
5834
- "web",
5835
- "landing",
5836
- "keycloak",
5837
- "openfga",
5838
- "postgre*",
5839
- "redis",
5840
- "rustfs",
5841
- "clickstack",
5842
- "obs",
5843
- "envsync_api_*",
5844
- "envsync_management-api",
5845
- "envsync-api"
5846
- ],
5847
- advertise_routes: raw.access_proxy?.advertise_routes ?? false,
5848
- include_admin_services: raw.access_proxy?.include_admin_services ?? false
5849
5928
  }
5850
5929
  };
5851
5930
  }
@@ -7222,7 +7301,19 @@ async function cmdPreinstall() {
7222
7301
  ensureDir(TRAEFIK_STATE_ROOT);
7223
7302
  run("bash", ["-lc", "command -v apt-get >/dev/null"]);
7224
7303
  run("sudo", ["apt-get", "update"]);
7225
- run("sudo", ["apt-get", "install", "-y", "docker.io", "docker-compose-v2", "git", "curl", "jq", "openssl", "tar"]);
7304
+ run("sudo", [
7305
+ "apt-get",
7306
+ "install",
7307
+ "-y",
7308
+ "docker.io",
7309
+ "docker-compose-v2",
7310
+ "git",
7311
+ "curl",
7312
+ "jq",
7313
+ "openssl",
7314
+ "tar",
7315
+ "wireguard-tools"
7316
+ ]);
7226
7317
  run("sudo", ["systemctl", "enable", "--now", "docker"]);
7227
7318
  try {
7228
7319
  run("docker", ["swarm", "init"]);
@@ -7255,14 +7346,6 @@ async function cmdSetup() {
7255
7346
  const licenseKey = licenseServerUrl ? await ask("Enterprise license key", "") : "";
7256
7347
  const certificateBundleFile = licenseServerUrl ? "" : await ask("Enterprise certificate bundle file (optional)", "");
7257
7348
  const installFingerprint = deterministicInstallFingerprint(rootDomain, "envsync");
7258
- const enableAccessProxy = asBool(await ask("Enable private access proxy for container services (y/N)", "n"));
7259
- const accessProxyProvider = enableAccessProxy ? await ask("Private access proxy provider", "tsdproxy") : "tsdproxy";
7260
- const accessProxyAuthToken = enableAccessProxy ? await ask("Proxy auth token or API key", "") : "";
7261
- const accessProxyDomain = enableAccessProxy ? await ask("Private access domain", `tsdproxy.${rootDomain}`) : `tsdproxy.${rootDomain}`;
7262
- const accessProxyExposeAll = enableAccessProxy ? asBool(await ask("Expose all services through proxy (Y/n)", "y")) : true;
7263
- const accessProxyServices = !accessProxyExposeAll ? asStringList(await ask("Service hostnames to expose (comma-separated)", "postgres,api,web,landing,keycloak,minikms,openfga,rustfs,clickstack,obs,manage-api")) : [];
7264
- const includeAdminServices = enableAccessProxy ? asBool(await ask("Expose admin/maintenance services (y/N)", "n")) : false;
7265
- const accessProxyAdvertiseRoutes = enableAccessProxy ? asBool(await ask("Advertise overlay subnet routes through the proxy provider (y/N)", "n")) : false;
7266
7349
  const config = {
7267
7350
  edition: "enterprise",
7268
7351
  source: defaultSourceConfig(releaseVersion),
@@ -7335,16 +7418,6 @@ async function cmdSetup() {
7335
7418
  install_fingerprint: installFingerprint,
7336
7419
  certificate_bundle_file: certificateBundleFile || void 0,
7337
7420
  certificate_validity_days: 1095
7338
- },
7339
- access_proxy: {
7340
- enabled: enableAccessProxy,
7341
- provider: accessProxyProvider,
7342
- auth_token: accessProxyAuthToken,
7343
- domain: accessProxyDomain,
7344
- service_scope: accessProxyExposeAll ? "all" : "selected",
7345
- services: accessProxyServices,
7346
- advertise_routes: accessProxyAdvertiseRoutes,
7347
- include_admin_services: includeAdminServices
7348
7421
  }
7349
7422
  };
7350
7423
  saveDesiredConfig(config);
@@ -8226,6 +8299,171 @@ async function cmdRestore(archivePath, autoDeploy = false) {
8226
8299
  }
8227
8300
  logInfo("Next: envsync-deploy deploy");
8228
8301
  }
8302
+ async function cmdVpn(args) {
8303
+ const action = args[0];
8304
+ switch (action) {
8305
+ case "pubkey":
8306
+ await cmdVpnPubkey();
8307
+ break;
8308
+ case "whitelist":
8309
+ await cmdVpnWhitelist(args[1]);
8310
+ break;
8311
+ case "status":
8312
+ await cmdVpnStatus();
8313
+ break;
8314
+ default:
8315
+ throw new Error("Usage: envsync-deploy vpn <pubkey|whitelist|status>");
8316
+ }
8317
+ }
8318
+ async function cmdVpnPubkey() {
8319
+ logSection("WireGuard VPN");
8320
+ const state = ensureWireguardState();
8321
+ writeWireguardConfig(state);
8322
+ logInfo(`WireGuard interface: envsync-wg`);
8323
+ logInfo(`Server config: ${WIREGUARD_CONFIG_FILE}`);
8324
+ logInfo(`Private key: ${WIREGUARD_PRIVATE_KEY_FILE}`);
8325
+ logInfo(`Public key: ${WIREGUARD_PUBLIC_KEY_FILE}`);
8326
+ logInfo(`Server listen port: ${state.listen_port}`);
8327
+ logInfo(`Network: ${state.network}`);
8328
+ console.log("");
8329
+ logSuccess(`Server public key: ${state.public_key}`);
8330
+ }
8331
+ async function cmdVpnWhitelist(clientPublicKeyArg) {
8332
+ if (!clientPublicKeyArg) {
8333
+ throw new Error("usage: envsync-deploy vpn whitelist <client_public_key>");
8334
+ }
8335
+ const clientPublicKey = clientPublicKeyArg.trim();
8336
+ if (!clientPublicKey) {
8337
+ throw new Error("client public key cannot be empty");
8338
+ }
8339
+ const state = ensureWireguardState();
8340
+ const existing = state.peers.find((peer) => peer.public_key === clientPublicKey);
8341
+ if (existing) {
8342
+ logWarn(`Client key already whitelisted for ${existing.allowed_ip}`);
8343
+ return;
8344
+ }
8345
+ const allowedIp = wireguardPeerAllowedIp(state);
8346
+ state.peers.push({
8347
+ public_key: clientPublicKey,
8348
+ allowed_ip: allowedIp,
8349
+ added_at: (/* @__PURE__ */ new Date()).toISOString()
8350
+ });
8351
+ state.peers.sort((a, b) => a.public_key.localeCompare(b.public_key));
8352
+ saveWireguardState(state);
8353
+ writeWireguardConfig(state);
8354
+ logSuccess(`Whitelisted client ${clientPublicKey}`);
8355
+ console.log(`AllowedIP: ${allowedIp}`);
8356
+ console.log(`Append to client config:`);
8357
+ console.log(`[Peer]`);
8358
+ console.log(`PublicKey = ${clientPublicKey}`);
8359
+ console.log(`AllowedIPs = ${allowedIp}`);
8360
+ }
8361
+ function getVpnStackName() {
8362
+ try {
8363
+ return loadConfig().services.stack_name;
8364
+ } catch {
8365
+ return "envsync";
8366
+ }
8367
+ }
8368
+ function listStackServiceNames(stackName) {
8369
+ const raw = tryRun("docker", ["service", "ls", "--filter", `label=com.docker.stack.namespace=${stackName}`, "--format", "{{.Name}}"], {
8370
+ quiet: true
8371
+ });
8372
+ if (!raw.trim()) return [];
8373
+ return raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8374
+ }
8375
+ function listServicePublishedPorts(serviceName) {
8376
+ const raw = tryRun("docker", ["service", "inspect", "--format", "{{json .Endpoint.Ports}}", serviceName], { quiet: true });
8377
+ const ports = parseJsonOrDefault(raw, []);
8378
+ const list = Array.isArray(ports) ? ports : [];
8379
+ if (list.length === 0) return [];
8380
+ return list.map((port) => {
8381
+ const typed = port;
8382
+ if (!typed || typeof typed !== "object") return "";
8383
+ if (!typed.PublishedPort || !typed.TargetPort || !typed.Protocol) return "";
8384
+ const mode = typed.PublishMode || "ingress";
8385
+ return `${typed.PublishedPort}/${typed.Protocol} -> ${typed.TargetPort}/${typed.Protocol} (${mode})`;
8386
+ }).filter(Boolean);
8387
+ }
8388
+ function listRunningContainerEndpoints(serviceName) {
8389
+ const raw = tryRun(
8390
+ "docker",
8391
+ ["service", "ps", serviceName, "--filter", "desired-state=running", "--no-trunc", "--format", "{{.ID}}"],
8392
+ { quiet: true }
8393
+ );
8394
+ const taskIds = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8395
+ if (taskIds.length === 0) return [];
8396
+ const results = [];
8397
+ for (const taskId of taskIds) {
8398
+ const taskRaw = tryRun("docker", ["inspect", taskId, "--format", "{{json .}}"], { quiet: true });
8399
+ if (!taskRaw) continue;
8400
+ const inspections = parseJsonOrDefault(taskRaw, []);
8401
+ if (!Array.isArray(inspections) || inspections.length === 0) continue;
8402
+ const inspect = inspections[0];
8403
+ const containerId = inspect.Status?.ContainerStatus?.ContainerID ?? "";
8404
+ const addresses = Array.isArray(inspect.NetworksAttachments) ? inspect.NetworksAttachments.flatMap((attachment) => {
8405
+ const networkName = attachment.Network?.Spec?.Name ?? "overlay";
8406
+ const ips = Array.isArray(attachment.Addresses) ? attachment.Addresses : [];
8407
+ return ips.map((ip) => {
8408
+ const trimmed = typeof ip === "string" ? ip.trim() : "";
8409
+ return trimmed ? `${networkName}:${trimmed}` : "";
8410
+ }).filter(Boolean);
8411
+ }) : [];
8412
+ const line = `task ${taskId.slice(0, 12)} container=${containerId.slice(0, 12) || "unknown"} ${addresses.length ? `ips=${addresses.join(", ")}` : "ips=none"}`;
8413
+ results.push(line);
8414
+ }
8415
+ return results;
8416
+ }
8417
+ async function cmdVpnStatus() {
8418
+ logSection("WireGuard VPN");
8419
+ const state = loadWireguardState();
8420
+ if (!state) {
8421
+ logWarn("WireGuard state not initialized.");
8422
+ logInfo("Run: envsync-deploy vpn pubkey");
8423
+ return;
8424
+ }
8425
+ logInfo(`Config file: ${WIREGUARD_CONFIG_FILE}`);
8426
+ logInfo(`State file: ${WIREGUARD_STATE_FILE}`);
8427
+ logInfo(`Server endpoint: 0.0.0.0:${state.listen_port}`);
8428
+ logInfo(`Network: ${state.network}`);
8429
+ logInfo(`Server IP: ${state.server_ip}`);
8430
+ if (state.peers.length === 0) {
8431
+ logInfo("Peers: none");
8432
+ } else {
8433
+ logInfo("Peers:");
8434
+ for (const peer of state.peers) {
8435
+ console.log(` ${peer.public_key} -> ${peer.allowed_ip}`);
8436
+ }
8437
+ }
8438
+ console.log("");
8439
+ logInfo("Container endpoints:");
8440
+ const stackName = getVpnStackName();
8441
+ const services = listStackServiceNames(stackName);
8442
+ if (services.length === 0) {
8443
+ logWarn(`No running services found for stack ${stackName}`);
8444
+ } else {
8445
+ for (const service of services) {
8446
+ const ports = listServicePublishedPorts(service);
8447
+ const endpoints = listRunningContainerEndpoints(service);
8448
+ console.log(` ${service}`);
8449
+ console.log(` Published ports: ${ports.length ? ports.join(", ") : "none"}`);
8450
+ if (endpoints.length === 0) {
8451
+ console.log(" Containers: none running");
8452
+ } else {
8453
+ for (const endpoint of endpoints) {
8454
+ console.log(` ${endpoint}`);
8455
+ }
8456
+ }
8457
+ }
8458
+ }
8459
+ try {
8460
+ const ifaceStatus = runCapture("wg", ["show", "envsync-wg"]);
8461
+ console.log("Runtime:");
8462
+ console.log(ifaceStatus);
8463
+ } catch {
8464
+ logWarn("WireGuard interface is not running in kernel yet. Run wg-quick up on the generated config to activate.");
8465
+ }
8466
+ }
8229
8467
  async function main() {
8230
8468
  const argv = process.argv.slice(2);
8231
8469
  const command = argv[0];
@@ -8273,6 +8511,9 @@ async function main() {
8273
8511
  case "upgrade-deps":
8274
8512
  await cmdUpgradeDeps();
8275
8513
  break;
8514
+ case "vpn":
8515
+ await cmdVpn(positionals);
8516
+ break;
8276
8517
  case "license":
8277
8518
  await cmdLicense(positionals[0]);
8278
8519
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.8.13",
3
+ "version": "0.8.14",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {