@lead-routing/cli 0.1.9 → 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.
- package/dist/index.js +73 -37
- 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
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
624
|
+
Fix the issues above and re-run lead-routing init.`
|
|
624
625
|
);
|
|
625
626
|
}
|
|
626
627
|
}
|
|
627
|
-
async function
|
|
628
|
-
const { stdout, code } = await ssh.execSilent("docker --version");
|
|
629
|
-
if (code
|
|
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:
|
|
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
|
|
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
|
|
736
|
+
import { spinner as spinner3 } from "@clack/prompts";
|
|
701
737
|
async function uploadFiles(ssh, localDir, remoteDir) {
|
|
702
|
-
const s =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
930
|
+
const s = spinner5();
|
|
895
931
|
s.start("Creating admin user");
|
|
896
932
|
try {
|
|
897
933
|
const DATABASE_URL = getTunneledDbUrl(localDir, localPort);
|
|
@@ -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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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"],
|