@lead-routing/cli 0.4.1 → 0.5.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
@@ -6,6 +6,8 @@ import { Command } from "commander";
6
6
  // src/commands/init.ts
7
7
  import { promises as dns } from "dns";
8
8
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
9
+ import { exec } from "child_process";
10
+ import { platform } from "os";
9
11
  import { join as join4 } from "path";
10
12
  import { intro, outro, note as note3, log as log7, confirm, cancel as cancel3, isCancel as isCancel3, password as promptPassword } from "@clack/prompts";
11
13
  import chalk from "chalk";
@@ -1000,6 +1002,11 @@ ${result.stderr || result.stdout}`
1000
1002
  };
1001
1003
 
1002
1004
  // src/commands/init.ts
1005
+ var MANAGED_PACKAGE_INSTALL_URL = "https://login.salesforce.com/packaging/installPackage.apexp?p0=04tgL000000CTnp";
1006
+ function openBrowser(url) {
1007
+ const cmd = platform() === "darwin" ? "open" : "xdg-open";
1008
+ exec(`${cmd} ${JSON.stringify(url)}`);
1009
+ }
1003
1010
  async function checkDnsResolvable(appUrl, engineUrl) {
1004
1011
  let hosts;
1005
1012
  try {
@@ -1058,26 +1065,9 @@ async function runInit(options = {}) {
1058
1065
  });
1059
1066
  log7.success(`Connected to ${saved.ssh.host}`);
1060
1067
  const remoteDir = await ssh.resolveHome(saved.remoteDir);
1061
- log7.step("Step 7/7 Verifying health");
1068
+ log7.step("Verifying health");
1062
1069
  await verifyHealth(saved.appUrl, saved.engineUrl, ssh, remoteDir);
1063
- note3(
1064
- `Open ${saved.appUrl} \u2192 Integrations \u2192 Salesforce to connect your CRM and deploy the package.`,
1065
- "Next: Connect Salesforce"
1066
- );
1067
- outro(
1068
- chalk.green("\u2714 You're live!") + `
1069
-
1070
- Dashboard: ${chalk.cyan(saved.appUrl)}
1071
- Routing engine: ${chalk.cyan(saved.engineUrl)}
1072
-
1073
- ` + chalk.bold(" Next steps:\n") + ` ${chalk.cyan("1.")} Open ${chalk.cyan(saved.appUrl)} and log in
1074
- ${chalk.cyan("2.")} Go to Integrations \u2192 Salesforce to connect your org
1075
- ${chalk.cyan("3.")} Deploy the package and configure routing objects
1076
- ${chalk.cyan("4.")} Create your first routing rule
1077
-
1078
- Run ${chalk.cyan("lead-routing doctor")} to check service health at any time.
1079
- Run ${chalk.cyan("lead-routing deploy")} to update to a new version.`
1080
- );
1070
+ outro(chalk.green("\u2714 Services are healthy!"));
1081
1071
  } catch (err) {
1082
1072
  const message = err instanceof Error ? err.message : String(err);
1083
1073
  log7.error(`Resume failed: ${message}`);
@@ -1088,9 +1078,33 @@ async function runInit(options = {}) {
1088
1078
  return;
1089
1079
  }
1090
1080
  try {
1091
- log7.step("Step 1/7 Checking local prerequisites");
1081
+ log7.step("Step 1/8 Install Salesforce Package");
1082
+ note3(
1083
+ `The Lead Router managed package installs the required Connected App,
1084
+ triggers, and custom objects in your Salesforce org.
1085
+
1086
+ Install URL: ${chalk.cyan(MANAGED_PACKAGE_INSTALL_URL)}`,
1087
+ "Salesforce Package"
1088
+ );
1089
+ log7.info("Opening install URL in your browser...");
1090
+ openBrowser(MANAGED_PACKAGE_INSTALL_URL);
1091
+ log7.info(`${chalk.dim("If the browser didn't open, visit the URL above manually.")}`);
1092
+ const installed = await confirm({
1093
+ message: 'Have you installed the package? (Click "Install for All Users" in Salesforce)',
1094
+ initialValue: false
1095
+ });
1096
+ if (isCancel3(installed)) {
1097
+ cancel3("Setup cancelled.");
1098
+ process.exit(0);
1099
+ }
1100
+ if (!installed) {
1101
+ log7.warn("You can install the package later from Integrations \u2192 Salesforce in the web app.");
1102
+ } else {
1103
+ log7.success("Salesforce package installed");
1104
+ }
1105
+ log7.step("Step 2/8 Checking local prerequisites");
1092
1106
  await checkPrerequisites();
1093
- log7.step("Step 2/7 SSH connection");
1107
+ log7.step("Step 3/8 SSH connection");
1094
1108
  const sshCfg = await collectSshConfig({
1095
1109
  sshPort: options.sshPort,
1096
1110
  sshUser: options.sshUser,
@@ -1107,13 +1121,13 @@ async function runInit(options = {}) {
1107
1121
  process.exit(1);
1108
1122
  }
1109
1123
  }
1110
- log7.step("Step 3/7 Configuration");
1124
+ log7.step("Step 4/8 Configuration");
1111
1125
  const cfg = await collectConfig({
1112
1126
  externalDb: options.externalDb,
1113
1127
  externalRedis: options.externalRedis
1114
1128
  });
1115
1129
  await checkDnsResolvable(cfg.appUrl, cfg.engineUrl);
1116
- log7.step("Step 4/7 Generating config files");
1130
+ log7.step("Step 5/8 Generating config files");
1117
1131
  const { dir, adminSecret } = generateFiles(cfg, sshCfg);
1118
1132
  note3(
1119
1133
  `Local config directory: ${chalk.cyan(dir)}
@@ -1130,13 +1144,13 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1130
1144
  );
1131
1145
  return;
1132
1146
  }
1133
- log7.step("Step 5/7 Remote setup");
1147
+ log7.step("Step 6/8 Remote setup");
1134
1148
  const remoteDir = await ssh.resolveHome(sshCfg.remoteDir);
1135
1149
  await checkRemotePrerequisites(ssh);
1136
1150
  await uploadFiles(ssh, dir, remoteDir);
1137
- log7.step("Step 6/7 Starting services");
1151
+ log7.step("Step 7/8 Starting services");
1138
1152
  await startServices(ssh, remoteDir);
1139
- log7.step("Step 7/7 Verifying health");
1153
+ log7.step("Step 8/8 Verifying health");
1140
1154
  await verifyHealth(cfg.appUrl, cfg.engineUrl, ssh, remoteDir);
1141
1155
  try {
1142
1156
  const envWebPath = join4(dir, ".env.web");
@@ -1147,7 +1161,8 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1147
1161
  } catch {
1148
1162
  }
1149
1163
  note3(
1150
- `Open ${cfg.appUrl} \u2192 Integrations \u2192 Salesforce to connect your CRM and deploy the package.`,
1164
+ `Open ${cfg.appUrl} \u2192 Integrations \u2192 Salesforce to connect your org.
1165
+ The managed package is already installed \u2014 just click "Connect Salesforce" to authorize.`,
1151
1166
  "Next: Connect Salesforce"
1152
1167
  );
