@envsync-cloud/deploy-cli 0.6.4 → 0.6.6

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 (3) hide show
  1. package/README.md +5 -2
  2. package/dist/index.js +151 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -40,7 +40,7 @@ bunx @envsync-cloud/deploy-cli <command>
40
40
  ```text
41
41
  envsync-deploy preinstall
42
42
  envsync-deploy setup
43
- envsync-deploy bootstrap [--dry-run]
43
+ envsync-deploy bootstrap [--dry-run] [--force]
44
44
  envsync-deploy deploy [--dry-run]
45
45
  envsync-deploy health [--json]
46
46
  envsync-deploy upgrade [--dry-run]
@@ -71,6 +71,8 @@ Bootstrap infra, migrations, RustFS, and OpenFGA:
71
71
  npx @envsync-cloud/deploy-cli bootstrap
72
72
  ```
73
73
 
74
+ `bootstrap` is destructive. It removes the existing EnvSync stack, matching containers, network, and managed volumes before rebuilding, and requires typing `yes` to continue. Use `--force` to bypass the prompt in automation or other non-interactive environments.
75
+
74
76
  Deploy the pending API and frontend services:
75
77
 
76
78
  ```bash
@@ -79,7 +81,7 @@ npx @envsync-cloud/deploy-cli deploy
79
81
 
80
82
  The staged flow is:
81
83
  - `setup` writes desired config
82
- - `bootstrap` starts base infra, runs OpenFGA and miniKMS migrations, starts runtime infra, and persists generated runtime env state
84
+ - `bootstrap` resets the existing EnvSync deployment, then starts base infra, runs OpenFGA and miniKMS migrations, starts runtime infra, and persists generated runtime env state
83
85
  - `deploy` starts the pending API and frontend services
84
86
 
85
87
  Check service health:
@@ -104,6 +106,7 @@ Preview mutating commands without changing the host:
104
106
 
105
107
  ```bash
106
108
  npx @envsync-cloud/deploy-cli bootstrap --dry-run
109
+ npx @envsync-cloud/deploy-cli bootstrap --force
107
110
  npx @envsync-cloud/deploy-cli deploy --dry-run
