@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 { stdout } = await ssh.execSilent(
748
- `ss -tlnp 2>/dev/null | grep ':${port} ' || netstat -tlnp 2>/dev/null | grep ':${port} ' || echo "free"`
749
- );
750
- const isBound = stdout.trim() !== "free" && stdout.trim() !== "";
751
- if (isBound) {
752
- return {
753
- ok: false,
754
- warn: true,
755
- label: `Port ${port} \u2014 already in use on server (Caddy needs it for HTTPS \u2014 ensure nothing else is binding it)`
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
- return { ok: true, label: `Port ${port} \u2014 available` };
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 responding yet \u2014 ${r.detail}`);
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: `timed out after ${maxAttempts * intervalMs / 1e3}s`
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 bundledPkg = join5(__dirname2, "..", "sfdc-package");
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: ${bundledPkg}
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", "--target-org", orgAlias, "--source-dir", "force-app"],
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", "--target-org", orgAlias],
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
- "--target-org",
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
- "--target-org",
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
- "--target-org",
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
- { input: accessToken + "\n" }
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 store sf CLI credentials: ${String(err)}`);
1271
- log6.info("Re-authenticate manually if deploy commands fail: sf org login web");
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.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"],