@lead-routing/cli 0.1.1 → 0.1.3
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
CHANGED
|
@@ -744,18 +744,41 @@ async function checkRemoteDockerCompose(ssh) {
|
|
|
744
744
|
return { ok: true, label: `Docker Compose \u2014 ${stdout.trim()}` };
|
|
745
745
|
}
|
|
746
746
|
async function checkRemotePort(ssh, port) {
|
|
747
|
-
const {
|
|
748
|
-
|
|
749
|
-
);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
747
|
+
const portCheckCmd = `ss -tlnp 2>/dev/null | grep ':${port} ' || netstat -tlnp 2>/dev/null | grep ':${port} ' || echo "free"`;
|
|
748
|
+
const { stdout: initial } = await ssh.execSilent(portCheckCmd);
|
|
749
|
+
const isBound = initial.trim() !== "free" && initial.trim() !== "";
|
|
750
|
+
if (!isBound) {
|
|
751
|
+
return { ok: true, label: `Port ${port} \u2014 available` };
|
|
752
|
+
}
|
|
753
|
+
const knownServices = ["nginx", "apache2", "httpd", "lighttpd", "caddy"];
|
|
754
|
+
for (const svc of knownServices) {
|
|
755
|
+
const { code: activeCode } = await ssh.execSilent(
|
|
756
|
+
`systemctl is-active --quiet ${svc} 2>/dev/null`
|
|
757
|
+
);
|
|
758
|
+
if (activeCode === 0) {
|
|
759
|
+
await ssh.execSilent(
|
|
760
|
+
`systemctl stop ${svc} 2>/dev/null; systemctl disable ${svc} 2>/dev/null`
|
|
761
|
+
);
|
|
762
|
+
const { stdout: recheck } = await ssh.execSilent(portCheckCmd);
|
|
763
|
+
if (recheck.trim() === "free" || !recheck.trim()) {
|
|
764
|
+
return {
|
|
765
|
+
ok: true,
|
|
766
|
+
label: `Port ${port} \u2014 freed (stopped and disabled system ${svc} service)`
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
757
770
|
}
|
|
758
|
-
|
|
771
|
+
const { stdout: occupant } = await ssh.execSilent(
|
|
772
|
+
`ss -tlnp 2>/dev/null | grep ':${port} ' | head -1 || echo "unknown process"`
|
|
773
|
+
);
|
|
774
|
+
return {
|
|
775
|
+
ok: false,
|
|
776
|
+
// Hard error — Caddy cannot get TLS certs without these ports
|
|
777
|
+
label: `Port ${port} is occupied and could not be freed automatically.
|
|
778
|
+
Occupant: ${occupant.trim()}
|
|
779
|
+
Stop the conflicting process on the server, then re-run:
|
|
780
|
+
lead-routing init`
|
|
781
|
+
};
|
|
759
782
|
}
|
|
760
783
|
|
|
761
784
|
// src/steps/upload-files.ts
|
|
@@ -986,7 +1009,7 @@ ON CONFLICT ("orgId", email) DO NOTHING;
|
|
|
986
1009
|
|
|
987
1010
|
// src/steps/verify-health.ts
|
|
988
1011
|
import { spinner as spinner5, log as log5 } from "@clack/prompts";
|
|
989
|
-
async function verifyHealth(appUrl, engineUrl) {
|
|
1012
|
+
async function verifyHealth(appUrl, engineUrl, ssh, remoteDir) {
|
|
990
1013
|
const checks = [
|
|
991
1014
|
{ service: "Web app", url: `${appUrl}/api/health` },
|
|
992
1015
|
{ service: "Routing engine", url: `${engineUrl}/health` }
|
|
@@ -996,9 +1019,42 @@ async function verifyHealth(appUrl, engineUrl) {
|
|
|
996
1019
|
if (r.ok) {
|
|
997
1020
|
log5.success(`${r.service} \u2014 ${r.url}`);
|
|
998
1021
|
} else {
|
|
999
|
-
log5.warn(`${r.service} not
|
|
1022
|
+
log5.warn(`${r.service} \u2014 did not respond after ${r.detail}`);
|
|
1000
1023
|
}
|
|
1001
1024
|
}
|
|
1025
|
+
const failed = results.filter((r) => !r.ok);
|
|
1026
|
+
if (failed.length === 0) return;
|
|
1027
|
+
log5.info("Fetching remote diagnostics\u2026");
|
|
1028
|
+
try {
|
|
1029
|
+
const { stdout: ps } = await ssh.execSilent("docker compose ps --format table", remoteDir);
|
|
1030
|
+
if (ps.trim()) log5.info(`Container status:
|
|
1031
|
+
${ps.trim()}`);
|
|
1032
|
+
} catch {
|
|
1033
|
+
}
|
|
1034
|
+
try {
|
|
1035
|
+
const { stdout: caddyLogs } = await ssh.execSilent(
|
|
1036
|
+
"docker compose logs caddy --tail 30 --no-color 2>&1",
|
|
1037
|
+
remoteDir
|
|
1038
|
+
);
|
|
1039
|
+
if (caddyLogs.trim()) log5.info(`Caddy logs (last 30 lines):
|
|
1040
|
+
${caddyLogs.trim()}`);
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
const failedNames = failed.map((r) => r.service).join(" and ");
|
|
1044
|
+
throw new Error(
|
|
1045
|
+
`${failedNames} did not respond after 2 minutes.
|
|
1046
|
+
|
|
1047
|
+
Common causes (check Caddy logs above):
|
|
1048
|
+
\u2022 Let's Encrypt rate limit \u2014 wait until tomorrow and re-run
|
|
1049
|
+
\u2022 Port 80/443 still blocked by another process
|
|
1050
|
+
\u2022 Container crashed \u2014 check container status above
|
|
1051
|
+
|
|
1052
|
+
To resume once fixed:
|
|
1053
|
+
1. SSH into your server:
|
|
1054
|
+
cd ${remoteDir} && docker compose restart caddy
|
|
1055
|
+
2. Then re-run Salesforce setup:
|
|
1056
|
+
lead-routing sfdc deploy`
|
|
1057
|
+
);
|
|
1002
1058
|
}
|
|
1003
1059
|
async function pollHealth(service, url, maxAttempts = 24, intervalMs = 5e3) {
|
|
1004
1060
|
const s = spinner5();
|
|
@@ -1022,7 +1078,7 @@ async function pollHealth(service, url, maxAttempts = 24, intervalMs = 5e3) {
|
|
|
1022
1078
|
service,
|
|
1023
1079
|
url,
|
|
1024
1080
|
ok: false,
|
|
1025
|
-
detail:
|
|
1081
|
+
detail: `${maxAttempts * intervalMs / 1e3}s`
|
|
1026
1082
|
};
|
|
1027
1083
|
}
|
|
1028
1084
|
function sleep2(ms) {
|
|
@@ -1050,18 +1106,26 @@ async function sfdcDeployInline(params) {
|
|
|
1050
1106
|
{ reject: false }
|
|
1051
1107
|
);
|
|
1052
1108
|
const alreadyAuthed = authCheck === 0;
|
|
1109
|
+
let sfCredEnv = {};
|
|
1110
|
+
let targetOrgArgs = ["--target-org", orgAlias];
|
|
1053
1111
|
if (alreadyAuthed) {
|
|
1054
1112
|
log6.success("Using existing Salesforce authentication");
|
|
1055
1113
|
} else {
|
|
1056
|
-
await loginViaAppBridge(appUrl, orgAlias);
|
|
1114
|
+
const { accessToken, instanceUrl, aliasStored } = await loginViaAppBridge(appUrl, orgAlias);
|
|
1115
|
+
sfCredEnv = { SF_ACCESS_TOKEN: accessToken, SF_ORG_INSTANCE_URL: instanceUrl };
|
|
1116
|
+
if (!aliasStored) {
|
|
1117
|
+
targetOrgArgs = [];
|
|
1118
|
+
}
|
|
1057
1119
|
}
|
|
1058
1120
|
s.start("Copying Salesforce package\u2026");
|
|
1059
|
-
const
|
|
1121
|
+
const inDist = join5(__dirname2, "sfdc-package");
|
|
1122
|
+
const nextToDist = join5(__dirname2, "..", "sfdc-package");
|
|
1123
|
+
const bundledPkg = existsSync4(inDist) ? inDist : nextToDist;
|
|
1060
1124
|
const destPkg = join5(installDir ?? tmpdir(), "lead-routing-sfdc-package");
|
|
1061
1125
|
if (!existsSync4(bundledPkg)) {
|
|
1062
1126
|
s.stop("sfdc-package not found in CLI bundle");
|
|
1063
1127
|
throw new Error(
|
|
1064
|
-
`Expected bundle at: ${
|
|
1128
|
+
`Expected bundle at: ${inDist}
|
|
1065
1129
|
The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
1066
1130
|
);
|
|
1067
1131
|
}
|
|
@@ -1111,8 +1175,8 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1111
1175
|
try {
|
|
1112
1176
|
await execa3(
|
|
1113
1177
|
"sf",
|
|
1114
|
-
["project", "deploy", "start",
|
|
1115
|
-
{ cwd: destPkg, stdio: "inherit" }
|
|
1178
|
+
["project", "deploy", "start", ...targetOrgArgs, "--source-dir", "force-app"],
|
|
1179
|
+
{ cwd: destPkg, stdio: "inherit", env: { ...process.env, ...sfCredEnv } }
|
|
1116
1180
|
);
|
|
1117
1181
|
s.stop("Package deployed");
|
|
1118
1182
|
} catch (err) {
|
|
@@ -1129,8 +1193,8 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1129
1193
|
try {
|
|
1130
1194
|
await execa3(
|
|
1131
1195
|
"sf",
|
|
1132
|
-
["org", "assign", "permset", "--name", "LeadRouterAdmin",
|
|
1133
|
-
{ stdio: "inherit" }
|
|
1196
|
+
["org", "assign", "permset", "--name", "LeadRouterAdmin", ...targetOrgArgs],
|
|
1197
|
+
{ stdio: "inherit", env: { ...process.env, ...sfCredEnv } }
|
|
1134
1198
|
);
|
|
1135
1199
|
s.stop("Permission set assigned \u2014 Lead Router Setup will appear in the App Launcher");
|
|
1136
1200
|
} catch (err) {
|
|
@@ -1152,12 +1216,11 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1152
1216
|
const qr = await execa3("sf", [
|
|
1153
1217
|
"data",
|
|
1154
1218
|
"query",
|
|
1155
|
-
|
|
1156
|
-
orgAlias,
|
|
1219
|
+
...targetOrgArgs,
|
|
1157
1220
|
"--query",
|
|
1158
1221
|
"SELECT Id FROM Routing_Settings__c LIMIT 1",
|
|
1159
1222
|
"--json"
|
|
1160
|
-
]);
|
|
1223
|
+
], { env: { ...process.env, ...sfCredEnv } });
|
|
1161
1224
|
const parsed = JSON.parse(qr.stdout);
|
|
1162
1225
|
existingId = parsed?.result?.records?.[0]?.Id;
|
|
1163
1226
|
} catch {
|
|
@@ -1167,27 +1230,25 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1167
1230
|
"data",
|
|
1168
1231
|
"update",
|
|
1169
1232
|
"record",
|
|
1170
|
-
|
|
1171
|
-
orgAlias,
|
|
1233
|
+
...targetOrgArgs,
|
|
1172
1234
|
"--sobject",
|
|
1173
1235
|
"Routing_Settings__c",
|
|
1174
1236
|
"--record-id",
|
|
1175
1237
|
existingId,
|
|
1176
1238
|
"--values",
|
|
1177
1239
|
`App_Url__c='${appUrl}' Engine_Endpoint__c='${engineUrl}'`
|
|
1178
|
-
], { stdio: "inherit" });
|
|
1240
|
+
], { stdio: "inherit", env: { ...process.env, ...sfCredEnv } });
|
|
1179
1241
|
} else {
|
|
1180
1242
|
await execa3("sf", [
|
|
1181
1243
|
"data",
|
|
1182
1244
|
"create",
|
|
1183
1245
|
"record",
|
|
1184
|
-
|
|
1185
|
-
orgAlias,
|
|
1246
|
+
...targetOrgArgs,
|
|
1186
1247
|
"--sobject",
|
|
1187
1248
|
"Routing_Settings__c",
|
|
1188
1249
|
"--values",
|
|
1189
1250
|
`App_Url__c='${appUrl}' Engine_Endpoint__c='${engineUrl}'`
|
|
1190
|
-
], { stdio: "inherit" });
|
|
1251
|
+
], { stdio: "inherit", env: { ...process.env, ...sfCredEnv } });
|
|
1191
1252
|
}
|
|
1192
1253
|
s.stop("Org settings written");
|
|
1193
1254
|
} catch (err) {
|
|
@@ -1259,17 +1320,20 @@ Ensure the app is running and the URL is correct.`
|
|
|
1259
1320
|
);
|
|
1260
1321
|
}
|
|
1261
1322
|
s.stop("Authenticated with Salesforce");
|
|
1323
|
+
let aliasStored = false;
|
|
1262
1324
|
try {
|
|
1263
1325
|
await execa3(
|
|
1264
1326
|
"sf",
|
|
1265
1327
|
["org", "login", "access-token", "--instance-url", instanceUrl, "--alias", orgAlias, "--no-prompt"],
|
|
1266
|
-
{
|
|
1328
|
+
{ env: { ...process.env, SFDX_ACCESS_TOKEN: accessToken } }
|
|
1267
1329
|
);
|
|
1268
1330
|
log6.success(`Salesforce org saved as "${orgAlias}"`);
|
|
1331
|
+
aliasStored = true;
|
|
1269
1332
|
} catch (err) {
|
|
1270
|
-
log6.warn(`Could not
|
|
1271
|
-
log6.info("
|
|
1333
|
+
log6.warn(`Could not persist sf CLI credentials: ${String(err)}`);
|
|
1334
|
+
log6.info("Continuing with direct token auth for this session.");
|
|
1272
1335
|
}
|
|
1336
|
+
return { accessToken, instanceUrl, aliasStored };
|
|
1273
1337
|
}
|
|
1274
1338
|
|
|
1275
1339
|
// src/steps/app-launcher-guide.ts
|
|
@@ -1471,7 +1535,7 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1471
1535
|
log8.step("Step 7/9 Database migrations");
|
|
1472
1536
|
await runMigrations(ssh, dir, cfg.adminEmail, cfg.adminPassword);
|
|
1473
1537
|
log8.step("Step 8/9 Verifying health");
|
|
1474
|
-
await verifyHealth(cfg.appUrl, cfg.engineUrl);
|
|
1538
|
+
await verifyHealth(cfg.appUrl, cfg.engineUrl, ssh, remoteDir);
|
|
1475
1539
|
log8.step("Step 9/9 Deploying Salesforce package");
|
|
1476
1540
|
await sfdcDeployInline({
|
|
1477
1541
|
appUrl: cfg.appUrl,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<RemoteSiteSetting xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
3
|
+
<description>Lead Router App URL — patched by lead-routing sfdc deploy</description>
|
|
4
|
+
<disableProtocolSecurity>false</disableProtocolSecurity>
|
|
5
|
+
<isActive>true</isActive>
|
|
6
|
+
<url>https://app.example.com</url>
|
|
7
|
+
</RemoteSiteSetting>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lead-routing/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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"],
|