@lead-routing/cli 0.1.8 → 0.1.10

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 +76 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -593,14 +593,15 @@ function generateFiles(cfg, sshCfg) {
593
593
  }
594
594
 
595
595
  // src/steps/check-remote-prerequisites.ts
596
- import { log as log4 } from "@clack/prompts";
596
+ import { log as log4, spinner as spinner2 } from "@clack/prompts";
597
597
  async function checkRemotePrerequisites(ssh) {
598
- const results = await Promise.all([
599
- checkRemoteDocker(ssh),
600
- checkRemoteDockerCompose(ssh),
598
+ const dockerResult = await checkOrInstallDocker(ssh);
599
+ const composeResult = await checkRemoteDockerCompose(ssh);
600
+ const portResults = await Promise.all([
601
601
  checkRemotePort(ssh, 80),
602
602
  checkRemotePort(ssh, 443)
603
603
  ]);
604
+ const results = [dockerResult, composeResult, ...portResults];
604
605
  const failed = results.filter((r) => !r.ok && !r.warn);
605
606
  const warnings = results.filter((r) => !r.ok && r.warn);
606
607
  for (const r of results) {
@@ -620,30 +621,65 @@ async function checkRemotePrerequisites(ssh) {
620
621
  `Remote server is missing required software:
621
622
  ` + failed.map((r) => ` \u2022 ${r.label}`).join("\n") + `
622
623
 
623
- Install the missing software on your server and re-run lead-routing init.`
624
+ Fix the issues above and re-run lead-routing init.`
624
625
  );
625
626
  }
626
627
  }
627
- async function checkRemoteDocker(ssh) {
628
- const { stdout, code } = await ssh.execSilent("docker --version");
629
- if (code !== 0 || !stdout) {
628
+ async function checkOrInstallDocker(ssh) {
629
+ const { stdout, code } = await ssh.execSilent("docker --version 2>/dev/null");
630
+ if (code === 0 && stdout) {
631
+ const match = stdout.match(/Docker version (\d+)/);
632
+ if (match && parseInt(match[1], 10) < 24) {
633
+ return { ok: false, label: `Docker ${stdout.trim()} \u2014 version 24+ required` };
634
+ }
635
+ return { ok: true, label: `Docker \u2014 ${stdout.trim()}` };
636
+ }
637
+ const s = spinner2();
638
+ s.start("Docker not found \u2014 installing via get.docker.com (~2 min)\u2026");
639
+ try {
640
+ const { code: curlCode } = await ssh.execSilent("command -v curl 2>/dev/null");
641
+ if (curlCode !== 0) {
642
+ await ssh.execSilent(
643
+ "apt-get install -y curl 2>/dev/null || yum install -y curl 2>/dev/null"
644
+ );
645
+ }
646
+ const { code: installCode } = await ssh.execSilent(
647
+ "curl -fsSL https://get.docker.com | sh 2>&1"
648
+ );
649
+ if (installCode !== 0) {
650
+ s.stop("Docker auto-install failed");
651
+ return {
652
+ ok: false,
653
+ label: "Docker \u2014 auto-install failed.\n SSH in and run manually: curl -fsSL https://get.docker.com | sh"
654
+ };
655
+ }
656
+ await ssh.execSilent(
657
+ "systemctl start docker 2>/dev/null; systemctl enable docker 2>/dev/null"
658
+ );
659
+ const { stdout: ver, code: verCode } = await ssh.execSilent("docker --version 2>/dev/null");
660
+ if (verCode !== 0 || !ver) {
661
+ s.stop("Docker installed but not responding");
662
+ return {
663
+ ok: false,
664
+ label: "Docker \u2014 installed but daemon not responding. Try rebooting the server."
665
+ };
666
+ }
667
+ s.stop(`Docker installed \u2014 ${ver.trim()}`);
668
+ return { ok: true, label: `Docker \u2014 installed ${ver.trim()}` };
669
+ } catch (err) {
670
+ s.stop("Docker auto-install failed");
630
671
  return {
631
672
  ok: false,
632
- label: "Docker \u2014 not found on server (install Docker Engine 24+)"
673
+ label: `Docker \u2014 auto-install failed: ${err instanceof Error ? err.message : String(err)}`
633
674
  };
634
675
  }
635
- const match = stdout.match(/Docker version (\d+)/);
636
- if (match && parseInt(match[1], 10) < 24) {
637
- return { ok: false, label: `Docker ${stdout.trim()} \u2014 version 24+ required` };
638
- }
639
- return { ok: true, label: `Docker \u2014 ${stdout.trim()}` };
640
676
  }
641
677
  async function checkRemoteDockerCompose(ssh) {
642
- const { stdout, code } = await ssh.execSilent("docker compose version");
678
+ const { stdout, code } = await ssh.execSilent("docker compose version 2>/dev/null");
643
679
  if (code !== 0 || !stdout) {
644
680
  return {
645
681
  ok: false,
646
- label: "Docker Compose \u2014 not found on server (update Docker to include Compose v2)"
682
+ label: "Docker Compose \u2014 not found (update Docker to include Compose v2)"
647
683
  };
648
684
  }
649
685
  return { ok: true, label: `Docker Compose \u2014 ${stdout.trim()}` };
@@ -697,9 +733,9 @@ async function checkRemotePort(ssh, port) {
697
733
 
698
734
  // src/steps/upload-files.ts
699
735
  import { join as join4 } from "path";
700
- import { spinner as spinner2 } from "@clack/prompts";
736
+ import { spinner as spinner3 } from "@clack/prompts";
701
737
  async function uploadFiles(ssh, localDir, remoteDir) {
702
- const s = spinner2();
738
+ const s = spinner3();
703
739
  s.start("Uploading config files to server");
704
740
  try {
705
741
  await ssh.mkdir(remoteDir);
@@ -724,7 +760,7 @@ async function uploadFiles(ssh, localDir, remoteDir) {
724
760
  }
725
761
 
726
762
  // src/steps/start-services.ts
727
- import { spinner as spinner3, log as log5 } from "@clack/prompts";
763
+ import { spinner as spinner4, log as log5 } from "@clack/prompts";
728
764
  async function startServices(ssh, remoteDir) {
729
765
  await wipeStalePostgresVolume(ssh, remoteDir);
730
766
  await pullImages(ssh, remoteDir);
@@ -738,7 +774,7 @@ async function wipeStalePostgresVolume(ssh, remoteDir) {
738
774
  if (code !== 0) {
739
775
  return;
740
776
  }
741
- const s = spinner3();
777
+ const s = spinner4();
742
778
  s.start("Removing existing database volume for clean install");
743
779
  try {
744
780
  await ssh.exec("docker compose down -v --remove-orphans", remoteDir);
@@ -752,7 +788,7 @@ async function wipeStalePostgresVolume(ssh, remoteDir) {
752
788
  }
753
789
  }
754
790
  async function pullImages(ssh, remoteDir) {
755
- const s = spinner3();
791
+ const s = spinner4();
756
792
  s.start("Pulling Docker images on server (this may take a few minutes)");
757
793
  try {
758
794
  await ssh.exec("docker compose pull", remoteDir);
@@ -765,7 +801,7 @@ async function pullImages(ssh, remoteDir) {
765
801
  }
766
802
  }
767
803
  async function startContainers(ssh, remoteDir) {
768
- const s = spinner3();
804
+ const s = spinner4();
769
805
  s.start("Starting services");
770
806
  try {
771
807
  await ssh.exec("docker compose up -d --remove-orphans", remoteDir);
@@ -776,7 +812,7 @@ async function startContainers(ssh, remoteDir) {
776
812
  }
777
813
  }
778
814
  async function waitForPostgres(ssh, remoteDir) {
779
- const s = spinner3();
815
+ const s = spinner4();
780
816
  s.start("Waiting for PostgreSQL to be ready");
781
817
  const maxAttempts = 24;
782
818
  let containerReady = false;
@@ -821,7 +857,7 @@ import * as path from "path";
821
857
  import * as crypto from "crypto";
822
858
  import { fileURLToPath as fileURLToPath2 } from "url";
823
859
  import { execa as execa2 } from "execa";
824
- import { spinner as spinner4 } from "@clack/prompts";
860
+ import { spinner as spinner5 } from "@clack/prompts";
825
861
  var __filename = fileURLToPath2(import.meta.url);
826
862
  var __dirname2 = path.dirname(__filename);
827
863
  function readEnvVar(envFile, key) {
@@ -857,7 +893,7 @@ function findPrismaBin() {
857
893
  return found;
858
894
  }
859
895
  async function runMigrations(ssh, localDir, adminEmail, adminPassword) {
860
- const s = spinner4();
896
+ const s = spinner5();
861
897
  s.start("Opening secure tunnel to database");
862
898
  let tunnelClose;
863
899
  try {
@@ -873,7 +909,7 @@ async function runMigrations(ssh, localDir, adminEmail, adminPassword) {
873
909
  }
874
910
  }
875
911
  async function applyMigrations(localDir, localPort) {
876
- const s = spinner4();
912
+ const s = spinner5();
877
913
  s.start("Running database migrations");
878
914
  try {
879
915
  const DATABASE_URL = getTunneledDbUrl(localDir, localPort);
@@ -891,7 +927,7 @@ async function applyMigrations(localDir, localPort) {
891
927
  }
892
928
  }
893
929
  async function seedAdminUser(localDir, localPort, adminEmail, adminPassword) {
894
- const s = spinner4();
930
+ const s = spinner5();
895
931
  s.start("Creating admin user");
896
932
  try {
897
933
  const DATABASE_URL = getTunneledDbUrl(localDir, localPort);
@@ -902,9 +938,9 @@ async function seedAdminUser(localDir, localPort, adminEmail, adminPassword) {
902
938
  const safeEmail = adminEmail.replace(/'/g, "''");
903
939
  const safeWebhookSecret = webhookSecret.replace(/'/g, "''");
904
940
  const sql = `
905
- -- Create initial organisation if none exists (self-hosted defaults: PAID plan, unlimited seats/quota)
906
- INSERT INTO organizations (id, "webhookSecret", plan, "seatsPurchased", "routingQuota", "isActive", "createdAt", "updatedAt")
907
- SELECT gen_random_uuid(), '${safeWebhookSecret}', 'PAID', 9999, 999999, true, NOW(), NOW()
941
+ -- Create initial organisation if none exists (self-hosted defaults: PAID plan, unlimited seats)
942
+ INSERT INTO organizations (id, "webhookSecret", plan, "seatsPurchased", "isActive", "createdAt", "updatedAt")
943
+ SELECT gen_random_uuid(), '${safeWebhookSecret}', 'PAID', 9999, true, NOW(), NOW()
908
944
  WHERE NOT EXISTS (SELECT 1 FROM organizations);
909
945
 
910
946
  -- Create admin AppUser under the first org (idempotent)
@@ -924,7 +960,7 @@ ON CONFLICT ("orgId", email) DO NOTHING;
924
960
  }
925
961
 
926
962
  // src/steps/verify-health.ts
927
- import { spinner as spinner5, log as log6 } from "@clack/prompts";
963
+ import { spinner as spinner6, log as log6 } from "@clack/prompts";
928
964
  async function verifyHealth(appUrl, engineUrl, ssh, remoteDir) {
929
965
  const checks = [
930
966
  { service: "Web app", url: `${appUrl}/api/health` },
@@ -973,7 +1009,7 @@ To resume once fixed:
973
1009
  );
974
1010
  }
975
1011
  async function pollHealth(service, url, maxAttempts = 24, intervalMs = 5e3) {
976
- const s = spinner5();
1012
+ const s = spinner6();
977
1013
  s.start(`Waiting for ${service}`);
978
1014
  for (let i = 0; i < maxAttempts; i++) {
979
1015
  try {
@@ -1006,7 +1042,7 @@ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsS
1006
1042
  import { join as join6, dirname as dirname3 } from "path";
1007
1043
  import { tmpdir } from "os";
1008
1044
  import { fileURLToPath as fileURLToPath3 } from "url";
1009
- import { spinner as spinner6, log as log7 } from "@clack/prompts";
1045
+ import { spinner as spinner7, log as log7 } from "@clack/prompts";
1010
1046
  import { execa as execa3 } from "execa";
1011
1047
  var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
1012
1048
  function patchXml(content, tag, value) {
@@ -1015,7 +1051,7 @@ function patchXml(content, tag, value) {
1015
1051
  }
1016
1052
  async function sfdcDeployInline(params) {
1017
1053
  const { appUrl, engineUrl, orgAlias, installDir } = params;
1018
- const s = spinner6();
1054
+ const s = spinner7();
1019
1055
  const { code: authCheck } = await execa3(
1020
1056
  "sf",
1021
1057
  ["org", "display", "--target-org", orgAlias, "--json"],
@@ -1175,7 +1211,7 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
1175
1211
  }
1176
1212
  async function loginViaAppBridge(rawAppUrl, orgAlias) {
1177
1213
  const appUrl = rawAppUrl.replace(/\/+$/, "");
1178
- const s = spinner6();
1214
+ const s = spinner7();
1179
1215
  s.start("Starting Salesforce authentication via your Lead Router app\u2026");
1180
1216
  let sessionId;
1181
1217
  let authUrl;
@@ -1823,7 +1859,7 @@ async function runStatus() {
1823
1859
  // src/commands/config.ts
1824
1860
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5 } from "fs";
1825
1861
  import { join as join8 } from "path";
1826
- import { intro as intro4, outro as outro4, text as text3, password as password3, spinner as spinner7, log as log14 } from "@clack/prompts";
1862
+ import { intro as intro4, outro as outro4, text as text3, password as password3, spinner as spinner8, log as log14 } from "@clack/prompts";
1827
1863
  import chalk5 from "chalk";
1828
1864
  import { execa as execa7 } from "execa";
1829
1865
  function parseEnv(filePath) {
@@ -1899,7 +1935,7 @@ Callback URL for your Connected App: ${callbackUrl}`
1899
1935
  writeEnv(envWeb, updates);
1900
1936
  writeEnv(envEngine, updates);
1901
1937
  log14.success("Updated .env.web and .env.engine");
1902
- const s = spinner7();
1938
+ const s = spinner8();
1903
1939
  s.start("Restarting web and engine containers\u2026");
1904
1940
  try {
1905
1941
  await execa7("docker", ["compose", "up", "-d", "--force-recreate", "web", "engine"], {
@@ -1936,7 +1972,7 @@ function runConfigShow() {
1936
1972
  }
1937
1973
 
1938
1974
  // src/commands/sfdc.ts
1939
- import { intro as intro5, outro as outro5, text as text4, spinner as spinner8, log as log15 } from "@clack/prompts";
1975
+ import { intro as intro5, outro as outro5, text as text4, spinner as spinner9, log as log15 } from "@clack/prompts";
1940
1976
  import chalk6 from "chalk";
1941
1977
  import { execa as execa8 } from "execa";
1942
1978
  async function runSfdcDeploy() {
@@ -1964,7 +2000,7 @@ async function runSfdcDeploy() {
1964
2000
  if (typeof rawEngine === "symbol") process.exit(0);
1965
2001
  engineUrl = rawEngine.trim();
1966
2002
  }
1967
- const s = spinner8();
2003
+ const s = spinner9();
1968
2004
  s.start("Checking Salesforce CLI\u2026");
1969
2005
  try {
1970
2006
  await execa8("sf", ["--version"], { all: true });
@@ -2007,7 +2043,7 @@ async function runSfdcDeploy() {
2007
2043
 
2008
2044
  // src/index.ts
2009
2045
  var program = new Command();
2010
- program.name("lead-routing").description("Self-hosted Lead Routing \u2014 scaffold, deploy, and manage your installation").version("0.1.8");
2046
+ program.name("lead-routing").description("Self-hosted Lead Routing \u2014 scaffold, deploy, and manage your installation").version("0.1.10");
2011
2047
  program.command("init").description("Interactive setup wizard \u2014 configure and deploy the full Lead Routing stack").option("--dry-run", "Generate config files without connecting or deploying").option("--resume", "Skip to health check using existing lead-routing.json (post-timeout recovery)").option("--sandbox", "Use Salesforce sandbox (test.salesforce.com) instead of production").option("--ssh-port <port>", "SSH port (default: 22)", parseInt).option("--ssh-user <user>", "SSH username (default: root)").option("--ssh-key <path>", "Path to SSH private key (overrides auto-detection)").option("--remote-dir <path>", "Remote install directory (default: ~/lead-routing)").option("--external-db <url>", "Use external PostgreSQL URL instead of managed Docker container").option("--external-redis <url>", "Use external Redis URL instead of managed Docker container").action((opts) => runInit({
2012
2048
  dryRun: opts.dryRun,
2013
2049
  resume: opts.resume,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lead-routing/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Self-hosted deployment CLI for Lead Routing",
5
5
  "homepage": "https://github.com/lead-routing/lead-routing",
6
6
  "keywords": ["salesforce", "lead-routing", "self-hosted", "deployment", "cli"],