108
111
  ```
109
112
 
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ var REQUIRED_BOOTSTRAP_ENV_KEYS = [
48
48
  "OPENFGA_MODEL_ID"
49
49
  ];
50
50
  var SEMVER_VERSION_RE = /^\d+\.\d+\.\d+$/;
51
- var currentOptions = { dryRun: false };
51
+ var currentOptions = { dryRun: false, force: false };
52
52
  function formatShellArg(arg) {
53
53
  if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) return arg;
54
54
  return JSON.stringify(arg);
@@ -69,6 +69,9 @@ function logInfo(message) {
69
69
  function logSuccess(message) {
70
70
  console.log(`${chalk.green("[ok]")} ${message}`);
71
71
  }
72
+ function logWarn(message) {
73
+ console.log(`${chalk.yellow("[warn]")} ${message}`);
74
+ }
72
75
  function logDryRun(message) {
73
76
  console.log(`${chalk.magenta("[dry-run]")} ${message}`);
74
77
  }
@@ -216,6 +219,18 @@ async function ask(question, fallback = "") {
216
219
  });
217
220
  });
218
221
  }
222
+ async function askRequired(question) {
223
+ if (!process.stdin.isTTY) {
224
+ throw new Error("Bootstrap confirmation requires an interactive terminal. Re-run with --force to bypass the prompt.");
225
+ }
226
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
227
+ return await new Promise((resolve) => {
228
+ rl.question(`${question} `, (answer) => {
229
+ rl.close();
230
+ resolve(answer.trim());
231
+ });
232
+ });
233
+ }
219
234
  function sleepSeconds(seconds) {
220
235
  spawnSync("sleep", [`${seconds}`], { stdio: "ignore" });
221
236
  }
@@ -364,6 +379,7 @@ function emptyGeneratedState() {
364
379
  },
365
380
  secrets: {
366
381
  s3_secret_key: "",
382
+ keycloak_db_password: "",
367
383
  keycloak_web_client_secret: "",
368
384
  keycloak_api_client_secret: "",
369
385
  openfga_db_password: "",
@@ -384,6 +400,7 @@ function normalizeGeneratedState(raw) {
384
400
  },
385
401
  secrets: {
386
402
  s3_secret_key: raw?.secrets?.s3_secret_key ?? defaults.secrets.s3_secret_key,
403
+ keycloak_db_password: raw?.secrets?.keycloak_db_password ?? defaults.secrets.keycloak_db_password,
387
404
  keycloak_web_client_secret: raw?.secrets?.keycloak_web_client_secret ?? defaults.secrets.keycloak_web_client_secret,
388
405
  keycloak_api_client_secret: raw?.secrets?.keycloak_api_client_secret ?? defaults.secrets.keycloak_api_client_secret,
389
406
  openfga_db_password: raw?.secrets?.openfga_db_password ?? defaults.secrets.openfga_db_password,
@@ -426,6 +443,7 @@ function mergeGeneratedState(env, generated) {
426
443
  },
427
444
  secrets: {
428
445
  s3_secret_key: env.S3_SECRET_KEY ?? normalized.secrets.s3_secret_key,
446
+ keycloak_db_password: env.KEYCLOAK_DB_PASSWORD ?? normalized.secrets.keycloak_db_password,
429
447
  keycloak_web_client_secret: env.KEYCLOAK_WEB_CLIENT_SECRET ?? normalized.secrets.keycloak_web_client_secret,
430
448
  keycloak_api_client_secret: env.KEYCLOAK_API_CLIENT_SECRET ?? normalized.secrets.keycloak_api_client_secret,
431
449
  openfga_db_password: env.OPENFGA_DB_PASSWORD ?? normalized.secrets.openfga_db_password,
@@ -446,6 +464,7 @@ function ensureGeneratedRuntimeState(generated) {
446
464
  openfga: generated.openfga,
447
465
  secrets: {
448
466
  s3_secret_key: generated.secrets.s3_secret_key || randomSecret(16),
467
+ keycloak_db_password: generated.secrets.keycloak_db_password || "",
449
468
  keycloak_web_client_secret: generated.secrets.keycloak_web_client_secret || randomSecret(),
450
469
  keycloak_api_client_secret: generated.secrets.keycloak_api_client_secret || randomSecret(),
451
470
  openfga_db_password: generated.secrets.openfga_db_password || randomSecret(),
@@ -488,6 +507,7 @@ function buildRuntimeEnv(config, generated) {
488
507
  KEYCLOAK_REALM: config.auth.keycloak_realm,
489
508
  KEYCLOAK_ADMIN_USER: config.auth.admin_user,
490
509
  KEYCLOAK_ADMIN_PASSWORD: config.auth.admin_password,
510
+ KEYCLOAK_DB_PASSWORD: generated.secrets.keycloak_db_password || config.auth.admin_password,
491
511
  KEYCLOAK_WEB_CLIENT_ID: config.auth.web_client_id,
492
512
  KEYCLOAK_WEB_CLIENT_SECRET: generated.secrets.keycloak_web_client_secret,
493
513
  KEYCLOAK_CLI_CLIENT_ID: config.auth.cli_client_id,
@@ -781,7 +801,7 @@ ${renderEnvList({
781
801
  environment:
782
802
  ${renderEnvList({
783
803
  POSTGRES_USER: "keycloak",
784
- POSTGRES_PASSWORD: runtimeEnv.KEYCLOAK_ADMIN_PASSWORD,
804
+ POSTGRES_PASSWORD: runtimeEnv.KEYCLOAK_DB_PASSWORD,
785
805
  POSTGRES_DB: "keycloak"
786
806
  })}
787
807
  volumes:
@@ -798,7 +818,7 @@ ${renderEnvList({
798
818
  KC_DB: "postgres",
799
819
  KC_DB_URL: "jdbc:postgresql://keycloak_db:5432/keycloak",
800
820
  KC_DB_USERNAME: "keycloak",
801
- KC_DB_PASSWORD: runtimeEnv.KEYCLOAK_ADMIN_PASSWORD,
821
+ KC_DB_PASSWORD: runtimeEnv.KEYCLOAK_DB_PASSWORD,
802
822
  KC_BOOTSTRAP_ADMIN_USERNAME: config.auth.admin_user,
803
823
  KC_BOOTSTRAP_ADMIN_PASSWORD: config.auth.admin_password,
804
824
  KC_HTTP_ENABLED: "true",
@@ -1070,7 +1090,29 @@ function waitForTcpService(config, label, host, port, timeoutSeconds = 120) {
1070
1090
  throw new Error(`Timed out waiting for ${label} at ${host}:${port}`);
1071
1091
  }
1072
1092
  function waitForHttpService(config, label, url, timeoutSeconds = 120) {
1073
- waitForCommand(config, `${label} HTTP readiness`, "alpine:3.20", `wget -q -O /dev/null ${JSON.stringify(url)}`, timeoutSeconds);
1093
+ if (currentOptions.dryRun) {
1094
+ logDryRun(`Would wait for ${label} at ${url}`);
1095
+ return;
1096
+ }
1097
+ logStep(`Waiting for ${label} on ${url}`);
1098
+ const deadline = Date.now() + timeoutSeconds * 1e3;
1099
+ while (Date.now() < deadline) {
1100
+ if (commandSucceeds("docker", [
1101
+ "run",
1102
+ "--rm",
1103
+ "--network",
1104
+ stackNetworkName(config),
1105
+ "alpine:3.20",
1106
+ "sh",
1107
+ "-lc",
1108
+ `wget -q -O /dev/null ${JSON.stringify(url)}`
1109
+ ])) {
1110
+ logSuccess(`${label} is ready`);
1111
+ return;
1112
+ }
1113
+ sleepSeconds(2);
1114
+ }
1115
+ throw new Error(`Timed out waiting for ${label} at ${url}`);
1074
1116
  }
1075
1117
  function runOpenFgaMigrate(config, runtimeEnv) {
1076
1118
  logStep("Running OpenFGA datastore migrations");
@@ -1251,6 +1293,100 @@ function listStackServices(config) {
1251
1293
  }
1252
1294
  return services;
1253
1295
  }
1296
+ function listManagedContainers(config) {
1297
+ const output = tryRun("docker", ["ps", "-aq", "--filter", `name=^/${config.services.stack_name}_`], { quiet: true });
1298
+ return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1299
+ }
1300
+ function stackExists(config) {
1301
+ const output = tryRun("docker", ["stack", "ls", "--format", "{{.Name}}"], { quiet: true });
1302
+ return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).includes(config.services.stack_name);
1303
+ }
1304
+ function waitForStackRemoval(config, timeoutSeconds = 60) {
1305
+ if (currentOptions.dryRun) {
1306
+ logDryRun(`Would wait for stack ${config.services.stack_name} to be removed`);
1307
+ return;
1308
+ }
1309
+ const deadline = Date.now() + timeoutSeconds * 1e3;
1310
+ while (Date.now() < deadline) {
1311
+ if (!stackExists(config)) {
1312
+ return;
1313
+ }
1314
+ sleepSeconds(2);
1315
+ }
1316
+ throw new Error(`Timed out waiting for stack ${config.services.stack_name} to be removed`);
1317
+ }
1318
+ async function confirmBootstrapReset(config) {
1319
+ const volumeNames = STACK_VOLUMES.map((volume) => stackVolumeName(config, volume));
1320
+ const containerIds = listManagedContainers(config);
1321
+ const networkName = stackNetworkName(config);
1322
+ logWarn("Bootstrap will delete existing EnvSync Docker resources before rebuilding infra.");
1323
+ logWarn(`Stack: ${config.services.stack_name}`);
1324
+ logWarn(`Network: ${networkName}`);
1325
+ logWarn(`Volumes: ${volumeNames.join(", ")}`);
1326
+ if (containerIds.length > 0) {
1327
+ logWarn(`Containers: ${containerIds.join(", ")}`);
1328
+ } else {
1329
+ logWarn("Containers: none currently matched");
1330
+ }
1331
+ logWarn("This removes existing deployment data for the managed EnvSync services.");
1332
+ if (currentOptions.force) {
1333
+ logWarn("Skipping confirmation because --force was provided.");
1334
+ logSuccess("Destructive bootstrap reset confirmed");
1335
+ return;
1336
+ }
1337
+ const response = await askRequired(chalk.bold.red('Type "yes" to continue:'));
1338
+ if (response !== "yes") {
1339
+ throw new Error("Bootstrap aborted. Confirmation did not match 'yes'.");
1340
+ }
1341
+ logSuccess("Destructive bootstrap reset confirmed");
1342
+ }
1343
+ function cleanupBootstrapState(config) {
1344
+ const volumeNames = STACK_VOLUMES.map((volume) => stackVolumeName(config, volume));
1345
+ const containerIds = listManagedContainers(config);
1346
+ const networkName = stackNetworkName(config);
1347
+ logStep("Removing existing EnvSync deployment resources");
1348
+ if (currentOptions.dryRun) {
1349
+ if (stackExists(config)) {
1350
+ logDryRun(`Would remove stack ${config.services.stack_name}`);
1351
+ logCommand("docker", ["stack", "rm", config.services.stack_name]);
1352
+ } else {
1353
+ logDryRun(`No existing stack named ${config.services.stack_name} found`);
1354
+ }
1355
+ if (containerIds.length > 0) {
1356
+ logDryRun(`Would remove containers: ${containerIds.join(", ")}`);
1357
+ logCommand("docker", ["rm", "-f", ...containerIds]);
1358
+ }
1359
+ logDryRun(`Would remove network ${networkName} if present`);
1360
+ logCommand("docker", ["network", "rm", networkName]);
1361
+ for (const volumeName of volumeNames) {
1362
+ logDryRun(`Would remove volume ${volumeName}`);
1363
+ logCommand("docker", ["volume", "rm", "-f", volumeName]);
1364
+ }
1365
+ logSuccess("Bootstrap cleanup preview completed");
1366
+ return;
1367
+ }
1368
+ if (stackExists(config)) {
1369
+ logCommand("docker", ["stack", "rm", config.services.stack_name]);
1370
+ run("docker", ["stack", "rm", config.services.stack_name]);
1371
+ waitForStackRemoval(config);
1372
+ }
1373
+ const refreshedContainers = listManagedContainers(config);
1374
+ if (refreshedContainers.length > 0) {
1375
+ logCommand("docker", ["rm", "-f", ...refreshedContainers]);
1376
+ run("docker", ["rm", "-f", ...refreshedContainers]);
1377
+ }
1378
+ if (commandSucceeds("docker", ["network", "inspect", networkName])) {
1379
+ logCommand("docker", ["network", "rm", networkName]);
1380
+ run("docker", ["network", "rm", networkName]);
1381
+ }
1382
+ for (const volumeName of volumeNames) {
1383
+ if (commandSucceeds("docker", ["volume", "inspect", volumeName])) {
1384
+ logCommand("docker", ["volume", "rm", "-f", volumeName]);
1385
+ run("docker", ["volume", "rm", "-f", volumeName]);
1386
+ }
1387
+ }
1388
+ logSuccess("Existing EnvSync deployment resources removed");
1389
+ }
1254
1390
  function serviceHealth(services, name) {
1255
1391
  return services.get(`${name}`) ?? "missing";
1256
1392
  }
@@ -1369,6 +1505,11 @@ async function cmdBootstrap() {
1369
1505
  const runtimeEnv = buildRuntimeEnv(config, nextGenerated);
1370
1506
  logInfo(`Release version: ${config.release.version}`);
1371
1507
  assertSwarmManager();
1508
+ if (currentOptions.dryRun) {
1509
+ logWarn("Dry-run mode: bootstrap reset will be previewed but not executed.");
1510
+ }
1511
+ await confirmBootstrapReset(config);
1512
+ cleanupBootstrapState(config);
1372
1513
  ensureRepoCheckout(config);
1373
1514
  writeDeployArtifacts(config, nextGenerated);
1374
1515
  buildKeycloakImage(config.images.keycloak);
@@ -1384,7 +1525,7 @@ async function cmdBootstrap() {
1384
1525
  waitForPostgresService(config, "postgres", "postgres", "postgres", "envsync-postgres");
1385
1526
  waitForRedisService(config);
1386
1527
  waitForTcpService(config, "rustfs", "rustfs", 9e3);
1387
- waitForPostgresService(config, "keycloak", "keycloak_db", "keycloak", runtimeEnv.KEYCLOAK_ADMIN_PASSWORD);
1528
+ waitForPostgresService(config, "keycloak", "keycloak_db", "keycloak", runtimeEnv.KEYCLOAK_DB_PASSWORD);
1388
1529
  waitForPostgresService(config, "openfga", "openfga_db", "openfga", runtimeEnv.OPENFGA_DB_PASSWORD);
1389
1530
  waitForPostgresService(config, "minikms", "minikms_db", "postgres", runtimeEnv.MINIKMS_DB_PASSWORD);
1390
1531
  runOpenFgaMigrate(config, runtimeEnv);
@@ -1398,7 +1539,7 @@ async function cmdBootstrap() {
1398
1539
  run("docker", ["stack", "deploy", "-c", BOOTSTRAP_STACK_FILE, config.services.stack_name]);
1399
1540
  logSuccess("Runtime bootstrap stack deployed");
1400
1541
  }
1401
- waitForHttpService(config, "keycloak", "http://keycloak:8080/health/ready", 180);
1542
+ waitForHttpService(config, "keycloak management readiness", "http://keycloak:9000/health/ready", 180);
1402
1543
  waitForHttpService(config, "openfga", "http://openfga:8090/stores");
1403
1544
  waitForTcpService(config, "minikms", "minikms", 50051);
1404
1545
  const initResult = runBootstrapInit(config);
@@ -1655,9 +1796,10 @@ async function main() {
1655
1796
  const command = argv[0];
1656
1797
  const args = argv.slice(1);
1657
1798
  currentOptions = {
1658
- dryRun: args.includes("--dry-run")
1799
+ dryRun: args.includes("--dry-run"),
1800
+ force: args.includes("--force")
1659
1801
  };
1660
- const positionals = args.filter((arg) => arg !== "--dry-run");
1802
+ const positionals = args.filter((arg) => arg !== "--dry-run" && arg !== "--force");
1661
1803
  switch (command) {
1662
1804
  case "preinstall":
1663
1805
  await cmdPreinstall();
@@ -1688,7 +1830,7 @@ async function main() {
1688
1830
  break;
1689
1831
  default:
1690
1832
  console.log(
1691
- "Usage: envsync-deploy <preinstall|setup|bootstrap|deploy|health|upgrade|upgrade-deps|backup|restore> [--dry-run]"
1833
+ "Usage: envsync-deploy <preinstall|setup|bootstrap|deploy|health|upgrade|upgrade-deps|backup|restore> [--dry-run] [--force]"
1692
1834
  );
1693
1835
  process.exit(command ? 1 : 0);
1694
1836
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {