@lead-routing/cli 0.5.0 → 0.6.1
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 +352 -124
- package/dist/prisma/migrations/20260315000000_fix_aggregate_upsert_race/migration.sql +36 -0
- package/dist/prisma/migrations/20260316000000_add_license_activation_columns/migration.sql +5 -0
- package/dist/prisma/migrations/20260316100000_add_bulk_search_runs/migration.sql +36 -0
- package/dist/prisma/migrations/20260316203552_add_ai_agent_models/migration.sql +97 -0
- package/dist/prisma/migrations/20260317000000_add_routing_flows/migration.sql +76 -0
- package/dist/prisma/migrations/20260318000000_add_flow_edge_handles/migration.sql +3 -0
- package/dist/prisma/migrations/20260318100000_add_branch_steps/migration.sql +2 -0
- package/dist/prisma/migrations/20260318200000_add_api_tokens/migration.sql +24 -0
- package/dist/prisma/migrations/migration_lock.toml +2 -2
- package/dist/prisma/schema.prisma +195 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,12 +5,12 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import { promises as dns } from "dns";
|
|
8
|
-
import { readFileSync as
|
|
8
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
9
9
|
import { exec } from "child_process";
|
|
10
10
|
import { platform } from "os";
|
|
11
|
-
import { join as
|
|
11
|
+
import { join as join5 } from "path";
|
|
12
12
|
import { intro, outro, note as note3, log as log7, confirm, cancel as cancel3, isCancel as isCancel3, password as promptPassword } from "@clack/prompts";
|
|
13
|
-
import
|
|
13
|
+
import chalk2 from "chalk";
|
|
14
14
|
|
|
15
15
|
// src/steps/prerequisites.ts
|
|
16
16
|
import { log } from "@clack/prompts";
|
|
@@ -77,12 +77,12 @@ async function collectSshConfig(opts = {}) {
|
|
|
77
77
|
privateKeyPath = resolved;
|
|
78
78
|
log2.info(`Using SSH key: ${opts.sshKey}`);
|
|
79
79
|
} else {
|
|
80
|
-
const
|
|
80
|
+
const p3 = await password({
|
|
81
81
|
message: `SSH password for ${opts.sshUser ?? "root"}@${host}`,
|
|
82
82
|
validate: (v) => !v ? "Required" : void 0
|
|
83
83
|
});
|
|
84
|
-
if (isCancel(
|
|
85
|
-
pwd =
|
|
84
|
+
if (isCancel(p3)) bail(p3);
|
|
85
|
+
pwd = p3;
|
|
86
86
|
}
|
|
87
87
|
return {
|
|
88
88
|
host,
|
|
@@ -170,7 +170,6 @@ async function collectConfig(opts = {}) {
|
|
|
170
170
|
if (isCancel2(adminPassword)) bail2(adminPassword);
|
|
171
171
|
const sessionSecret = generateSecret(32);
|
|
172
172
|
const engineWebhookSecret = generateSecret(32);
|
|
173
|
-
const adminSecret = generateSecret(16);
|
|
174
173
|
const internalApiKey = generateSecret(32);
|
|
175
174
|
return {
|
|
176
175
|
appUrl: appUrl.trim().replace(/\/+$/, ""),
|
|
@@ -187,7 +186,6 @@ async function collectConfig(opts = {}) {
|
|
|
187
186
|
feedbackToEmail: "",
|
|
188
187
|
sessionSecret,
|
|
189
188
|
engineWebhookSecret,
|
|
190
|
-
adminSecret,
|
|
191
189
|
internalApiKey
|
|
192
190
|
};
|
|
193
191
|
}
|
|
@@ -336,13 +334,17 @@ function renderEnvWeb(c) {
|
|
|
336
334
|
`SESSION_SECRET=${c.sessionSecret}`,
|
|
337
335
|
``,
|
|
338
336
|
`# Admin`,
|
|
339
|
-
`ADMIN_SECRET=${c.adminSecret}`,
|
|
340
337
|
`ADMIN_EMAIL=${c.adminEmail}`,
|
|
341
338
|
`ADMIN_PASSWORD=${c.adminPassword}`,
|
|
342
339
|
``,
|
|
343
340
|
`# Internal API key (shared with engine for analytics)`,
|
|
344
341
|
`INTERNAL_API_KEY=${c.internalApiKey}`,
|
|
345
342
|
``,
|
|
343
|
+
`# License`,
|
|
344
|
+
`LICENSE_KEY=${c.licenseKey ?? ""}`,
|
|
345
|
+
`LICENSE_TIER=${c.licenseTier}`,
|
|
346
|
+
`LICENSE_API_URL=https://lead-routing-license.artyagi2011.workers.dev`,
|
|
347
|
+
``,
|
|
346
348
|
`# Email (optional)`,
|
|
347
349
|
`RESEND_API_KEY=${c.resendApiKey ?? ""}`,
|
|
348
350
|
`FEEDBACK_TO_EMAIL=${c.feedbackToEmail ?? ""}`
|
|
@@ -370,7 +372,12 @@ function renderEnvEngine(c) {
|
|
|
370
372
|
`ENGINE_WEBHOOK_SECRET=${c.engineWebhookSecret}`,
|
|
371
373
|
``,
|
|
372
374
|
`# Internal API key (Bearer token for analytics endpoints)`,
|
|
373
|
-
`INTERNAL_API_KEY=${c.internalApiKey}
|
|
375
|
+
`INTERNAL_API_KEY=${c.internalApiKey}`,
|
|
376
|
+
``,
|
|
377
|
+
`# License`,
|
|
378
|
+
`LICENSE_KEY=${c.licenseKey ?? ""}`,
|
|
379
|
+
`LICENSE_TIER=${c.licenseTier}`,
|
|
380
|
+
`LICENSE_API_URL=https://lead-routing-license.artyagi2011.workers.dev`
|
|
374
381
|
].join("\n");
|
|
375
382
|
}
|
|
376
383
|
|
|
@@ -411,6 +418,14 @@ function renderCaddyfile(appUrl, engineUrl) {
|
|
|
411
418
|
` health_uri /health`,
|
|
412
419
|
` health_interval 15s`,
|
|
413
420
|
` }`,
|
|
421
|
+
`}`,
|
|
422
|
+
``,
|
|
423
|
+
`# Marketing site \u2014 served independently`,
|
|
424
|
+
`openedgeai.tech {`,
|
|
425
|
+
` import security_headers`,
|
|
426
|
+
` root * /srv/marketing-site`,
|
|
427
|
+
` file_server`,
|
|
428
|
+
` try_files {path} /index.html`,
|
|
414
429
|
`}`
|
|
415
430
|
].join("\n");
|
|
416
431
|
}
|
|
@@ -443,6 +458,14 @@ function renderCaddyfile(appUrl, engineUrl) {
|
|
|
443
458
|
` health_uri /health`,
|
|
444
459
|
` health_interval 15s`,
|
|
445
460
|
` }`,
|
|
461
|
+
`}`,
|
|
462
|
+
``,
|
|
463
|
+
`# Marketing site \u2014 served independently`,
|
|
464
|
+
`openedgeai.tech {`,
|
|
465
|
+
` import security_headers`,
|
|
466
|
+
` root * /srv/marketing-site`,
|
|
467
|
+
` file_server`,
|
|
468
|
+
` try_files {path} /index.html`,
|
|
446
469
|
`}`
|
|
447
470
|
].join("\n");
|
|
448
471
|
}
|
|
@@ -483,7 +506,7 @@ function getCliVersion() {
|
|
|
483
506
|
return "0.1.0";
|
|
484
507
|
}
|
|
485
508
|
}
|
|
486
|
-
function generateFiles(cfg, sshCfg) {
|
|
509
|
+
function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
|
|
487
510
|
const dir = join2(process.cwd(), "lead-routing");
|
|
488
511
|
mkdirSync(dir, { recursive: true });
|
|
489
512
|
const dockerEngineUrl = `http://engine:3001`;
|
|
@@ -507,12 +530,13 @@ function generateFiles(cfg, sshCfg) {
|
|
|
507
530
|
redisUrl: cfg.redisUrl,
|
|
508
531
|
sessionSecret: cfg.sessionSecret,
|
|
509
532
|
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
510
|
-
adminSecret: cfg.adminSecret,
|
|
511
533
|
adminEmail: cfg.adminEmail,
|
|
512
534
|
adminPassword: cfg.adminPassword,
|
|
513
535
|
internalApiKey: cfg.internalApiKey,
|
|
514
536
|
resendApiKey: cfg.resendApiKey || void 0,
|
|
515
|
-
feedbackToEmail: cfg.feedbackToEmail || void 0
|
|
537
|
+
feedbackToEmail: cfg.feedbackToEmail || void 0,
|
|
538
|
+
licenseKey: license.licenseKey,
|
|
539
|
+
licenseTier: license.licenseTier
|
|
516
540
|
});
|
|
517
541
|
const envWeb = join2(dir, ".env.web");
|
|
518
542
|
writeFileSync2(envWeb, envWebContent, "utf8");
|
|
@@ -521,7 +545,9 @@ function generateFiles(cfg, sshCfg) {
|
|
|
521
545
|
databaseUrl: cfg.databaseUrl,
|
|
522
546
|
redisUrl: cfg.redisUrl,
|
|
523
547
|
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
524
|
-
internalApiKey: cfg.internalApiKey
|
|
548
|
+
internalApiKey: cfg.internalApiKey,
|
|
549
|
+
licenseKey: license.licenseKey,
|
|
550
|
+
licenseTier: license.licenseTier
|
|
525
551
|
});
|
|
526
552
|
const envEngine = join2(dir, ".env.engine");
|
|
527
553
|
writeFileSync2(envEngine, envEngineContent, "utf8");
|
|
@@ -543,11 +569,13 @@ function generateFiles(cfg, sshCfg) {
|
|
|
543
569
|
redis: cfg.managedRedis
|
|
544
570
|
},
|
|
545
571
|
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
572
|
+
licenseKey: license.licenseKey,
|
|
573
|
+
licenseTier: license.licenseTier,
|
|
546
574
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
547
575
|
version: getCliVersion()
|
|
548
576
|
});
|
|
549
577
|
log3.success("Generated lead-routing.json");
|
|
550
|
-
return { dir, composeFile, envWeb, envEngine
|
|
578
|
+
return { dir, composeFile, envWeb, envEngine };
|
|
551
579
|
}
|
|
552
580
|
|
|
553
581
|
// src/steps/check-remote-prerequisites.ts
|
|
@@ -1001,6 +1029,104 @@ ${result.stderr || result.stdout}`
|
|
|
1001
1029
|
}
|
|
1002
1030
|
};
|
|
1003
1031
|
|
|
1032
|
+
// src/utils/auth.ts
|
|
1033
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
1034
|
+
import { homedir as homedir2 } from "os";
|
|
1035
|
+
import { join as join4 } from "path";
|
|
1036
|
+
|
|
1037
|
+
// src/utils/license.ts
|
|
1038
|
+
import chalk from "chalk";
|
|
1039
|
+
var LICENSE_API_URL = process.env.LICENSE_API_URL || "https://lead-routing-license.artyagi2011.workers.dev";
|
|
1040
|
+
function formatTierBadge(tier) {
|
|
1041
|
+
if (tier === "pro") return chalk.bgGreen.black(" PRO ");
|
|
1042
|
+
return chalk.bgGray.white(" FREE ");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/utils/auth.ts
|
|
1046
|
+
var CRED_DIR = join4(homedir2(), ".lead-routing");
|
|
1047
|
+
var CRED_FILE = join4(CRED_DIR, "credentials.json");
|
|
1048
|
+
function loadCredentials() {
|
|
1049
|
+
try {
|
|
1050
|
+
const data = readFileSync3(CRED_FILE, "utf-8");
|
|
1051
|
+
return JSON.parse(data);
|
|
1052
|
+
} catch {
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function saveCredentials(creds) {
|
|
1057
|
+
mkdirSync2(CRED_DIR, { recursive: true });
|
|
1058
|
+
writeFileSync3(CRED_FILE, JSON.stringify(creds, null, 2));
|
|
1059
|
+
}
|
|
1060
|
+
function clearCredentials() {
|
|
1061
|
+
try {
|
|
1062
|
+
writeFileSync3(CRED_FILE, "");
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async function requireAuth() {
|
|
1067
|
+
const creds = loadCredentials();
|
|
1068
|
+
if (!creds?.token) {
|
|
1069
|
+
throw new Error("Not logged in. Run `lead-routing login` first.");
|
|
1070
|
+
}
|
|
1071
|
+
const res = await fetch(`${LICENSE_API_URL}/v1/auth/me`, {
|
|
1072
|
+
headers: { "Authorization": `Bearer ${creds.token}` },
|
|
1073
|
+
signal: AbortSignal.timeout(1e4)
|
|
1074
|
+
});
|
|
1075
|
+
if (!res.ok) {
|
|
1076
|
+
clearCredentials();
|
|
1077
|
+
throw new Error("Session expired. Run `lead-routing login` to sign in again.");
|
|
1078
|
+
}
|
|
1079
|
+
const data = await res.json();
|
|
1080
|
+
if (!data.customer.emailVerified) {
|
|
1081
|
+
throw new Error("Email not verified. Check your inbox for the verification link.");
|
|
1082
|
+
}
|
|
1083
|
+
const updated = {
|
|
1084
|
+
token: creds.token,
|
|
1085
|
+
customer: data.customer,
|
|
1086
|
+
storedAt: creds.storedAt
|
|
1087
|
+
};
|
|
1088
|
+
saveCredentials(updated);
|
|
1089
|
+
return updated;
|
|
1090
|
+
}
|
|
1091
|
+
async function apiLogin(email, password5) {
|
|
1092
|
+
const res = await fetch(`${LICENSE_API_URL}/v1/auth/login`, {
|
|
1093
|
+
method: "POST",
|
|
1094
|
+
headers: { "Content-Type": "application/json" },
|
|
1095
|
+
body: JSON.stringify({ email, password: password5 }),
|
|
1096
|
+
signal: AbortSignal.timeout(1e4)
|
|
1097
|
+
});
|
|
1098
|
+
if (!res.ok) {
|
|
1099
|
+
const body = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
1100
|
+
throw new Error(body.error || "Login failed");
|
|
1101
|
+
}
|
|
1102
|
+
return await res.json();
|
|
1103
|
+
}
|
|
1104
|
+
async function apiSignup(data) {
|
|
1105
|
+
const res = await fetch(`${LICENSE_API_URL}/v1/auth/signup`, {
|
|
1106
|
+
method: "POST",
|
|
1107
|
+
headers: { "Content-Type": "application/json" },
|
|
1108
|
+
body: JSON.stringify(data),
|
|
1109
|
+
signal: AbortSignal.timeout(1e4)
|
|
1110
|
+
});
|
|
1111
|
+
if (!res.ok) {
|
|
1112
|
+
const body = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
1113
|
+
throw new Error(body.error || "Signup failed");
|
|
1114
|
+
}
|
|
1115
|
+
const result = await res.json();
|
|
1116
|
+
return result.message;
|
|
1117
|
+
}
|
|
1118
|
+
async function apiResendVerification(token) {
|
|
1119
|
+
const res = await fetch(`${LICENSE_API_URL}/v1/auth/resend-verification`, {
|
|
1120
|
+
method: "POST",
|
|
1121
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
1122
|
+
signal: AbortSignal.timeout(1e4)
|
|
1123
|
+
});
|
|
1124
|
+
if (!res.ok) {
|
|
1125
|
+
const body = await res.json().catch(() => ({ error: "Failed" }));
|
|
1126
|
+
throw new Error(body.error || "Failed to resend");
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1004
1130
|
// src/commands/init.ts
|
|
1005
1131
|
var MANAGED_PACKAGE_INSTALL_URL = "https://login.salesforce.com/packaging/installPackage.apexp?p0=04tgL000000CTnp";
|
|
1006
1132
|
function openBrowser(url) {
|
|
@@ -1019,7 +1145,7 @@ async function checkDnsResolvable(appUrl, engineUrl) {
|
|
|
1019
1145
|
await dns.lookup(host);
|
|
1020
1146
|
} catch {
|
|
1021
1147
|
log7.warn(
|
|
1022
|
-
`${
|
|
1148
|
+
`${chalk2.yellow(host)} does not resolve in DNS yet.
|
|
1023
1149
|
Check for typos \u2014 a bad domain will cause a 2-minute timeout at step 7.`
|
|
1024
1150
|
);
|
|
1025
1151
|
const go = await confirm({ message: "Continue anyway?", initialValue: true });
|
|
@@ -1035,7 +1161,7 @@ async function runInit(options = {}) {
|
|
|
1035
1161
|
const resume = options.resume ?? false;
|
|
1036
1162
|
console.log();
|
|
1037
1163
|
intro(
|
|
1038
|
-
|
|
1164
|
+
chalk2.bold.cyan("Lead Routing \u2014 Self-Hosted Setup") + (dryRun ? chalk2.yellow(" [dry run]") : "") + (resume ? chalk2.yellow(" [resume]") : "")
|
|
1039
1165
|
);
|
|
1040
1166
|
const ssh = new SshConnection();
|
|
1041
1167
|
if (resume) {
|
|
@@ -1067,7 +1193,7 @@ async function runInit(options = {}) {
|
|
|
1067
1193
|
const remoteDir = await ssh.resolveHome(saved.remoteDir);
|
|
1068
1194
|
log7.step("Verifying health");
|
|
1069
1195
|
await verifyHealth(saved.appUrl, saved.engineUrl, ssh, remoteDir);
|
|
1070
|
-
outro(
|
|
1196
|
+
outro(chalk2.green("\u2714 Services are healthy!"));
|
|
1071
1197
|
} catch (err) {
|
|
1072
1198
|
const message = err instanceof Error ? err.message : String(err);
|
|
1073
1199
|
log7.error(`Resume failed: ${message}`);
|
|
@@ -1077,18 +1203,35 @@ async function runInit(options = {}) {
|
|
|
1077
1203
|
}
|
|
1078
1204
|
return;
|
|
1079
1205
|
}
|
|
1206
|
+
let auth;
|
|
1207
|
+
try {
|
|
1208
|
+
auth = await requireAuth();
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
log7.error(err instanceof Error ? err.message : "Authentication required");
|
|
1211
|
+
note3(
|
|
1212
|
+
`Run one of the following:
|
|
1213
|
+
|
|
1214
|
+
${chalk2.cyan("lead-routing signup")} Create a new account
|
|
1215
|
+
${chalk2.cyan("lead-routing login")} Log in to existing account`,
|
|
1216
|
+
"Account Required"
|
|
1217
|
+
);
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
log7.success(`Logged in as ${auth.customer.firstName} ${auth.customer.lastName} \u2014 ${formatTierBadge(auth.customer.tier)}`);
|
|
1080
1221
|
try {
|
|
1081
|
-
log7.step("Step 1/
|
|
1222
|
+
log7.step("Step 1/9 License validation");
|
|
1223
|
+
const licenseResult = { tier: auth.customer.tier, key: void 0 };
|
|
1224
|
+
log7.step("Step 2/9 Install Salesforce Package");
|
|
1082
1225
|
note3(
|
|
1083
1226
|
`The Lead Router managed package installs the required Connected App,
|
|
1084
1227
|
triggers, and custom objects in your Salesforce org.
|
|
1085
1228
|
|
|
1086
|
-
Install URL: ${
|
|
1229
|
+
Install URL: ${chalk2.cyan(MANAGED_PACKAGE_INSTALL_URL)}`,
|
|
1087
1230
|
"Salesforce Package"
|
|
1088
1231
|
);
|
|
1089
1232
|
log7.info("Opening install URL in your browser...");
|
|
1090
1233
|
openBrowser(MANAGED_PACKAGE_INSTALL_URL);
|
|
1091
|
-
log7.info(`${
|
|
1234
|
+
log7.info(`${chalk2.dim("If the browser didn't open, visit the URL above manually.")}`);
|
|
1092
1235
|
const installed = await confirm({
|
|
1093
1236
|
message: 'Have you installed the package? (Click "Install for All Users" in Salesforce)',
|
|
1094
1237
|
initialValue: false
|
|
@@ -1102,9 +1245,9 @@ Install URL: ${chalk.cyan(MANAGED_PACKAGE_INSTALL_URL)}`,
|
|
|
1102
1245
|
} else {
|
|
1103
1246
|
log7.success("Salesforce package installed");
|
|
1104
1247
|
}
|
|
1105
|
-
log7.step("Step
|
|
1248
|
+
log7.step("Step 3/9 Checking local prerequisites");
|
|
1106
1249
|
await checkPrerequisites();
|
|
1107
|
-
log7.step("Step
|
|
1250
|
+
log7.step("Step 4/9 SSH connection");
|
|
1108
1251
|
const sshCfg = await collectSshConfig({
|
|
1109
1252
|
sshPort: options.sshPort,
|
|
1110
1253
|
sshUser: options.sshUser,
|
|
@@ -1121,42 +1264,45 @@ Install URL: ${chalk.cyan(MANAGED_PACKAGE_INSTALL_URL)}`,
|
|
|
1121
1264
|
process.exit(1);
|
|
1122
1265
|
}
|
|
1123
1266
|
}
|
|
1124
|
-
log7.step("Step
|
|
1267
|
+
log7.step("Step 5/9 Configuration");
|
|
1125
1268
|
const cfg = await collectConfig({
|
|
1126
1269
|
externalDb: options.externalDb,
|
|
1127
1270
|
externalRedis: options.externalRedis
|
|
1128
1271
|
});
|
|
1129
1272
|
await checkDnsResolvable(cfg.appUrl, cfg.engineUrl);
|
|
1130
|
-
log7.step("Step
|
|
1131
|
-
const { dir
|
|
1273
|
+
log7.step("Step 6/9 Generating config files");
|
|
1274
|
+
const { dir } = generateFiles(cfg, sshCfg, {
|
|
1275
|
+
licenseKey: licenseResult.key,
|
|
1276
|
+
licenseTier: licenseResult.tier
|
|
1277
|
+
});
|
|
1132
1278
|
note3(
|
|
1133
|
-
`Local config directory: ${
|
|
1279
|
+
`Local config directory: ${chalk2.cyan(dir)}
|
|
1134
1280
|
Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routing.json`,
|
|
1135
1281
|
"Files"
|
|
1136
1282
|
);
|
|
1137
1283
|
if (dryRun) {
|
|
1138
1284
|
outro(
|
|
1139
|
-
|
|
1285
|
+
chalk2.yellow("Dry run complete \u2014 no connection made, no services started.") + `
|
|
1140
1286
|
|
|
1141
|
-
Config files written to: ${
|
|
1287
|
+
Config files written to: ${chalk2.cyan(dir)}
|
|
1142
1288
|
|
|
1143
|
-
When ready, run ${
|
|
1289
|
+
When ready, run ${chalk2.cyan("lead-routing init")} (without --dry-run) to deploy.`
|
|
1144
1290
|
);
|
|
1145
1291
|
return;
|
|
1146
1292
|
}
|
|
1147
|
-
log7.step("Step
|
|
1293
|
+
log7.step("Step 7/9 Remote setup");
|
|
1148
1294
|
const remoteDir = await ssh.resolveHome(sshCfg.remoteDir);
|
|
1149
1295
|
await checkRemotePrerequisites(ssh);
|
|
1150
1296
|
await uploadFiles(ssh, dir, remoteDir);
|
|
1151
|
-
log7.step("Step
|
|
1297
|
+
log7.step("Step 8/9 Starting services");
|
|
1152
1298
|
await startServices(ssh, remoteDir);
|
|
1153
|
-
log7.step("Step
|
|
1299
|
+
log7.step("Step 9/9 Verifying health");
|
|
1154
1300
|
await verifyHealth(cfg.appUrl, cfg.engineUrl, ssh, remoteDir);
|
|
1155
1301
|
try {
|
|
1156
|
-
const envWebPath =
|
|
1157
|
-
const envContent =
|
|
1302
|
+
const envWebPath = join5(dir, ".env.web");
|
|
1303
|
+
const envContent = readFileSync4(envWebPath, "utf-8");
|
|
1158
1304
|
const cleaned = envContent.split("\n").filter((line) => !line.startsWith("ADMIN_PASSWORD=")).join("\n");
|
|
1159
|
-
|
|
1305
|
+
writeFileSync4(envWebPath, cleaned, "utf-8");
|
|
1160
1306
|
log7.success("Removed ADMIN_PASSWORD from .env.web (no longer needed after seed)");
|
|
1161
1307
|
} catch {
|
|
1162
1308
|
}
|
|
@@ -1166,22 +1312,20 @@ The managed package is already installed \u2014 just click "Connect Salesforce"
|
|
|
1166
1312
|
"Next: Connect Salesforce"
|
|
1167
1313
|
);
|
|
1168
1314
|
outro(
|
|
1169
|
-
|
|
1315
|
+
chalk2.green("\u2714 You're live!") + `
|
|
1170
1316
|
|
|
1171
|
-
Dashboard: ${
|
|
1172
|
-
Routing engine: ${
|
|
1317
|
+
Dashboard: ${chalk2.cyan(cfg.appUrl)}
|
|
1318
|
+
Routing engine: ${chalk2.cyan(cfg.engineUrl)}
|
|
1173
1319
|
|
|
1174
|
-
Admin email: ${
|
|
1175
|
-
Admin secret: ${chalk.yellow(adminSecret)}
|
|
1176
|
-
${chalk.dim("run `lead-routing config show` to retrieve later")}
|
|
1320
|
+
Admin email: ${chalk2.white(cfg.adminEmail)}
|
|
1177
1321
|
|
|
1178
|
-
` +
|
|
1179
|
-
${
|
|
1180
|
-
${
|
|
1181
|
-
${
|
|
1322
|
+
` + chalk2.bold(" Next steps:\n") + ` ${chalk2.cyan("1.")} Open ${chalk2.cyan(cfg.appUrl)} and log in
|
|
1323
|
+
${chalk2.cyan("2.")} Go to Integrations \u2192 Salesforce \u2192 Connect
|
|
1324
|
+
${chalk2.cyan("3.")} Complete the onboarding wizard in Salesforce
|
|
1325
|
+
${chalk2.cyan("4.")} Create your first routing rule
|
|
1182
1326
|
|
|
1183
|
-
Run ${
|
|
1184
|
-
Run ${
|
|
1327
|
+
Run ${chalk2.cyan("lead-routing doctor")} to check service health at any time.
|
|
1328
|
+
Run ${chalk2.cyan("lead-routing deploy")} to update to a new version.`
|
|
1185
1329
|
);
|
|
1186
1330
|
} catch (err) {
|
|
1187
1331
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1193,14 +1337,14 @@ The managed package is already installed \u2014 just click "Connect Salesforce"
|
|
|
1193
1337
|
}
|
|
1194
1338
|
|
|
1195
1339
|
// src/commands/deploy.ts
|
|
1196
|
-
import { writeFileSync as
|
|
1197
|
-
import { join as
|
|
1340
|
+
import { writeFileSync as writeFileSync5, unlinkSync } from "fs";
|
|
1341
|
+
import { join as join6 } from "path";
|
|
1198
1342
|
import { tmpdir } from "os";
|
|
1199
1343
|
import { intro as intro2, outro as outro2, log as log8, password as promptPassword2 } from "@clack/prompts";
|
|
1200
|
-
import
|
|
1344
|
+
import chalk3 from "chalk";
|
|
1201
1345
|
async function runDeploy() {
|
|
1202
1346
|
console.log();
|
|
1203
|
-
intro2(
|
|
1347
|
+
intro2(chalk3.bold.cyan("Lead Routing \u2014 Deploy"));
|
|
1204
1348
|
const dir = findInstallDir();
|
|
1205
1349
|
if (!dir) {
|
|
1206
1350
|
log8.error(
|
|
@@ -1236,8 +1380,8 @@ async function runDeploy() {
|
|
|
1236
1380
|
const remoteDir = await ssh.resolveHome(cfg.remoteDir);
|
|
1237
1381
|
log8.step("Syncing Caddyfile");
|
|
1238
1382
|
const caddyContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl);
|
|
1239
|
-
const tmpCaddy =
|
|
1240
|
-
|
|
1383
|
+
const tmpCaddy = join6(tmpdir(), "lead-routing-Caddyfile");
|
|
1384
|
+
writeFileSync5(tmpCaddy, caddyContent, "utf8");
|
|
1241
1385
|
await ssh.upload([{ local: tmpCaddy, remote: `${remoteDir}/Caddyfile` }]);
|
|
1242
1386
|
unlinkSync(tmpCaddy);
|
|
1243
1387
|
await ssh.exec("docker compose restart caddy", remoteDir);
|
|
@@ -1249,9 +1393,9 @@ async function runDeploy() {
|
|
|
1249
1393
|
await ssh.exec("docker compose up -d --remove-orphans", remoteDir);
|
|
1250
1394
|
log8.success("Services restarted");
|
|
1251
1395
|
outro2(
|
|
1252
|
-
|
|
1396
|
+
chalk3.green("\u2714 Deployment complete!") + `
|
|
1253
1397
|
|
|
1254
|
-
${
|
|
1398
|
+
${chalk3.cyan(cfg.appUrl)}`
|
|
1255
1399
|
);
|
|
1256
1400
|
} catch (err) {
|
|
1257
1401
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1264,11 +1408,11 @@ async function runDeploy() {
|
|
|
1264
1408
|
|
|
1265
1409
|
// src/commands/doctor.ts
|
|
1266
1410
|
import { intro as intro3, outro as outro3, log as log9 } from "@clack/prompts";
|
|
1267
|
-
import
|
|
1411
|
+
import chalk4 from "chalk";
|
|
1268
1412
|
import { execa } from "execa";
|
|
1269
1413
|
async function runDoctor() {
|
|
1270
1414
|
console.log();
|
|
1271
|
-
intro3(
|
|
1415
|
+
intro3(chalk4.bold.cyan("Lead Routing \u2014 Health Check"));
|
|
1272
1416
|
const dir = findInstallDir();
|
|
1273
1417
|
if (!dir) {
|
|
1274
1418
|
log9.error("No lead-routing.json found. Run `lead-routing init` first.");
|
|
@@ -1287,17 +1431,17 @@ async function runDoctor() {
|
|
|
1287
1431
|
checks.push(await checkEndpoint("Routing engine", `${cfg.engineUrl}/health`));
|
|
1288
1432
|
console.log();
|
|
1289
1433
|
for (const c of checks) {
|
|
1290
|
-
const icon = c.pass ?
|
|
1291
|
-
const label = c.pass ?
|
|
1292
|
-
const detail = c.detail ?
|
|
1434
|
+
const icon = c.pass ? chalk4.green("\u2714") : chalk4.red("\u2717");
|
|
1435
|
+
const label = c.pass ? chalk4.white(c.label) : chalk4.red(c.label);
|
|
1436
|
+
const detail = c.detail ? chalk4.dim(` \u2014 ${c.detail}`) : "";
|
|
1293
1437
|
console.log(` ${icon} ${label}${detail}`);
|
|
1294
1438
|
}
|
|
1295
1439
|
console.log();
|
|
1296
1440
|
const failed = checks.filter((c) => !c.pass);
|
|
1297
1441
|
if (failed.length === 0) {
|
|
1298
|
-
outro3(
|
|
1442
|
+
outro3(chalk4.green("All checks passed"));
|
|
1299
1443
|
} else {
|
|
1300
|
-
outro3(
|
|
1444
|
+
outro3(chalk4.yellow(`${failed.length} check(s) failed`));
|
|
1301
1445
|
process.exit(1);
|
|
1302
1446
|
}
|
|
1303
1447
|
}
|
|
@@ -1399,14 +1543,14 @@ async function runStatus() {
|
|
|
1399
1543
|
}
|
|
1400
1544
|
|
|
1401
1545
|
// src/commands/config.ts
|
|
1402
|
-
import { readFileSync as
|
|
1403
|
-
import { join as
|
|
1546
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync3 } from "fs";
|
|
1547
|
+
import { join as join7 } from "path";
|
|
1404
1548
|
import { intro as intro4, outro as outro4, log as log12 } from "@clack/prompts";
|
|
1405
|
-
import
|
|
1549
|
+
import chalk5 from "chalk";
|
|
1406
1550
|
function parseEnv(filePath) {
|
|
1407
1551
|
const map = /* @__PURE__ */ new Map();
|
|
1408
1552
|
if (!existsSync3(filePath)) return map;
|
|
1409
|
-
for (const line of
|
|
1553
|
+
for (const line of readFileSync5(filePath, "utf8").split("\n")) {
|
|
1410
1554
|
const trimmed = line.trim();
|
|
1411
1555
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1412
1556
|
const eq = trimmed.indexOf("=");
|
|
@@ -1434,25 +1578,23 @@ function runConfigShow() {
|
|
|
1434
1578
|
console.error("No lead-routing installation found in the current directory.");
|
|
1435
1579
|
process.exit(1);
|
|
1436
1580
|
}
|
|
1437
|
-
const envWeb =
|
|
1581
|
+
const envWeb = join7(dir, ".env.web");
|
|
1438
1582
|
const cfg = parseEnv(envWeb);
|
|
1439
|
-
const adminSecret = cfg.get("ADMIN_SECRET") ?? "(not found)";
|
|
1440
1583
|
const appUrl = cfg.get("APP_URL") ?? "(not found)";
|
|
1441
1584
|
console.log();
|
|
1442
|
-
console.log(
|
|
1585
|
+
console.log(chalk5.bold("Lead Routing \u2014 Installation Config"));
|
|
1443
1586
|
console.log();
|
|
1444
|
-
console.log(`
|
|
1445
|
-
console.log(` Admin secret: ${chalk4.yellow(adminSecret)}`);
|
|
1587
|
+
console.log(` Dashboard: ${chalk5.cyan(appUrl)}`);
|
|
1446
1588
|
console.log();
|
|
1447
1589
|
}
|
|
1448
1590
|
|
|
1449
1591
|
// src/commands/sfdc.ts
|
|
1450
1592
|
import { intro as intro5, outro as outro5, text as text3, log as log15 } from "@clack/prompts";
|
|
1451
|
-
import
|
|
1593
|
+
import chalk7 from "chalk";
|
|
1452
1594
|
|
|
1453
1595
|
// src/steps/sfdc-deploy-inline.ts
|
|
1454
|
-
import { readFileSync as
|
|
1455
|
-
import { join as
|
|
1596
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync5, cpSync, rmSync } from "fs";
|
|
1597
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
1456
1598
|
import { tmpdir as tmpdir2 } from "os";
|
|
1457
1599
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1458
1600
|
import { execSync } from "child_process";
|
|
@@ -1577,9 +1719,9 @@ Content-Type: application/zip\r
|
|
|
1577
1719
|
body
|
|
1578
1720
|
});
|
|
1579
1721
|
if (!res.ok) {
|
|
1580
|
-
const
|
|
1722
|
+
const text6 = await res.text();
|
|
1581
1723
|
throw new Error(
|
|
1582
|
-
`Metadata deploy request failed (${res.status}): ${
|
|
1724
|
+
`Metadata deploy request failed (${res.status}): ${text6}`
|
|
1583
1725
|
);
|
|
1584
1726
|
}
|
|
1585
1727
|
const result = await res.json();
|
|
@@ -1596,9 +1738,9 @@ Content-Type: application/zip\r
|
|
|
1596
1738
|
const url = `${this.baseUrl}/metadata/deployRequest/${deployId}?includeDetails=true`;
|
|
1597
1739
|
const res = await fetch(url, { headers: this.headers() });
|
|
1598
1740
|
if (!res.ok) {
|
|
1599
|
-
const
|
|
1741
|
+
const text6 = await res.text();
|
|
1600
1742
|
throw new Error(
|
|
1601
|
-
`Deploy status check failed (${res.status}): ${
|
|
1743
|
+
`Deploy status check failed (${res.status}): ${text6}`
|
|
1602
1744
|
);
|
|
1603
1745
|
}
|
|
1604
1746
|
const data = await res.json();
|
|
@@ -1619,8 +1761,8 @@ var DuplicateError = class extends Error {
|
|
|
1619
1761
|
};
|
|
1620
1762
|
|
|
1621
1763
|
// src/utils/zip-source.ts
|
|
1622
|
-
import { join as
|
|
1623
|
-
import { readdirSync, readFileSync as
|
|
1764
|
+
import { join as join8 } from "path";
|
|
1765
|
+
import { readdirSync, readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
|
|
1624
1766
|
import archiver from "archiver";
|
|
1625
1767
|
var META_TYPE_MAP = {
|
|
1626
1768
|
applications: "CustomApplication",
|
|
@@ -1633,10 +1775,10 @@ var META_TYPE_MAP = {
|
|
|
1633
1775
|
tabs: "CustomTab"
|
|
1634
1776
|
};
|
|
1635
1777
|
async function zipSourcePackage(packageDir) {
|
|
1636
|
-
const forceAppDefault =
|
|
1778
|
+
const forceAppDefault = join8(packageDir, "force-app", "main", "default");
|
|
1637
1779
|
let apiVersion = "59.0";
|
|
1638
1780
|
try {
|
|
1639
|
-
const proj = JSON.parse(
|
|
1781
|
+
const proj = JSON.parse(readFileSync6(join8(packageDir, "sfdx-project.json"), "utf8"));
|
|
1640
1782
|
if (proj.sourceApiVersion) apiVersion = proj.sourceApiVersion;
|
|
1641
1783
|
} catch {
|
|
1642
1784
|
}
|
|
@@ -1652,15 +1794,15 @@ async function zipSourcePackage(packageDir) {
|
|
|
1652
1794
|
archive.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1653
1795
|
archive.on("error", reject);
|
|
1654
1796
|
for (const [dirName, metaType] of Object.entries(META_TYPE_MAP)) {
|
|
1655
|
-
const srcDir =
|
|
1797
|
+
const srcDir = join8(forceAppDefault, dirName);
|
|
1656
1798
|
if (!existsSync4(srcDir)) continue;
|
|
1657
1799
|
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
1658
1800
|
for (const entry of entries) {
|
|
1659
1801
|
if (dirName === "lwc" && entry.isDirectory()) {
|
|
1660
1802
|
addMember(metaType, entry.name);
|
|
1661
|
-
archive.directory(
|
|
1803
|
+
archive.directory(join8(srcDir, entry.name), `${dirName}/${entry.name}`);
|
|
1662
1804
|
} else if (entry.isFile()) {
|
|
1663
|
-
archive.file(
|
|
1805
|
+
archive.file(join8(srcDir, entry.name), { name: `${dirName}/${entry.name}` });
|
|
1664
1806
|
if (!entry.name.endsWith("-meta.xml")) {
|
|
1665
1807
|
const memberName = entry.name.replace(/\.[^.]+$/, "");
|
|
1666
1808
|
addMember(metaType, memberName);
|
|
@@ -1668,13 +1810,13 @@ async function zipSourcePackage(packageDir) {
|
|
|
1668
1810
|
}
|
|
1669
1811
|
}
|
|
1670
1812
|
}
|
|
1671
|
-
const objectsDir =
|
|
1813
|
+
const objectsDir = join8(forceAppDefault, "objects");
|
|
1672
1814
|
if (existsSync4(objectsDir)) {
|
|
1673
1815
|
for (const objEntry of readdirSync(objectsDir, { withFileTypes: true })) {
|
|
1674
1816
|
if (!objEntry.isDirectory()) continue;
|
|
1675
1817
|
const objName = objEntry.name;
|
|
1676
1818
|
addMember("CustomObject", objName);
|
|
1677
|
-
const objDir =
|
|
1819
|
+
const objDir = join8(objectsDir, objName);
|
|
1678
1820
|
const objectXml = mergeObjectXml(objDir, objName, apiVersion);
|
|
1679
1821
|
archive.append(Buffer.from(objectXml, "utf8"), {
|
|
1680
1822
|
name: `objects/${objName}.object`
|
|
@@ -1691,17 +1833,17 @@ function mergeObjectXml(objDir, objName, apiVersion) {
|
|
|
1691
1833
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1692
1834
|
'<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">'
|
|
1693
1835
|
];
|
|
1694
|
-
const objMetaPath =
|
|
1836
|
+
const objMetaPath = join8(objDir, `${objName}.object-meta.xml`);
|
|
1695
1837
|
if (existsSync4(objMetaPath)) {
|
|
1696
|
-
const content =
|
|
1838
|
+
const content = readFileSync6(objMetaPath, "utf8");
|
|
1697
1839
|
const inner = content.replace(/<\?xml[^?]*\?>\s*/g, "").replace(/<CustomObject[^>]*>/g, "").replace(/<\/CustomObject>/g, "").trim();
|
|
1698
1840
|
if (inner) lines.push(inner);
|
|
1699
1841
|
}
|
|
1700
|
-
const fieldsDir =
|
|
1842
|
+
const fieldsDir = join8(objDir, "fields");
|
|
1701
1843
|
if (existsSync4(fieldsDir)) {
|
|
1702
1844
|
for (const fieldFile of readdirSync(fieldsDir).sort()) {
|
|
1703
1845
|
if (!fieldFile.endsWith(".field-meta.xml")) continue;
|
|
1704
|
-
const content =
|
|
1846
|
+
const content = readFileSync6(join8(fieldsDir, fieldFile), "utf8");
|
|
1705
1847
|
const inner = content.replace(/<\?xml[^?]*\?>\s*/g, "").replace(/<CustomField[^>]*>/g, "").replace(/<\/CustomField>/g, "").trim();
|
|
1706
1848
|
if (inner) {
|
|
1707
1849
|
lines.push(" <fields>");
|
|
@@ -1743,10 +1885,10 @@ async function sfdcDeployInline(params) {
|
|
|
1743
1885
|
const { accessToken, instanceUrl } = await loginViaAppBridge(appUrl);
|
|
1744
1886
|
const sf = new SalesforceApi(instanceUrl, accessToken);
|
|
1745
1887
|
s.start("Copying Salesforce package\u2026");
|
|
1746
|
-
const inDist =
|
|
1747
|
-
const nextToDist =
|
|
1888
|
+
const inDist = join9(__dirname2, "sfdc-package");
|
|
1889
|
+
const nextToDist = join9(__dirname2, "..", "sfdc-package");
|
|
1748
1890
|
const bundledPkg = existsSync5(inDist) ? inDist : nextToDist;
|
|
1749
|
-
const destPkg =
|
|
1891
|
+
const destPkg = join9(installDir ?? tmpdir2(), "lead-routing-sfdc-package");
|
|
1750
1892
|
if (!existsSync5(bundledPkg)) {
|
|
1751
1893
|
s.stop("sfdc-package not found in CLI bundle");
|
|
1752
1894
|
throw new Error(
|
|
@@ -1757,7 +1899,7 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1757
1899
|
if (existsSync5(destPkg)) rmSync(destPkg, { recursive: true, force: true });
|
|
1758
1900
|
cpSync(bundledPkg, destPkg, { recursive: true });
|
|
1759
1901
|
s.stop("Package copied");
|
|
1760
|
-
const ncPath =
|
|
1902
|
+
const ncPath = join9(
|
|
1761
1903
|
destPkg,
|
|
1762
1904
|
"force-app",
|
|
1763
1905
|
"main",
|
|
@@ -1766,10 +1908,10 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1766
1908
|
"RoutingEngine.namedCredential-meta.xml"
|
|
1767
1909
|
);
|
|
1768
1910
|
if (existsSync5(ncPath)) {
|
|
1769
|
-
const nc = patchXml(
|
|
1770
|
-
|
|
1911
|
+
const nc = patchXml(readFileSync7(ncPath, "utf8"), "endpoint", engineUrl);
|
|
1912
|
+
writeFileSync7(ncPath, nc, "utf8");
|
|
1771
1913
|
}
|
|
1772
|
-
const rssEnginePath =
|
|
1914
|
+
const rssEnginePath = join9(
|
|
1773
1915
|
destPkg,
|
|
1774
1916
|
"force-app",
|
|
1775
1917
|
"main",
|
|
@@ -1778,11 +1920,11 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1778
1920
|
"LeadRouterEngine.remoteSite-meta.xml"
|
|
1779
1921
|
);
|
|
1780
1922
|
if (existsSync5(rssEnginePath)) {
|
|
1781
|
-
let rss = patchXml(
|
|
1923
|
+
let rss = patchXml(readFileSync7(rssEnginePath, "utf8"), "url", engineUrl);
|
|
1782
1924
|
rss = patchXml(rss, "description", "Lead Router Engine endpoint");
|
|
1783
|
-
|
|
1925
|
+
writeFileSync7(rssEnginePath, rss, "utf8");
|
|
1784
1926
|
}
|
|
1785
|
-
const rssAppPath =
|
|
1927
|
+
const rssAppPath = join9(
|
|
1786
1928
|
destPkg,
|
|
1787
1929
|
"force-app",
|
|
1788
1930
|
"main",
|
|
@@ -1791,9 +1933,9 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1791
1933
|
"LeadRouterApp.remoteSite-meta.xml"
|
|
1792
1934
|
);
|
|
1793
1935
|
if (existsSync5(rssAppPath)) {
|
|
1794
|
-
let rss = patchXml(
|
|
1936
|
+
let rss = patchXml(readFileSync7(rssAppPath, "utf8"), "url", appUrl);
|
|
1795
1937
|
rss = patchXml(rss, "description", "Lead Router App URL");
|
|
1796
|
-
|
|
1938
|
+
writeFileSync7(rssAppPath, rss, "utf8");
|
|
1797
1939
|
}
|
|
1798
1940
|
log13.success("Remote Site Settings patched");
|
|
1799
1941
|
s.start("Deploying Salesforce package (this may take ~2 min)\u2026");
|
|
@@ -1950,24 +2092,24 @@ Ensure the app is running and the URL is correct.`
|
|
|
1950
2092
|
|
|
1951
2093
|
// src/steps/app-launcher-guide.ts
|
|
1952
2094
|
import { note as note4, confirm as confirm2, isCancel as isCancel4, log as log14 } from "@clack/prompts";
|
|
1953
|
-
import
|
|
2095
|
+
import chalk6 from "chalk";
|
|
1954
2096
|
async function guideAppLauncherSetup(appUrl) {
|
|
1955
2097
|
note4(
|
|
1956
2098
|
`Complete the following steps in Salesforce now:
|
|
1957
2099
|
|
|
1958
|
-
${
|
|
1959
|
-
${
|
|
1960
|
-
${
|
|
1961
|
-
\u2192 You will be redirected to ${
|
|
2100
|
+
${chalk6.cyan("1.")} Open ${chalk6.bold("App Launcher")} (grid icon, top-left in Salesforce)
|
|
2101
|
+
${chalk6.cyan("2.")} Search for ${chalk6.white('"Lead Router Setup"')} and click it
|
|
2102
|
+
${chalk6.cyan("3.")} Click ${chalk6.white('"Connect to Lead Router"')}
|
|
2103
|
+
\u2192 You will be redirected to ${chalk6.dim(appUrl)} and back
|
|
1962
2104
|
\u2192 Authorize the OAuth connection when prompted
|
|
1963
2105
|
|
|
1964
|
-
${
|
|
1965
|
-
${
|
|
1966
|
-
${
|
|
1967
|
-
${
|
|
1968
|
-
\u2192 ${
|
|
2106
|
+
${chalk6.cyan("4.")} ${chalk6.bold("Step 1")} \u2014 wait for the ${chalk6.green('"Connected"')} checkmark (~5 sec)
|
|
2107
|
+
${chalk6.cyan("5.")} ${chalk6.bold("Step 2")} \u2014 click ${chalk6.white("Activate")} to enable Lead triggers
|
|
2108
|
+
${chalk6.cyan("6.")} ${chalk6.bold("Step 3")} \u2014 click ${chalk6.white("Sync Fields")} to index your Lead field schema
|
|
2109
|
+
${chalk6.cyan("7.")} ${chalk6.bold("Step 4")} \u2014 click ${chalk6.white("Send Test")} to fire a test routing event
|
|
2110
|
+
\u2192 ${chalk6.dim('"Test successful"')} or ${chalk6.dim('"No matching rule"')} are both valid
|
|
1969
2111
|
|
|
1970
|
-
` +
|
|
2112
|
+
` + chalk6.dim("Keep this terminal open while you complete the wizard."),
|
|
1971
2113
|
"Complete Salesforce setup"
|
|
1972
2114
|
);
|
|
1973
2115
|
const done = await confirm2({
|
|
@@ -2038,19 +2180,19 @@ async function runSfdcDeploy() {
|
|
|
2038
2180
|
}
|
|
2039
2181
|
await guideAppLauncherSetup(appUrl);
|
|
2040
2182
|
outro5(
|
|
2041
|
-
|
|
2183
|
+
chalk7.green("\u2714 Salesforce package deployed!") + `
|
|
2042
2184
|
|
|
2043
|
-
Your Lead Router dashboard: ${
|
|
2185
|
+
Your Lead Router dashboard: ${chalk7.cyan(appUrl)}`
|
|
2044
2186
|
);
|
|
2045
2187
|
}
|
|
2046
2188
|
|
|
2047
2189
|
// src/commands/uninstall.ts
|
|
2048
2190
|
import { rmSync as rmSync2, existsSync as existsSync6 } from "fs";
|
|
2049
2191
|
import { intro as intro6, outro as outro6, log as log16, confirm as confirm3, password as promptPassword3, isCancel as isCancel5 } from "@clack/prompts";
|
|
2050
|
-
import
|
|
2192
|
+
import chalk8 from "chalk";
|
|
2051
2193
|
async function runUninstall() {
|
|
2052
2194
|
console.log();
|
|
2053
|
-
intro6(
|
|
2195
|
+
intro6(chalk8.bold.red("Lead Routing \u2014 Uninstall"));
|
|
2054
2196
|
const dir = findInstallDir();
|
|
2055
2197
|
if (!dir) {
|
|
2056
2198
|
log16.error(
|
|
@@ -2065,13 +2207,13 @@ async function runUninstall() {
|
|
|
2065
2207
|
);
|
|
2066
2208
|
process.exit(1);
|
|
2067
2209
|
}
|
|
2068
|
-
log16.warn(
|
|
2210
|
+
log16.warn(chalk8.red("This will permanently destroy:"));
|
|
2069
2211
|
log16.warn(` \u2022 Remote: ${cfg.ssh.username}@${cfg.ssh.host}:${cfg.remoteDir}`);
|
|
2070
2212
|
log16.warn(" \u2514\u2500 All containers, Postgres data, Redis data, config files");
|
|
2071
2213
|
log16.warn(` \u2022 Local: ${dir}/`);
|
|
2072
2214
|
log16.warn(" \u2514\u2500 docker-compose.yml, .env.web, .env.engine, Caddyfile, lead-routing.json");
|
|
2073
2215
|
const confirmed = await confirm3({
|
|
2074
|
-
message:
|
|
2216
|
+
message: chalk8.bold("Are you sure you want to uninstall? This cannot be undone."),
|
|
2075
2217
|
initialValue: false
|
|
2076
2218
|
});
|
|
2077
2219
|
if (isCancel5(confirmed) || !confirmed) {
|
|
@@ -2126,12 +2268,96 @@ async function runUninstall() {
|
|
|
2126
2268
|
log16.success(`Removed ${dir}`);
|
|
2127
2269
|
}
|
|
2128
2270
|
outro6(
|
|
2129
|
-
|
|
2271
|
+
chalk8.green("\u2714 Uninstall complete.") + `
|
|
2130
2272
|
|
|
2131
|
-
Run ${
|
|
2273
|
+
Run ${chalk8.cyan("npx @lead-routing/cli init")} to start fresh.`
|
|
2132
2274
|
);
|
|
2133
2275
|
}
|
|
2134
2276
|
|
|
2277
|
+
// src/commands/signup.ts
|
|
2278
|
+
import * as p from "@clack/prompts";
|
|
2279
|
+
import chalk9 from "chalk";
|
|
2280
|
+
async function runSignup() {
|
|
2281
|
+
p.intro(chalk9.bgBlue.white(" lead-routing signup "));
|
|
2282
|
+
const firstName = await p.text({ message: "First name", placeholder: "John", validate: (v) => v.trim() ? void 0 : "Required" });
|
|
2283
|
+
if (p.isCancel(firstName)) process.exit(0);
|
|
2284
|
+
const lastName = await p.text({ message: "Last name", placeholder: "Smith", validate: (v) => v.trim() ? void 0 : "Required" });
|
|
2285
|
+
if (p.isCancel(lastName)) process.exit(0);
|
|
2286
|
+
const email = await p.text({ message: "Email", placeholder: "john@acme.com", validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()) ? void 0 : "Invalid email" });
|
|
2287
|
+
if (p.isCancel(email)) process.exit(0);
|
|
2288
|
+
const password5 = await p.password({ message: "Password (min 8 characters)", validate: (v) => v.length >= 8 ? void 0 : "Must be at least 8 characters" });
|
|
2289
|
+
if (p.isCancel(password5)) process.exit(0);
|
|
2290
|
+
const confirm5 = await p.password({ message: "Confirm password", validate: (v) => v === password5 ? void 0 : "Passwords do not match" });
|
|
2291
|
+
if (p.isCancel(confirm5)) process.exit(0);
|
|
2292
|
+
const spinner8 = p.spinner();
|
|
2293
|
+
spinner8.start("Creating account...");
|
|
2294
|
+
try {
|
|
2295
|
+
const message = await apiSignup({
|
|
2296
|
+
firstName: firstName.trim(),
|
|
2297
|
+
lastName: lastName.trim(),
|
|
2298
|
+
email: email.trim(),
|
|
2299
|
+
password: password5
|
|
2300
|
+
});
|
|
2301
|
+
spinner8.stop("Account created!");
|
|
2302
|
+
p.note(
|
|
2303
|
+
`Check your email (${email.trim()}) for a verification link.
|
|
2304
|
+
|
|
2305
|
+
After verifying, run:
|
|
2306
|
+
|
|
2307
|
+
${chalk9.cyan("lead-routing login")}`,
|
|
2308
|
+
"Next Steps"
|
|
2309
|
+
);
|
|
2310
|
+
} catch (err) {
|
|
2311
|
+
const msg = err instanceof Error ? err.message : "Signup failed";
|
|
2312
|
+
spinner8.stop(msg);
|
|
2313
|
+
if (msg.includes("already")) {
|
|
2314
|
+
p.log.info(`Try ${chalk9.cyan("lead-routing login")} instead.`);
|
|
2315
|
+
}
|
|
2316
|
+
process.exit(1);
|
|
2317
|
+
}
|
|
2318
|
+
p.outro("Done!");
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// src/commands/login.ts
|
|
2322
|
+
import * as p2 from "@clack/prompts";
|
|
2323
|
+
import chalk10 from "chalk";
|
|
2324
|
+
async function runLogin() {
|
|
2325
|
+
p2.intro(chalk10.bgBlue.white(" lead-routing login "));
|
|
2326
|
+
const email = await p2.text({ message: "Email", placeholder: "john@acme.com" });
|
|
2327
|
+
if (p2.isCancel(email)) process.exit(0);
|
|
2328
|
+
const password5 = await p2.password({ message: "Password" });
|
|
2329
|
+
if (p2.isCancel(password5)) process.exit(0);
|
|
2330
|
+
const spinner8 = p2.spinner();
|
|
2331
|
+
spinner8.start("Authenticating...");
|
|
2332
|
+
try {
|
|
2333
|
+
const { token, customer } = await apiLogin(email.trim(), password5);
|
|
2334
|
+
if (!customer.emailVerified) {
|
|
2335
|
+
spinner8.stop("Email not verified");
|
|
2336
|
+
p2.log.warn(`Your email (${customer.email}) hasn't been verified yet.`);
|
|
2337
|
+
const resend = await p2.confirm({ message: "Resend verification email?" });
|
|
2338
|
+
if (resend && !p2.isCancel(resend)) {
|
|
2339
|
+
try {
|
|
2340
|
+
await apiResendVerification(token);
|
|
2341
|
+
p2.log.success("Verification email sent! Check your inbox.");
|
|
2342
|
+
} catch {
|
|
2343
|
+
p2.log.error("Failed to resend. Try again later.");
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
p2.note(`Verify your email, then run:
|
|
2347
|
+
|
|
2348
|
+
${chalk10.cyan("lead-routing login")}`, "Next Steps");
|
|
2349
|
+
process.exit(1);
|
|
2350
|
+
}
|
|
2351
|
+
saveCredentials({ token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2352
|
+
spinner8.stop(`Logged in as ${customer.firstName} ${customer.lastName} \u2014 ${formatTierBadge(customer.tier)}`);
|
|
2353
|
+
p2.note(`Credentials saved to ~/.lead-routing/credentials.json`, "Saved");
|
|
2354
|
+
} catch (err) {
|
|
2355
|
+
spinner8.stop(err instanceof Error ? err.message : "Login failed");
|
|
2356
|
+
process.exit(1);
|
|
2357
|
+
}
|
|
2358
|
+
p2.outro("Done!");
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2135
2361
|
// src/index.ts
|
|
2136
2362
|
var program = new Command();
|
|
2137
2363
|
program.name("lead-routing").description("Self-hosted Lead Routing \u2014 scaffold, deploy, and manage your installation").version("0.1.13");
|
|
@@ -2150,11 +2376,13 @@ program.command("doctor").description("Check the health of all services in your
|
|
|
2150
2376
|
program.command("logs [service]").description("Stream logs from a service (web, engine, postgres, redis). Defaults to engine.").action((service) => runLogs(service));
|
|
2151
2377
|
program.command("status").description("Show the running state of all Docker containers").action(runStatus);
|
|
2152
2378
|
var config = program.command("config").description("Update configuration values in a live installation");
|
|
2153
|
-
config.command("show").description("Print key config values for this installation (
|
|
2379
|
+
config.command("show").description("Print key config values for this installation (app URL)").action(runConfigShow);
|
|
2154
2380
|
config.command("sfdc").description("Update Salesforce Connected App credentials (Consumer Key + Secret)").action(runConfigSfdc);
|
|
2155
2381
|
var sfdc = program.command("sfdc").description("Manage the Salesforce package for this installation");
|
|
2156
2382
|
sfdc.command("deploy").description("Deploy (or redeploy) the Lead Router Salesforce package to your Salesforce org").action(runSfdcDeploy);
|
|
2157
2383
|
program.command("uninstall").description("Stop all containers, remove all data, and delete the remote installation").action(runUninstall);
|
|
2384
|
+
program.command("signup").description("Create a new Lead Routing account").action(runSignup);
|
|
2385
|
+
program.command("login").description("Log in to your Lead Routing account").action(runLogin);
|
|
2158
2386
|
program.parseAsync(process.argv).catch((err) => {
|
|
2159
2387
|
console.error(err instanceof Error ? err.message : String(err));
|
|
2160
2388
|
process.exit(1);
|