@lead-routing/cli 0.2.0 → 0.3.0

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
@@ -5,6 +5,8 @@ import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
7
  import { promises as dns } from "dns";
8
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
9
+ import { join as join6 } from "path";
8
10
  import { intro, outro, note as note4, log as log9, confirm as confirm2, cancel as cancel3, isCancel as isCancel4, password as promptPassword } from "@clack/prompts";
9
11
  import chalk2 from "chalk";
10
12
 
@@ -170,8 +172,9 @@ async function collectConfig(opts = {}) {
170
172
  const dbPassword = generateSecret(16);
171
173
  const managedDb = !opts.externalDb;
172
174
  const databaseUrl = opts.externalDb ?? `postgresql://leadrouting:${dbPassword}@postgres:5432/leadrouting`;
175
+ const redisPassword = generateSecret(16);
173
176
  const managedRedis = !opts.externalRedis;
174
- const redisUrl = opts.externalRedis ?? "redis://redis:6379";
177
+ const redisUrl = opts.externalRedis ?? `redis://:${redisPassword}@redis:6379`;
175
178
  note2("This creates the first admin user for the web app.", "Admin Account");
176
179
  const adminEmail = await text2({
177
180
  message: "Admin email address",
@@ -193,6 +196,7 @@ async function collectConfig(opts = {}) {
193
196
  const sessionSecret = generateSecret(32);
194
197
  const engineWebhookSecret = generateSecret(32);
195
198
  const adminSecret = generateSecret(16);
199
+ const internalApiKey = generateSecret(32);
196
200
  return {
197
201
  appUrl: appUrl.trim().replace(/\/+$/, ""),
198
202
  engineUrl: engineUrl.trim().replace(/\/+$/, ""),
@@ -204,13 +208,15 @@ async function collectConfig(opts = {}) {
204
208
  dbPassword: managedDb ? dbPassword : "",
205
209
  managedRedis,
206
210
  redisUrl,
211
+ redisPassword: managedRedis ? redisPassword : "",
207
212
  adminEmail,
208
213
  adminPassword,
209
214
  resendApiKey: "",
210
215
  feedbackToEmail: "",
211
216
  sessionSecret,
212
217
  engineWebhookSecret,
213
- adminSecret
218
+ adminSecret,
219
+ internalApiKey
214
220
  };
215
221
  }
216
222
 
@@ -243,14 +249,16 @@ function renderDockerCompose(c) {
243
249
  timeout: 5s
244
250
  retries: 10
245
251
  ` : "";
252
+ const redisPassword = c.redisPassword ?? "";
246
253
  const redisService = c.managedRedis ? `
247
254
  redis:
248
255
  image: redis:7-alpine
249
- restart: unless-stopped
256
+ restart: unless-stopped${redisPassword ? `
257
+ command: redis-server --requirepass ${redisPassword}` : ""}
250
258
  volumes:
251
259
  - redis_data:/data
252
260
  healthcheck:
253
- test: ["CMD", "redis-cli", "ping"]
261
+ test: ["CMD-SHELL", "redis-cli${redisPassword ? ` -a ${redisPassword}` : ""} ping | grep PONG"]
254
262
  interval: 5s
255
263
  timeout: 3s
256
264
  retries: 10
@@ -366,6 +374,9 @@ function renderEnvWeb(c) {
366
374
  `ADMIN_EMAIL=${c.adminEmail}`,
367
375
  `ADMIN_PASSWORD=${c.adminPassword}`,
368
376
  ``,
377
+ `# Internal API key (shared with engine for analytics)`,
378
+ `INTERNAL_API_KEY=${c.internalApiKey}`,
379
+ ``,
369
380
  `# Email (optional)`,
370
381
  `RESEND_API_KEY=${c.resendApiKey ?? ""}`,
371
382
  `FEEDBACK_TO_EMAIL=${c.feedbackToEmail ?? ""}`
@@ -395,7 +406,10 @@ function renderEnvEngine(c) {
395
406
  `SFDC_LOGIN_URL=${c.sfdcLoginUrl}`,
396
407
  ``,
397
408
  `# Webhook`,
398
- `ENGINE_WEBHOOK_SECRET=${c.engineWebhookSecret}`
409
+ `ENGINE_WEBHOOK_SECRET=${c.engineWebhookSecret}`,
410
+ ``,
411
+ `# Internal API key (Bearer token for analytics endpoints)`,
412
+ `INTERNAL_API_KEY=${c.internalApiKey}`
399
413
  ].join("\n");
400
414
  }
401
415
 
@@ -412,7 +426,18 @@ function renderCaddyfile(appUrl, engineUrl) {
412
426
  `# Generated by lead-routing CLI`,
413
427
  `# Caddy auto-provisions SSL certificates via Let's Encrypt`,
414
428
  ``,
429
+ `(security_headers) {`,
430
+ ` header {`,
431
+ ` X-Content-Type-Options nosniff`,
432
+ ` X-Frame-Options DENY`,
433
+ ` Referrer-Policy strict-origin-when-cross-origin`,
434
+ ` Permissions-Policy interest-cohort=()`,
435
+ ` Strict-Transport-Security "max-age=31536000; includeSubDomains"`,
436
+ ` }`,
437
+ `}`,
438
+ ``,
415
439
  `${appHost} {`,
440
+ ` import security_headers`,
416
441
  ` reverse_proxy web:3000 {`,
417
442
  ` health_uri /api/health`,
418
443
  ` health_interval 15s`,
@@ -420,6 +445,7 @@ function renderCaddyfile(appUrl, engineUrl) {
420
445
  `}`,
421
446
  ``,
422
447
  `${appHost}:${enginePort} {`,
448
+ ` import security_headers`,
423
449
  ` reverse_proxy engine:3001 {`,
424
450
  ` health_uri /health`,
425
451
  ` health_interval 15s`,
@@ -432,7 +458,18 @@ function renderCaddyfile(appUrl, engineUrl) {
432
458
  `# Generated by lead-routing CLI`,
433
459
  `# Caddy auto-provisions SSL certificates via Let's Encrypt`,
434
460
  ``,
461
+ `(security_headers) {`,
462
+ ` header {`,
463
+ ` X-Content-Type-Options nosniff`,
464
+ ` X-Frame-Options DENY`,
465
+ ` Referrer-Policy strict-origin-when-cross-origin`,
466
+ ` Permissions-Policy interest-cohort=()`,
467
+ ` Strict-Transport-Security "max-age=31536000; includeSubDomains"`,
468
+ ` }`,
469
+ `}`,
470
+ ``,
435
471
  `${appHost} {`,
472
+ ` import security_headers`,
436
473
  ` reverse_proxy web:3000 {`,
437
474
  ` health_uri /api/health`,
438
475
  ` health_interval 15s`,
@@ -440,6 +477,7 @@ function renderCaddyfile(appUrl, engineUrl) {
440
477
  `}`,
441
478
  ``,
442
479
  `${engineHost} {`,
480
+ ` import security_headers`,
443
481
  ` reverse_proxy engine:3001 {`,
444
482
  ` health_uri /health`,
445
483
  ` health_interval 15s`,
@@ -491,7 +529,8 @@ function generateFiles(cfg, sshCfg) {
491
529
  const composeContent = renderDockerCompose({
492
530
  managedDb: cfg.managedDb,
493
531
  managedRedis: cfg.managedRedis,
494
- dbPassword: cfg.dbPassword
532
+ dbPassword: cfg.dbPassword,
533
+ redisPassword: cfg.redisPassword
495
534
  });
496
535
  const composeFile = join2(dir, "docker-compose.yml");
497
536
  writeFileSync2(composeFile, composeContent, "utf8");
@@ -513,6 +552,7 @@ function generateFiles(cfg, sshCfg) {
513
552
  adminSecret: cfg.adminSecret,
514
553
  adminEmail: cfg.adminEmail,
515
554
  adminPassword: cfg.adminPassword,
555
+ internalApiKey: cfg.internalApiKey,
516
556
  resendApiKey: cfg.resendApiKey || void 0,
517
557
  feedbackToEmail: cfg.feedbackToEmail || void 0
518
558
  });
@@ -525,7 +565,8 @@ function generateFiles(cfg, sshCfg) {
525
565
  sfdcClientId: cfg.sfdcClientId,
526
566
  sfdcClientSecret: cfg.sfdcClientSecret,
527
567
  sfdcLoginUrl: cfg.sfdcLoginUrl,
528
- engineWebhookSecret: cfg.engineWebhookSecret
568
+ engineWebhookSecret: cfg.engineWebhookSecret,
569
+ internalApiKey: cfg.internalApiKey
529
570
  });
530
571
  const envEngine = join2(dir, ".env.engine");
531
572
  writeFileSync2(envEngine, envEngineContent, "utf8");
@@ -549,6 +590,7 @@ function generateFiles(cfg, sshCfg) {
549
590
  // Stored so `lead-routing sfdc deploy` can re-authenticate without re-prompting
550
591
  sfdcClientId: cfg.sfdcClientId,
551
592
  sfdcLoginUrl: cfg.sfdcLoginUrl,
593
+ engineWebhookSecret: cfg.engineWebhookSecret,
552
594
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
553
595
  version: getCliVersion()
554
596
  });
@@ -894,7 +936,7 @@ function sleep2(ms) {
894
936
  }
895
937
 
896
938
  // src/steps/sfdc-deploy-inline.ts
897
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, cpSync, rmSync } from "fs";
939
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, cpSync, rmSync } from "fs";
898
940
  import { join as join5, dirname as dirname2 } from "path";
899
941
  import { tmpdir } from "os";
900
942
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -983,8 +1025,7 @@ var SalesforceApi = class {
983
1025
  const deployOptions = JSON.stringify({
984
1026
  deployOptions: {
985
1027
  rollbackOnError: true,
986
- singlePackage: true,
987
- rest: true
1028
+ singlePackage: true
988
1029
  }
989
1030
  });
990
1031
  const parts = [];
@@ -1064,21 +1105,116 @@ var DuplicateError = class extends Error {
1064
1105
 
1065
1106
  // src/utils/zip-source.ts
1066
1107
  import { join as join4 } from "path";
1108
+ import { readdirSync, readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
1067
1109
  import archiver from "archiver";
1110
+ var META_TYPE_MAP = {
1111
+ applications: "CustomApplication",
1112
+ classes: "ApexClass",
1113
+ triggers: "ApexTrigger",
1114
+ lwc: "LightningComponentBundle",
1115
+ permissionsets: "PermissionSet",
1116
+ namedCredentials: "NamedCredential",
1117
+ remoteSiteSettings: "RemoteSiteSetting",
1118
+ tabs: "CustomTab"
1119
+ };
1068
1120
  async function zipSourcePackage(packageDir) {
1121
+ const forceAppDefault = join4(packageDir, "force-app", "main", "default");
1122
+ let apiVersion = "59.0";
1123
+ try {
1124
+ const proj = JSON.parse(readFileSync3(join4(packageDir, "sfdx-project.json"), "utf8"));
1125
+ if (proj.sourceApiVersion) apiVersion = proj.sourceApiVersion;
1126
+ } catch {
1127
+ }
1128
+ const members = /* @__PURE__ */ new Map();
1129
+ const addMember = (type, name) => {
1130
+ if (!members.has(type)) members.set(type, /* @__PURE__ */ new Set());
1131
+ members.get(type).add(name);
1132
+ };
1069
1133
  return new Promise((resolve, reject) => {
1070
1134
  const chunks = [];
1071
1135
  const archive = archiver("zip", { zlib: { level: 9 } });
1072
1136
  archive.on("data", (chunk) => chunks.push(chunk));
1073
1137
  archive.on("end", () => resolve(Buffer.concat(chunks)));
1074
1138
  archive.on("error", reject);
1075
- archive.directory(join4(packageDir, "force-app"), "force-app");
1076
- archive.file(join4(packageDir, "sfdx-project.json"), {
1077
- name: "sfdx-project.json"
1078
- });
1139
+ for (const [dirName, metaType] of Object.entries(META_TYPE_MAP)) {
1140
+ const srcDir = join4(forceAppDefault, dirName);
1141
+ if (!existsSync3(srcDir)) continue;
1142
+ const entries = readdirSync(srcDir, { withFileTypes: true });
1143
+ for (const entry of entries) {
1144
+ if (dirName === "lwc" && entry.isDirectory()) {
1145
+ addMember(metaType, entry.name);
1146
+ archive.directory(join4(srcDir, entry.name), `${dirName}/${entry.name}`);
1147
+ } else if (entry.isFile()) {
1148
+ archive.file(join4(srcDir, entry.name), { name: `${dirName}/${entry.name}` });
1149
+ if (!entry.name.endsWith("-meta.xml")) {
1150
+ const memberName = entry.name.replace(/\.[^.]+$/, "");
1151
+ addMember(metaType, memberName);
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+ const objectsDir = join4(forceAppDefault, "objects");
1157
+ if (existsSync3(objectsDir)) {
1158
+ for (const objEntry of readdirSync(objectsDir, { withFileTypes: true })) {
1159
+ if (!objEntry.isDirectory()) continue;
1160
+ const objName = objEntry.name;
1161
+ addMember("CustomObject", objName);
1162
+ const objDir = join4(objectsDir, objName);
1163
+ const objectXml = mergeObjectXml(objDir, objName, apiVersion);
1164
+ archive.append(Buffer.from(objectXml, "utf8"), {
1165
+ name: `objects/${objName}.object`
1166
+ });
1167
+ }
1168
+ }
1169
+ const packageXml = generatePackageXml(members, apiVersion);
1170
+ archive.append(Buffer.from(packageXml, "utf8"), { name: "package.xml" });
1079
1171
  archive.finalize();
1080
1172
  });
1081
1173
  }
1174
+ function mergeObjectXml(objDir, objName, apiVersion) {
1175
+ const lines = [
1176
+ '<?xml version="1.0" encoding="UTF-8"?>',
1177
+ '<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">'
1178
+ ];
1179
+ const objMetaPath = join4(objDir, `${objName}.object-meta.xml`);
1180
+ if (existsSync3(objMetaPath)) {
1181
+ const content = readFileSync3(objMetaPath, "utf8");
1182
+ const inner = content.replace(/<\?xml[^?]*\?>\s*/g, "").replace(/<CustomObject[^>]*>/g, "").replace(/<\/CustomObject>/g, "").trim();
1183
+ if (inner) lines.push(inner);
1184
+ }
1185
+ const fieldsDir = join4(objDir, "fields");
1186
+ if (existsSync3(fieldsDir)) {
1187
+ for (const fieldFile of readdirSync(fieldsDir).sort()) {
1188
+ if (!fieldFile.endsWith(".field-meta.xml")) continue;
1189
+ const content = readFileSync3(join4(fieldsDir, fieldFile), "utf8");
1190
+ const inner = content.replace(/<\?xml[^?]*\?>\s*/g, "").replace(/<CustomField[^>]*>/g, "").replace(/<\/CustomField>/g, "").trim();
1191
+ if (inner) {
1192
+ lines.push(" <fields>");
1193
+ lines.push(` ${inner}`);
1194
+ lines.push(" </fields>");
1195
+ }
1196
+ }
1197
+ }
1198
+ lines.push("</CustomObject>");
1199
+ return lines.join("\n");
1200
+ }
1201
+ function generatePackageXml(members, apiVersion) {
1202
+ const lines = [
1203
+ '<?xml version="1.0" encoding="UTF-8"?>',
1204
+ '<Package xmlns="http://soap.sforce.com/2006/04/metadata">'
1205
+ ];
1206
+ for (const [metaType, names] of [...members.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
1207
+ lines.push(" <types>");
1208
+ for (const name of [...names].sort()) {
1209
+ lines.push(` <members>${name}</members>`);
1210
+ }
1211
+ lines.push(` <name>${metaType}</name>`);
1212
+ lines.push(" </types>");
1213
+ }
1214
+ lines.push(` <version>${apiVersion}</version>`);
1215
+ lines.push("</Package>");
1216
+ return lines.join("\n");
1217
+ }
1082
1218
 
1083
1219
  // src/steps/sfdc-deploy-inline.ts
1084
1220
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
@@ -1094,16 +1230,16 @@ async function sfdcDeployInline(params) {
1094
1230
  s.start("Copying Salesforce package\u2026");
1095
1231
  const inDist = join5(__dirname2, "sfdc-package");
1096
1232
  const nextToDist = join5(__dirname2, "..", "sfdc-package");
1097
- const bundledPkg = existsSync3(inDist) ? inDist : nextToDist;
1233
+ const bundledPkg = existsSync4(inDist) ? inDist : nextToDist;
1098
1234
  const destPkg = join5(installDir ?? tmpdir(), "lead-routing-sfdc-package");
1099
- if (!existsSync3(bundledPkg)) {
1235
+ if (!existsSync4(bundledPkg)) {
1100
1236
  s.stop("sfdc-package not found in CLI bundle");
1101
1237
  throw new Error(
1102
1238
  `Expected bundle at: ${inDist}
1103
1239
  The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
1104
1240
  );
1105
1241
  }
1106
- if (existsSync3(destPkg)) rmSync(destPkg, { recursive: true, force: true });
1242
+ if (existsSync4(destPkg)) rmSync(destPkg, { recursive: true, force: true });
1107
1243
  cpSync(bundledPkg, destPkg, { recursive: true });
1108
1244
  s.stop("Package copied");
1109
1245
  const ncPath = join5(
@@ -1114,8 +1250,8 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
1114
1250
  "namedCredentials",
1115
1251
  "RoutingEngine.namedCredential-meta.xml"
1116
1252
  );
1117
- if (existsSync3(ncPath)) {
1118
- const nc = patchXml(readFileSync3(ncPath, "utf8"), "endpoint", engineUrl);
1253
+ if (existsSync4(ncPath)) {
1254
+ const nc = patchXml(readFileSync4(ncPath, "utf8"), "endpoint", engineUrl);
1119
1255
  writeFileSync3(ncPath, nc, "utf8");
1120
1256
  }
1121
1257
  const rssEnginePath = join5(
@@ -1126,8 +1262,8 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
1126
1262
  "remoteSiteSettings",
1127
1263
  "LeadRouterEngine.remoteSite-meta.xml"
1128
1264
  );
1129
- if (existsSync3(rssEnginePath)) {
1130
- let rss = patchXml(readFileSync3(rssEnginePath, "utf8"), "url", engineUrl);
1265
+ if (existsSync4(rssEnginePath)) {
1266
+ let rss = patchXml(readFileSync4(rssEnginePath, "utf8"), "url", engineUrl);
1131
1267
  rss = patchXml(rss, "description", "Lead Router Engine endpoint");
1132
1268
  writeFileSync3(rssEnginePath, rss, "utf8");
1133
1269
  }
@@ -1139,8 +1275,8 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
1139
1275
  "remoteSiteSettings",
1140
1276
  "LeadRouterApp.remoteSite-meta.xml"
1141
1277
  );
1142
- if (existsSync3(rssAppPath)) {
1143
- let rss = patchXml(readFileSync3(rssAppPath, "utf8"), "url", appUrl);
1278
+ if (existsSync4(rssAppPath)) {
1279
+ let rss = patchXml(readFileSync4(rssAppPath, "utf8"), "url", appUrl);
1144
1280
  rss = patchXml(rss, "description", "Lead Router App URL");
1145
1281
  writeFileSync3(rssAppPath, rss, "utf8");
1146
1282
  }
@@ -1210,16 +1346,17 @@ ${failureMsg || result.errorMessage || "Unknown error"}`
1210
1346
  const existing = await sf.query(
1211
1347
  "SELECT Id FROM Routing_Settings__c LIMIT 1"
1212
1348
  );
1349
+ const settingsData = {
1350
+ App_Url__c: appUrl,
1351
+ Engine_Endpoint__c: engineUrl
1352
+ };
1353
+ if (params.webhookSecret) {
1354
+ settingsData.Webhook_Secret__c = params.webhookSecret;
1355
+ }
1213
1356
  if (existing.length > 0) {
1214
- await sf.update("Routing_Settings__c", existing[0].Id, {
1215
- App_Url__c: appUrl,
1216
- Engine_Endpoint__c: engineUrl
1217
- });
1357
+ await sf.update("Routing_Settings__c", existing[0].Id, settingsData);
1218
1358
  } else {
1219
- await sf.create("Routing_Settings__c", {
1220
- App_Url__c: appUrl,
1221
- Engine_Endpoint__c: engineUrl
1222
- });
1359
+ await sf.create("Routing_Settings__c", settingsData);
1223
1360
  }
1224
1361
  s.stop("Org settings written");
1225
1362
  } catch (err) {
@@ -1521,7 +1658,8 @@ async function runInit(options = {}) {
1521
1658
  orgAlias: "lead-routing",
1522
1659
  sfdcClientId: saved.sfdcClientId ?? "",
1523
1660
  sfdcLoginUrl: saved.sfdcLoginUrl ?? "https://login.salesforce.com",
1524
- installDir: dir
1661
+ installDir: dir,
1662
+ webhookSecret: saved.engineWebhookSecret
1525
1663
  });
1526
1664
  await guideAppLauncherSetup(saved.appUrl);
1527
1665
  outro(
@@ -1597,6 +1735,14 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1597
1735
  await startServices(ssh, remoteDir);
1598
1736
  log9.step("Step 7/8 Verifying health");
1599
1737
  await verifyHealth(cfg.appUrl, cfg.engineUrl, ssh, remoteDir);
1738
+ try {
1739
+ const envWebPath = join6(dir, ".env.web");
1740
+ const envContent = readFileSync5(envWebPath, "utf-8");
1741
+ const cleaned = envContent.split("\n").filter((line) => !line.startsWith("ADMIN_PASSWORD=")).join("\n");
1742
+ writeFileSync4(envWebPath, cleaned, "utf-8");
1743
+ log9.success("Removed ADMIN_PASSWORD from .env.web (no longer needed after seed)");
1744
+ } catch {
1745
+ }
1600
1746
  log9.step("Step 8/8 Deploying Salesforce package");
1601
1747
  await sfdcDeployInline({
1602
1748
  appUrl: cfg.appUrl,
@@ -1604,7 +1750,8 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1604
1750
  orgAlias: "lead-routing",
1605
1751
  sfdcClientId: cfg.sfdcClientId,
1606
1752
  sfdcLoginUrl: cfg.sfdcLoginUrl,
1607
- installDir: dir
1753
+ installDir: dir,
1754
+ webhookSecret: cfg.engineWebhookSecret
1608
1755
  });
1609
1756
  await guideAppLauncherSetup(cfg.appUrl);
1610
1757
  outro(
@@ -1633,8 +1780,8 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1633
1780
  }
1634
1781
 
1635
1782
  // src/commands/deploy.ts
1636
- import { writeFileSync as writeFileSync4, unlinkSync } from "fs";
1637
- import { join as join6 } from "path";
1783
+ import { writeFileSync as writeFileSync5, unlinkSync } from "fs";
1784
+ import { join as join7 } from "path";
1638
1785
  import { tmpdir as tmpdir2 } from "os";
1639
1786
  import { intro as intro2, outro as outro2, log as log10, password as promptPassword2 } from "@clack/prompts";
1640
1787
  import chalk3 from "chalk";
@@ -1676,8 +1823,8 @@ async function runDeploy() {
1676
1823
  const remoteDir = await ssh.resolveHome(cfg.remoteDir);
1677
1824
  log10.step("Syncing Caddyfile");
1678
1825
  const caddyContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl);
1679
- const tmpCaddy = join6(tmpdir2(), "lead-routing-Caddyfile");
1680
- writeFileSync4(tmpCaddy, caddyContent, "utf8");
1826
+ const tmpCaddy = join7(tmpdir2(), "lead-routing-Caddyfile");
1827
+ writeFileSync5(tmpCaddy, caddyContent, "utf8");
1681
1828
  await ssh.upload([{ local: tmpCaddy, remote: `${remoteDir}/Caddyfile` }]);
1682
1829
  unlinkSync(tmpCaddy);
1683
1830
  await ssh.exec("docker compose restart caddy", remoteDir);
@@ -1839,15 +1986,15 @@ async function runStatus() {
1839
1986
  }
1840
1987
 
1841
1988
  // src/commands/config.ts
1842
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync4 } from "fs";
1843
- import { join as join7 } from "path";
1989
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync5 } from "fs";
1990
+ import { join as join8 } from "path";
1844
1991
  import { intro as intro4, outro as outro4, text as text3, password as password3, spinner as spinner6, log as log14 } from "@clack/prompts";
1845
1992
  import chalk5 from "chalk";
1846
1993
  import { execa as execa4 } from "execa";
1847
1994
  function parseEnv(filePath) {
1848
1995
  const map = /* @__PURE__ */ new Map();
1849
- if (!existsSync4(filePath)) return map;
1850
- for (const line of readFileSync4(filePath, "utf8").split("\n")) {
1996
+ if (!existsSync5(filePath)) return map;
1997
+ for (const line of readFileSync6(filePath, "utf8").split("\n")) {
1851
1998
  const trimmed = line.trim();
1852
1999
  if (!trimmed || trimmed.startsWith("#")) continue;
1853
2000
  const eq = trimmed.indexOf("=");
@@ -1857,7 +2004,7 @@ function parseEnv(filePath) {
1857
2004
  return map;
1858
2005
  }
1859
2006
  function writeEnv(filePath, updates) {
1860
- const lines = existsSync4(filePath) ? readFileSync4(filePath, "utf8").split("\n") : [];
2007
+ const lines = existsSync5(filePath) ? readFileSync6(filePath, "utf8").split("\n") : [];
1861
2008
  const updated = /* @__PURE__ */ new Set();
1862
2009
  const result = lines.map((line) => {
1863
2010
  const trimmed = line.trim();
@@ -1874,7 +2021,7 @@ function writeEnv(filePath, updates) {
1874
2021
  for (const [key, val] of Object.entries(updates)) {
1875
2022
  if (!updated.has(key)) result.push(`${key}=${val}`);
1876
2023
  }
1877
- writeFileSync5(filePath, result.join("\n"), "utf8");
2024
+ writeFileSync6(filePath, result.join("\n"), "utf8");
1878
2025
  }
1879
2026
  async function runConfigSfdc() {
1880
2027
  intro4("Lead Routing \u2014 Update Salesforce Credentials");
@@ -1884,8 +2031,8 @@ async function runConfigSfdc() {
1884
2031
  log14.info("Run `lead-routing init` first, or cd into your installation directory.");
1885
2032
  process.exit(1);
1886
2033
  }
1887
- const envWeb = join7(dir, ".env.web");
1888
- const envEngine = join7(dir, ".env.engine");
2034
+ const envWeb = join8(dir, ".env.web");
2035
+ const envEngine = join8(dir, ".env.engine");
1889
2036
  const currentWeb = parseEnv(envWeb);
1890
2037
  const currentClientId = currentWeb.get("SFDC_CLIENT_ID") ?? "";
1891
2038
  const currentLoginUrl = currentWeb.get("SFDC_LOGIN_URL") ?? "https://login.salesforce.com";
@@ -1938,7 +2085,7 @@ function runConfigShow() {
1938
2085
  console.error("No lead-routing installation found in the current directory.");
1939
2086
  process.exit(1);
1940
2087
  }
1941
- const envWeb = join7(dir, ".env.web");
2088
+ const envWeb = join8(dir, ".env.web");
1942
2089
  const cfg = parseEnv(envWeb);
1943
2090
  const adminSecret = cfg.get("ADMIN_SECRET") ?? "(not found)";
1944
2091
  const appUrl = cfg.get("APP_URL") ?? "(not found)";
@@ -1996,7 +2143,8 @@ async function runSfdcDeploy() {
1996
2143
  // Read from config if available
1997
2144
  sfdcClientId: config2?.sfdcClientId ?? "",
1998
2145
  sfdcLoginUrl: config2?.sfdcLoginUrl ?? "https://login.salesforce.com",
1999
- installDir: dir ?? void 0
2146
+ installDir: dir ?? void 0,
2147
+ webhookSecret: config2?.engineWebhookSecret
2000
2148
  });
2001
2149
  } catch (err) {
2002
2150
  log15.error(err instanceof Error ? err.message : String(err));
@@ -2011,7 +2159,7 @@ async function runSfdcDeploy() {
2011
2159
  }
2012
2160
 
2013
2161
  // src/commands/uninstall.ts
2014
- import { rmSync as rmSync2, existsSync as existsSync5 } from "fs";
2162
+ import { rmSync as rmSync2, existsSync as existsSync6 } from "fs";
2015
2163
  import { intro as intro6, outro as outro6, log as log16, confirm as confirm3, password as promptPassword3, isCancel as isCancel5 } from "@clack/prompts";
2016
2164
  import chalk7 from "chalk";
2017
2165
  async function runUninstall() {
@@ -2087,7 +2235,7 @@ async function runUninstall() {
2087
2235
  await ssh.disconnect();
2088
2236
  }
2089
2237
  log16.step("Removing local config directory");
2090
- if (existsSync5(dir)) {
2238
+ if (existsSync6(dir)) {
2091
2239
  rmSync2(dir, { recursive: true, force: true });
2092
2240
  log16.success(`Removed ${dir}`);
2093
2241
  }
@@ -0,0 +1,2 @@
1
+ -- AlterTable: make assignmentType nullable on routing_branches (allows saving unconfigured branches)
2
+ ALTER TABLE "routing_branches" ALTER COLUMN "assignmentType" DROP NOT NULL;
@@ -230,7 +230,7 @@ model RoutingBranch {
230
230
  rule RoutingRule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
231
231
  label String?
232
232
  priority Int @default(0)
233
- assignmentType AssignmentType
233
+ assignmentType AssignmentType?
234
234
  assigneeUserId String?
235
235
  assigneeTeamId String?
236
236
  assigneeQueueId String?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lead-routing/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Self-hosted deployment CLI for Lead Routing",
5
5
  "homepage": "https://github.com/lead-routing/lead-routing",
6
6
  "keywords": [