1153
1168
  outro(
@@ -1161,8 +1176,8 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1161
1176
  ${chalk.dim("run `lead-routing config show` to retrieve later")}
1162
1177
 
1163
1178
  ` + chalk.bold(" Next steps:\n") + ` ${chalk.cyan("1.")} Open ${chalk.cyan(cfg.appUrl)} and log in
1164
- ${chalk.cyan("2.")} Go to Integrations \u2192 Salesforce to connect your org
1165
- ${chalk.cyan("3.")} Deploy the package and configure routing objects
1179
+ ${chalk.cyan("2.")} Go to Integrations \u2192 Salesforce \u2192 Connect
1180
+ ${chalk.cyan("3.")} Complete the onboarding wizard in Salesforce
1166
1181
  ${chalk.cyan("4.")} Create your first routing rule
1167
1182
 
1168
1183
  Run ${chalk.cyan("lead-routing doctor")} to check service health at any time.
@@ -0,0 +1,3 @@
1
+ -- AlterEnum
2
+ ALTER TYPE "RoutingStatus" ADD VALUE 'COOLDOWN_SKIPPED';
3
+ ALTER TYPE "RoutingStatus" ADD VALUE 'STAMP_SKIPPED';
@@ -0,0 +1,5 @@
1
+ -- AlterTable
2
+ ALTER TABLE "routing_logs" ADD COLUMN "decisionTrace" JSONB;
3
+
4
+ -- CreateIndex
5
+ CREATE INDEX "routing_logs_orgId_sfdcRecordId_createdAt_idx" ON "routing_logs"("orgId", "sfdcRecordId", "createdAt");
@@ -0,0 +1,5 @@
1
+ -- Add licensedVia to User model
2
+ ALTER TABLE "users" ADD COLUMN "licensedVia" TEXT;
3
+
4
+ -- Add isLicensed to SfdcQueue model
5
+ ALTER TABLE "sfdc_queues" ADD COLUMN "isLicensed" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,19 @@
1
+ -- AlterEnum: Add SEARCH to TriggerEvent
2
+ ALTER TYPE "TriggerEvent" ADD VALUE 'SEARCH';
3
+
4
+ -- CreateEnum: RouteType
5
+ CREATE TYPE "RouteType" AS ENUM ('REALTIME', 'SCHEDULED');
6
+
7
+ -- AlterTable: Add route type and scheduled fields to routing_rules
8
+ ALTER TABLE "routing_rules" ADD COLUMN "routeType" "RouteType" NOT NULL DEFAULT 'REALTIME';
9
+ ALTER TABLE "routing_rules" ADD COLUMN "scheduleFrequency" TEXT;
10
+ ALTER TABLE "routing_rules" ADD COLUMN "scheduleTime" TEXT;
11
+ ALTER TABLE "routing_rules" ADD COLUMN "scheduleTimezone" TEXT;
12
+ ALTER TABLE "routing_rules" ADD COLUMN "scheduleCron" TEXT;
13
+ ALTER TABLE "routing_rules" ADD COLUMN "searchCriteria" JSONB;
14
+ ALTER TABLE "routing_rules" ADD COLUMN "lastRunAt" TIMESTAMP(3);
15
+ ALTER TABLE "routing_rules" ADD COLUMN "lastRunStatus" TEXT;
16
+ ALTER TABLE "routing_rules" ADD COLUMN "lastRunRecords" INTEGER;
17
+ ALTER TABLE "routing_rules" ADD COLUMN "lastRunDurationMs" INTEGER;
18
+ ALTER TABLE "routing_rules" ADD COLUMN "totalRuns" INTEGER NOT NULL DEFAULT 0;
19
+ ALTER TABLE "routing_rules" ADD COLUMN "totalRecordsRouted" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,2 @@
1
+ -- AlterTable
2
+ ALTER TABLE "round_robin_teams" ADD COLUMN "distributionType" TEXT NOT NULL DEFAULT 'round-robin';
@@ -20,6 +20,12 @@ enum TriggerEvent {
20
20
  INSERT
21
21
  UPDATE
22
22
  BOTH
23
+ SEARCH
24
+ }
25
+
26
+ enum RouteType {
27
+ REALTIME
28
+ SCHEDULED
23
29
  }
24
30
 
25
31
  enum RuleStatus {
@@ -44,6 +50,8 @@ enum RoutingStatus {
44
50
  UNMATCHED
45
51
  RETRY
46
52
  MERGED
53
+ COOLDOWN_SKIPPED
54
+ STAMP_SKIPPED
47
55
  }
48
56
 
49
57
  enum LeadMatchAction {
@@ -160,6 +168,7 @@ model User {
160
168
  profile String?
161
169
  department String?
162
170
  isLicensed Boolean @default(false)
171
+ licensedVia String? // "individual" | "role" | "profile" | "custom_field" — how user was licensed
163
172
  isActive Boolean @default(true) // false if no longer in SFDC
164
173
  lastRoutedAt DateTime?
165
174
  syncedAt DateTime @default(now())
@@ -179,8 +188,9 @@ model RoundRobinTeam {
179
188
  id String @id @default(cuid())
180
189
  orgId String
181
190
  name String
182
- description String?
183
- pointerIndex Int @default(0) // kept in sync with Redis; Redis is authoritative
191
+ description String?
192
+ distributionType String @default("round-robin") // "round-robin" | "weighted"
193
+ pointerIndex Int @default(0) // kept in sync with Redis; Redis is authoritative
184
194
  createdAt DateTime @default(now())
185
195
  updatedAt DateTime @updatedAt
186
196
 
@@ -226,6 +236,20 @@ model RoutingRule {
226
236
  isDryRun Boolean @default(false)
227
237
  triggerName String @default("")
228
238
  criteriaSyncedAt DateTime?
239
+ // Route type (real-time Apex trigger vs scheduled search)
240
+ routeType RouteType @default(REALTIME)
241
+ // Scheduled route fields
242
+ scheduleFrequency String? // "DAILY" | "WEEKLY" | "MONTHLY" | null (one-time)
243
+ scheduleTime String? // "06:00" (24h format)
244
+ scheduleTimezone String? // "UTC", "US/Eastern", etc.
245
+ scheduleCron String? // computed cron expression for BullMQ
246
+ searchCriteria Json? // ConditionGroup[] — search trigger criteria
247
+ lastRunAt DateTime?
248
+ lastRunStatus String? // "SUCCESS" | "FAILED" | "PARTIAL"
249
+ lastRunRecords Int? // records routed in last run
250
+ lastRunDurationMs Int?
251
+ totalRuns Int @default(0)
252
+ totalRecordsRouted Int @default(0)
229
253
  // Default owner: absolute catch-all for new Route Builder routes
230
254
  defaultOwnerType AssignmentType?
231
255
  defaultOwnerUserId String?
@@ -374,6 +398,7 @@ model RoutingLog {
374
398
  retryCount Int @default(0)
375
399
  dismissed Boolean @default(false)
376
400
  recordSnapshot Json? // full field payload from SFDC at time of routing
401
+ decisionTrace Json? // structured routing decision trace for Record Journey
377
402
  teamId String?
378
403
  teamName String?
379
404
  routingDurationMs Int? // ms from webhook receipt → SFDC assignment
@@ -388,6 +413,7 @@ model RoutingLog {
388
413
  @@index([orgId, status])
389
414
  @@index([orgId, teamId])
390
415
  @@index([orgId, ruleId])
416
+ @@index([orgId, sfdcRecordId, createdAt])
391
417
  @@map("routing_logs")
392
418
  }
393
419
 
@@ -414,6 +440,7 @@ model SfdcQueue {
414
440
  orgId String
415
441
  sfdcQueueId String // SFDC Queue ID (00G...)
416
442
  name String
443
+ isLicensed Boolean @default(false) // whether queue can receive routed records
417
444
  syncedAt DateTime @default(now())
418
445
 
419
446
  org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lead-routing/cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Self-hosted deployment CLI for Lead Routing",
5
5
  "homepage": "https://github.com/lead-routing/lead-routing",
6
6
  "keywords": [