@lead-routing/cli 0.1.13 → 0.2.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 +315 -310
- package/dist/prisma/migrations/20260308100000_route_match_config/migration.sql +95 -0
- package/dist/prisma/migrations/20260309200000_add_path_label_to_routing_logs/migration.sql +2 -0
- package/dist/prisma/migrations/20260310000000_add_team_to_routing_log/migration.sql +6 -0
- package/dist/prisma/migrations/20260310100000_add_field_type_to_branch_conditions/migration.sql +2 -0
- package/dist/prisma/migrations/20260310200000_analytics_foundation/migration.sql +84 -0
- package/dist/prisma/schema.prisma +188 -17
- package/dist/sfdc-package/force-app/main/default/classes/AccountTriggerTest.cls +26 -0
- package/dist/sfdc-package/force-app/main/default/classes/LeadTriggerTest.cls +49 -0
- package/dist/sfdc-package/force-app/main/default/classes/RoutingEngineCallout.cls +31 -4
- package/dist/sfdc-package/force-app/main/default/classes/RoutingEngineMock.cls +9 -2
- package/dist/sfdc-package/force-app/main/default/classes/RoutingPayloadBuilder.cls +36 -0
- package/dist/sfdc-package/force-app/main/default/triggers/AccountTrigger.trigger +14 -4
- package/dist/sfdc-package/force-app/main/default/triggers/ContactTrigger.trigger +14 -4
- package/dist/sfdc-package/force-app/main/default/triggers/LeadTrigger.trigger +16 -4
- package/package.json +11 -3
package/dist/index.js
CHANGED
|
@@ -10,24 +10,9 @@ import chalk2 from "chalk";
|
|
|
10
10
|
|
|
11
11
|
// src/steps/prerequisites.ts
|
|
12
12
|
import { log } from "@clack/prompts";
|
|
13
|
-
|
|
14
|
-
// src/utils/exec.ts
|
|
15
|
-
import { execa } from "execa";
|
|
16
|
-
import { spinner } from "@clack/prompts";
|
|
17
|
-
async function runSilent(cmd, args, opts = {}) {
|
|
18
|
-
try {
|
|
19
|
-
const result = await execa(cmd, args, { cwd: opts.cwd, reject: false });
|
|
20
|
-
return result.stdout;
|
|
21
|
-
} catch {
|
|
22
|
-
return "";
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// src/steps/prerequisites.ts
|
|
27
13
|
async function checkPrerequisites() {
|
|
28
14
|
const results = await Promise.all([
|
|
29
|
-
checkNodeVersion()
|
|
30
|
-
checkSalesforceCLI()
|
|
15
|
+
checkNodeVersion()
|
|
31
16
|
]);
|
|
32
17
|
const failed = results.filter((r) => !r.ok);
|
|
33
18
|
for (const r of results) {
|
|
@@ -54,17 +39,6 @@ async function checkNodeVersion() {
|
|
|
54
39
|
}
|
|
55
40
|
return { ok: true, label: `Node.js ${version}` };
|
|
56
41
|
}
|
|
57
|
-
async function checkSalesforceCLI() {
|
|
58
|
-
const out = await runSilent("sf", ["--version"]);
|
|
59
|
-
if (!out) {
|
|
60
|
-
return {
|
|
61
|
-
ok: false,
|
|
62
|
-
label: "Salesforce CLI (sf) \u2014 not found",
|
|
63
|
-
detail: "install from https://developer.salesforce.com/tools/salesforcecli"
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return { ok: true, label: `Salesforce CLI \u2014 ${out.trim()}` };
|
|
67
|
-
}
|
|
68
42
|
|
|
69
43
|
// src/steps/collect-ssh-config.ts
|
|
70
44
|
import { existsSync } from "fs";
|
|
@@ -389,6 +363,8 @@ function renderEnvWeb(c) {
|
|
|
389
363
|
``,
|
|
390
364
|
`# Admin`,
|
|
391
365
|
`ADMIN_SECRET=${c.adminSecret}`,
|
|
366
|
+
`ADMIN_EMAIL=${c.adminEmail}`,
|
|
367
|
+
`ADMIN_PASSWORD=${c.adminPassword}`,
|
|
392
368
|
``,
|
|
393
369
|
`# Email (optional)`,
|
|
394
370
|
`RESEND_API_KEY=${c.resendApiKey ?? ""}`,
|
|
@@ -479,10 +455,10 @@ function getConfigPath(dir) {
|
|
|
479
455
|
return join(dir, "lead-routing.json");
|
|
480
456
|
}
|
|
481
457
|
function readConfig(dir) {
|
|
482
|
-
const
|
|
483
|
-
if (!existsSync2(
|
|
458
|
+
const path = getConfigPath(dir);
|
|
459
|
+
if (!existsSync2(path)) return null;
|
|
484
460
|
try {
|
|
485
|
-
return JSON.parse(readFileSync(
|
|
461
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
486
462
|
} catch {
|
|
487
463
|
return null;
|
|
488
464
|
}
|
|
@@ -535,6 +511,8 @@ function generateFiles(cfg, sshCfg) {
|
|
|
535
511
|
sessionSecret: cfg.sessionSecret,
|
|
536
512
|
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
537
513
|
adminSecret: cfg.adminSecret,
|
|
514
|
+
adminEmail: cfg.adminEmail,
|
|
515
|
+
adminPassword: cfg.adminPassword,
|
|
538
516
|
resendApiKey: cfg.resendApiKey || void 0,
|
|
539
517
|
feedbackToEmail: cfg.feedbackToEmail || void 0
|
|
540
518
|
});
|
|
@@ -579,7 +557,7 @@ function generateFiles(cfg, sshCfg) {
|
|
|
579
557
|
}
|
|
580
558
|
|
|
581
559
|
// src/steps/check-remote-prerequisites.ts
|
|
582
|
-
import { log as log4, spinner
|
|
560
|
+
import { log as log4, spinner } from "@clack/prompts";
|
|
583
561
|
async function checkRemotePrerequisites(ssh) {
|
|
584
562
|
const dockerResult = await checkOrInstallDocker(ssh);
|
|
585
563
|
const composeResult = await checkRemoteDockerCompose(ssh);
|
|
@@ -620,7 +598,7 @@ async function checkOrInstallDocker(ssh) {
|
|
|
620
598
|
}
|
|
621
599
|
return { ok: true, label: `Docker \u2014 ${stdout.trim()}` };
|
|
622
600
|
}
|
|
623
|
-
const s =
|
|
601
|
+
const s = spinner();
|
|
624
602
|
s.start("Docker not found \u2014 installing via get.docker.com (~2 min)\u2026");
|
|
625
603
|
try {
|
|
626
604
|
const { code: curlCode } = await ssh.execSilent("command -v curl 2>/dev/null");
|
|
@@ -719,9 +697,9 @@ async function checkRemotePort(ssh, port) {
|
|
|
719
697
|
|
|
720
698
|
// src/steps/upload-files.ts
|
|
721
699
|
import { join as join3 } from "path";
|
|
722
|
-
import { spinner as
|
|
700
|
+
import { spinner as spinner2 } from "@clack/prompts";
|
|
723
701
|
async function uploadFiles(ssh, localDir, remoteDir) {
|
|
724
|
-
const s =
|
|
702
|
+
const s = spinner2();
|
|
725
703
|
s.start("Uploading config files to server");
|
|
726
704
|
try {
|
|
727
705
|
await ssh.mkdir(remoteDir);
|
|
@@ -746,7 +724,7 @@ async function uploadFiles(ssh, localDir, remoteDir) {
|
|
|
746
724
|
}
|
|
747
725
|
|
|
748
726
|
// src/steps/start-services.ts
|
|
749
|
-
import { spinner as
|
|
727
|
+
import { spinner as spinner3, log as log5 } from "@clack/prompts";
|
|
750
728
|
async function startServices(ssh, remoteDir) {
|
|
751
729
|
await wipeStalePostgresVolume(ssh, remoteDir);
|
|
752
730
|
await pullImages(ssh, remoteDir);
|
|
@@ -760,7 +738,7 @@ async function wipeStalePostgresVolume(ssh, remoteDir) {
|
|
|
760
738
|
if (code !== 0) {
|
|
761
739
|
return;
|
|
762
740
|
}
|
|
763
|
-
const s =
|
|
741
|
+
const s = spinner3();
|
|
764
742
|
s.start("Removing existing database volume for clean install");
|
|
765
743
|
try {
|
|
766
744
|
await ssh.exec("docker compose down -v --remove-orphans", remoteDir);
|
|
@@ -774,7 +752,7 @@ async function wipeStalePostgresVolume(ssh, remoteDir) {
|
|
|
774
752
|
}
|
|
775
753
|
}
|
|
776
754
|
async function pullImages(ssh, remoteDir) {
|
|
777
|
-
const s =
|
|
755
|
+
const s = spinner3();
|
|
778
756
|
s.start("Pulling Docker images on server (this may take a few minutes)");
|
|
779
757
|
try {
|
|
780
758
|
await ssh.exec("docker compose pull", remoteDir);
|
|
@@ -787,7 +765,7 @@ async function pullImages(ssh, remoteDir) {
|
|
|
787
765
|
}
|
|
788
766
|
}
|
|
789
767
|
async function startContainers(ssh, remoteDir) {
|
|
790
|
-
const s =
|
|
768
|
+
const s = spinner3();
|
|
791
769
|
s.start("Starting services");
|
|
792
770
|
try {
|
|
793
771
|
await ssh.exec("docker compose up -d --remove-orphans", remoteDir);
|
|
@@ -798,7 +776,7 @@ async function startContainers(ssh, remoteDir) {
|
|
|
798
776
|
}
|
|
799
777
|
}
|
|
800
778
|
async function waitForPostgres(ssh, remoteDir) {
|
|
801
|
-
const s =
|
|
779
|
+
const s = spinner3();
|
|
802
780
|
s.start("Waiting for PostgreSQL to be ready");
|
|
803
781
|
const maxAttempts = 24;
|
|
804
782
|
let containerReady = false;
|
|
@@ -834,119 +812,11 @@ async function waitForPostgres(ssh, remoteDir) {
|
|
|
834
812
|
log5.warn("Host TCP port check timed out \u2014 tunnel may have issues. Proceeding anyway.");
|
|
835
813
|
}
|
|
836
814
|
function sleep(ms) {
|
|
837
|
-
return new Promise((
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// src/steps/run-migrations.ts
|
|
841
|
-
import * as fs from "fs";
|
|
842
|
-
import * as path from "path";
|
|
843
|
-
import * as crypto from "crypto";
|
|
844
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
845
|
-
import { execa as execa2 } from "execa";
|
|
846
|
-
import { spinner as spinner5 } from "@clack/prompts";
|
|
847
|
-
var __filename = fileURLToPath2(import.meta.url);
|
|
848
|
-
var __dirname2 = path.dirname(__filename);
|
|
849
|
-
function readEnvVar(envFile, key) {
|
|
850
|
-
const content = fs.readFileSync(envFile, "utf8");
|
|
851
|
-
const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
|
|
852
|
-
if (!match) throw new Error(`${key} not found in ${envFile}`);
|
|
853
|
-
return match[1].trim().replace(/^["']|["']$/g, "");
|
|
854
|
-
}
|
|
855
|
-
function getTunneledDbUrl(localDir, localPort) {
|
|
856
|
-
const rawUrl = readEnvVar(path.join(localDir, ".env.web"), "DATABASE_URL");
|
|
857
|
-
const parsed = new URL(rawUrl);
|
|
858
|
-
parsed.hostname = "localhost";
|
|
859
|
-
parsed.port = String(localPort);
|
|
860
|
-
return parsed.toString();
|
|
861
|
-
}
|
|
862
|
-
function findPrismaBin() {
|
|
863
|
-
const candidates = [
|
|
864
|
-
// npx / npm global install: @lead-routing/cli is nested under the scope dir,
|
|
865
|
-
// so prisma lands 3 levels above dist/ in node_modules/.bin/
|
|
866
|
-
// e.g. ~/.npm/_npx/HASH/node_modules/.bin/prisma
|
|
867
|
-
path.join(__dirname2, "../../../.bin/prisma"),
|
|
868
|
-
path.join(__dirname2, "../../../prisma/bin/prisma.js"),
|
|
869
|
-
// Fallback: prisma nested inside the package's own node_modules (hoisted install)
|
|
870
|
-
path.join(__dirname2, "../node_modules/.bin/prisma"),
|
|
871
|
-
path.join(__dirname2, "../node_modules/prisma/bin/prisma.js"),
|
|
872
|
-
// Monorepo dev paths
|
|
873
|
-
path.resolve("packages/db/node_modules/.bin/prisma"),
|
|
874
|
-
path.resolve("node_modules/.bin/prisma"),
|
|
875
|
-
path.resolve("node_modules/.pnpm/node_modules/.bin/prisma")
|
|
876
|
-
];
|
|
877
|
-
const found = candidates.find(fs.existsSync);
|
|
878
|
-
if (!found) throw new Error("Prisma binary not found \u2014 CLI may need to be reinstalled.");
|
|
879
|
-
return found;
|
|
880
|
-
}
|
|
881
|
-
async function runMigrations(ssh, localDir, adminEmail, adminPassword) {
|
|
882
|
-
const s = spinner5();
|
|
883
|
-
s.start("Opening secure tunnel to database");
|
|
884
|
-
let tunnelClose;
|
|
885
|
-
try {
|
|
886
|
-
const { localPort, close } = await ssh.tunnel(5432);
|
|
887
|
-
tunnelClose = close;
|
|
888
|
-
s.stop(`Database tunnel open (local port ${localPort})`);
|
|
889
|
-
await applyMigrations(localDir, localPort);
|
|
890
|
-
if (adminEmail && adminPassword) {
|
|
891
|
-
await seedAdminUser(localDir, localPort, adminEmail, adminPassword);
|
|
892
|
-
}
|
|
893
|
-
} finally {
|
|
894
|
-
tunnelClose?.();
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
async function applyMigrations(localDir, localPort) {
|
|
898
|
-
const s = spinner5();
|
|
899
|
-
s.start("Running database migrations");
|
|
900
|
-
try {
|
|
901
|
-
const DATABASE_URL = getTunneledDbUrl(localDir, localPort);
|
|
902
|
-
const prismaBin = findPrismaBin();
|
|
903
|
-
const bundledSchema = path.join(__dirname2, "prisma/schema.prisma");
|
|
904
|
-
const monoSchema = path.resolve("packages/db/prisma/schema.prisma");
|
|
905
|
-
const schemaPath = fs.existsSync(bundledSchema) ? bundledSchema : monoSchema;
|
|
906
|
-
await execa2(prismaBin, ["migrate", "deploy", "--schema", schemaPath], {
|
|
907
|
-
env: { ...process.env, DATABASE_URL }
|
|
908
|
-
});
|
|
909
|
-
s.stop("Database migrations applied");
|
|
910
|
-
} catch (err) {
|
|
911
|
-
s.stop("Migrations failed");
|
|
912
|
-
throw err;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
async function seedAdminUser(localDir, localPort, adminEmail, adminPassword) {
|
|
916
|
-
const s = spinner5();
|
|
917
|
-
s.start("Creating admin user");
|
|
918
|
-
try {
|
|
919
|
-
const DATABASE_URL = getTunneledDbUrl(localDir, localPort);
|
|
920
|
-
const webhookSecret = readEnvVar(path.join(localDir, ".env.engine"), "ENGINE_WEBHOOK_SECRET");
|
|
921
|
-
const salt = crypto.randomBytes(16).toString("hex");
|
|
922
|
-
const pbkdf2Hash = crypto.pbkdf2Sync(adminPassword, salt, 31e4, 32, "sha256").toString("hex");
|
|
923
|
-
const passwordHash = `${salt}:${pbkdf2Hash}`;
|
|
924
|
-
const safeEmail = adminEmail.replace(/'/g, "''");
|
|
925
|
-
const safeWebhookSecret = webhookSecret.replace(/'/g, "''");
|
|
926
|
-
const sql = `
|
|
927
|
-
-- Create initial organisation if none exists (self-hosted defaults: PAID plan, unlimited seats)
|
|
928
|
-
INSERT INTO organizations (id, "webhookSecret", plan, "seatsPurchased", "isActive", "createdAt", "updatedAt")
|
|
929
|
-
SELECT gen_random_uuid(), '${safeWebhookSecret}', 'PAID', 9999, true, NOW(), NOW()
|
|
930
|
-
WHERE NOT EXISTS (SELECT 1 FROM organizations);
|
|
931
|
-
|
|
932
|
-
-- Create admin AppUser under the first org (idempotent)
|
|
933
|
-
INSERT INTO app_users (id, "orgId", email, name, "passwordHash", role, "isActive", "createdAt", "updatedAt")
|
|
934
|
-
SELECT gen_random_uuid(), o.id, '${safeEmail}', 'Admin', '${passwordHash}', 'ADMIN', true, NOW(), NOW()
|
|
935
|
-
FROM organizations o
|
|
936
|
-
LIMIT 1
|
|
937
|
-
ON CONFLICT ("orgId", email) DO NOTHING;
|
|
938
|
-
`;
|
|
939
|
-
const prismaBin = findPrismaBin();
|
|
940
|
-
await execa2(prismaBin, ["db", "execute", "--stdin", "--url", DATABASE_URL], { input: sql });
|
|
941
|
-
s.stop("Admin user ready");
|
|
942
|
-
} catch (err) {
|
|
943
|
-
s.stop("Seed failed");
|
|
944
|
-
throw err;
|
|
945
|
-
}
|
|
815
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
946
816
|
}
|
|
947
817
|
|
|
948
818
|
// src/steps/verify-health.ts
|
|
949
|
-
import { spinner as
|
|
819
|
+
import { spinner as spinner4, log as log6 } from "@clack/prompts";
|
|
950
820
|
async function verifyHealth(appUrl, engineUrl, ssh, remoteDir) {
|
|
951
821
|
const checks = [
|
|
952
822
|
{ service: "Web app", url: `${appUrl}/api/health` },
|
|
@@ -995,7 +865,7 @@ To resume once fixed:
|
|
|
995
865
|
);
|
|
996
866
|
}
|
|
997
867
|
async function pollHealth(service, url, maxAttempts = 24, intervalMs = 5e3) {
|
|
998
|
-
const s =
|
|
868
|
+
const s = spinner4();
|
|
999
869
|
s.start(`Waiting for ${service}`);
|
|
1000
870
|
for (let i = 0; i < maxAttempts; i++) {
|
|
1001
871
|
try {
|
|
@@ -1020,54 +890,220 @@ async function pollHealth(service, url, maxAttempts = 24, intervalMs = 5e3) {
|
|
|
1020
890
|
};
|
|
1021
891
|
}
|
|
1022
892
|
function sleep2(ms) {
|
|
1023
|
-
return new Promise((
|
|
893
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1024
894
|
}
|
|
1025
895
|
|
|
1026
896
|
// src/steps/sfdc-deploy-inline.ts
|
|
1027
|
-
import { readFileSync as
|
|
1028
|
-
import { join as join5, dirname as
|
|
897
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, cpSync, rmSync } from "fs";
|
|
898
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
1029
899
|
import { tmpdir } from "os";
|
|
1030
|
-
import { fileURLToPath as
|
|
1031
|
-
import {
|
|
1032
|
-
import {
|
|
1033
|
-
|
|
900
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
901
|
+
import { execSync } from "child_process";
|
|
902
|
+
import { spinner as spinner5, log as log7 } from "@clack/prompts";
|
|
903
|
+
|
|
904
|
+
// src/utils/sfdc-api.ts
|
|
905
|
+
var API_VERSION = "v59.0";
|
|
906
|
+
var SalesforceApi = class {
|
|
907
|
+
constructor(instanceUrl, accessToken) {
|
|
908
|
+
this.instanceUrl = instanceUrl;
|
|
909
|
+
this.accessToken = accessToken;
|
|
910
|
+
this.baseUrl = `${instanceUrl.replace(/\/+$/, "")}/services/data/${API_VERSION}`;
|
|
911
|
+
}
|
|
912
|
+
baseUrl;
|
|
913
|
+
headers(extra) {
|
|
914
|
+
return {
|
|
915
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
916
|
+
"Content-Type": "application/json",
|
|
917
|
+
...extra
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
/** Execute a SOQL query and return records */
|
|
921
|
+
async query(soql) {
|
|
922
|
+
const url = `${this.baseUrl}/query?q=${encodeURIComponent(soql)}`;
|
|
923
|
+
const res = await fetch(url, { headers: this.headers() });
|
|
924
|
+
if (!res.ok) {
|
|
925
|
+
const body = await res.text();
|
|
926
|
+
throw new Error(`SOQL query failed (${res.status}): ${body}`);
|
|
927
|
+
}
|
|
928
|
+
const data = await res.json();
|
|
929
|
+
return data.records;
|
|
930
|
+
}
|
|
931
|
+
/** Create an sObject record, returns the new record ID */
|
|
932
|
+
async create(sobject, data) {
|
|
933
|
+
const url = `${this.baseUrl}/sobjects/${sobject}`;
|
|
934
|
+
const res = await fetch(url, {
|
|
935
|
+
method: "POST",
|
|
936
|
+
headers: this.headers(),
|
|
937
|
+
body: JSON.stringify(data)
|
|
938
|
+
});
|
|
939
|
+
if (!res.ok) {
|
|
940
|
+
const body = await res.text();
|
|
941
|
+
if (res.status === 400 && body.includes("Duplicate")) {
|
|
942
|
+
throw new DuplicateError(body);
|
|
943
|
+
}
|
|
944
|
+
throw new Error(`Create ${sobject} failed (${res.status}): ${body}`);
|
|
945
|
+
}
|
|
946
|
+
const result = await res.json();
|
|
947
|
+
return result.id;
|
|
948
|
+
}
|
|
949
|
+
/** Update an sObject record */
|
|
950
|
+
async update(sobject, id, data) {
|
|
951
|
+
const url = `${this.baseUrl}/sobjects/${sobject}/${id}`;
|
|
952
|
+
const res = await fetch(url, {
|
|
953
|
+
method: "PATCH",
|
|
954
|
+
headers: this.headers(),
|
|
955
|
+
body: JSON.stringify(data)
|
|
956
|
+
});
|
|
957
|
+
if (!res.ok) {
|
|
958
|
+
const body = await res.text();
|
|
959
|
+
throw new Error(`Update ${sobject}/${id} failed (${res.status}): ${body}`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/** Get current user info (for permission set assignment) */
|
|
963
|
+
async getCurrentUserId() {
|
|
964
|
+
const url = `${this.instanceUrl.replace(/\/+$/, "")}/services/oauth2/userinfo`;
|
|
965
|
+
const res = await fetch(url, {
|
|
966
|
+
headers: { Authorization: `Bearer ${this.accessToken}` }
|
|
967
|
+
});
|
|
968
|
+
if (!res.ok) {
|
|
969
|
+
const body = await res.text();
|
|
970
|
+
throw new Error(`Get current user failed (${res.status}): ${body}`);
|
|
971
|
+
}
|
|
972
|
+
const data = await res.json();
|
|
973
|
+
return data.user_id;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Deploy metadata using the Source Deploy API (same API that `sf project deploy start` uses).
|
|
977
|
+
* Accepts a ZIP buffer containing the source-format package.
|
|
978
|
+
* Returns the deploy request ID for polling.
|
|
979
|
+
*/
|
|
980
|
+
async deployMetadata(zipBuffer) {
|
|
981
|
+
const url = `${this.baseUrl}/metadata/deployRequest`;
|
|
982
|
+
const boundary = `----FormBoundary${Date.now()}`;
|
|
983
|
+
const deployOptions = JSON.stringify({
|
|
984
|
+
deployOptions: {
|
|
985
|
+
rollbackOnError: true,
|
|
986
|
+
singlePackage: true,
|
|
987
|
+
rest: true
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
const parts = [];
|
|
991
|
+
parts.push(
|
|
992
|
+
Buffer.from(
|
|
993
|
+
`--${boundary}\r
|
|
994
|
+
Content-Disposition: form-data; name="json"\r
|
|
995
|
+
Content-Type: application/json\r
|
|
996
|
+
\r
|
|
997
|
+
${deployOptions}\r
|
|
998
|
+
`
|
|
999
|
+
)
|
|
1000
|
+
);
|
|
1001
|
+
parts.push(
|
|
1002
|
+
Buffer.from(
|
|
1003
|
+
`--${boundary}\r
|
|
1004
|
+
Content-Disposition: form-data; name="file"; filename="deploy.zip"\r
|
|
1005
|
+
Content-Type: application/zip\r
|
|
1006
|
+
\r
|
|
1007
|
+
`
|
|
1008
|
+
)
|
|
1009
|
+
);
|
|
1010
|
+
parts.push(zipBuffer);
|
|
1011
|
+
parts.push(Buffer.from(`\r
|
|
1012
|
+
--${boundary}--\r
|
|
1013
|
+
`));
|
|
1014
|
+
const body = Buffer.concat(parts);
|
|
1015
|
+
const res = await fetch(url, {
|
|
1016
|
+
method: "POST",
|
|
1017
|
+
headers: {
|
|
1018
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
1019
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`
|
|
1020
|
+
},
|
|
1021
|
+
body
|
|
1022
|
+
});
|
|
1023
|
+
if (!res.ok) {
|
|
1024
|
+
const text5 = await res.text();
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`Metadata deploy request failed (${res.status}): ${text5}`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const result = await res.json();
|
|
1030
|
+
return result.id;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Poll deploy status until complete.
|
|
1034
|
+
* Returns deploy result with success/failure info.
|
|
1035
|
+
*/
|
|
1036
|
+
async waitForDeploy(deployId, timeoutMs = 3e5) {
|
|
1037
|
+
const startTime = Date.now();
|
|
1038
|
+
const pollInterval = 3e3;
|
|
1039
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1040
|
+
const url = `${this.baseUrl}/metadata/deployRequest/${deployId}?includeDetails=true`;
|
|
1041
|
+
const res = await fetch(url, { headers: this.headers() });
|
|
1042
|
+
if (!res.ok) {
|
|
1043
|
+
const text5 = await res.text();
|
|
1044
|
+
throw new Error(
|
|
1045
|
+
`Deploy status check failed (${res.status}): ${text5}`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
const data = await res.json();
|
|
1049
|
+
const result = data.deployResult;
|
|
1050
|
+
if (result.done) {
|
|
1051
|
+
return result;
|
|
1052
|
+
}
|
|
1053
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
1054
|
+
}
|
|
1055
|
+
throw new Error(`Deploy timed out after ${timeoutMs / 1e3}s`);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
var DuplicateError = class extends Error {
|
|
1059
|
+
constructor(message) {
|
|
1060
|
+
super(message);
|
|
1061
|
+
this.name = "DuplicateError";
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
// src/utils/zip-source.ts
|
|
1066
|
+
import { join as join4 } from "path";
|
|
1067
|
+
import archiver from "archiver";
|
|
1068
|
+
async function zipSourcePackage(packageDir) {
|
|
1069
|
+
return new Promise((resolve, reject) => {
|
|
1070
|
+
const chunks = [];
|
|
1071
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
1072
|
+
archive.on("data", (chunk) => chunks.push(chunk));
|
|
1073
|
+
archive.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1074
|
+
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
|
+
});
|
|
1079
|
+
archive.finalize();
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/steps/sfdc-deploy-inline.ts
|
|
1084
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
1034
1085
|
function patchXml(content, tag, value) {
|
|
1035
1086
|
const re = new RegExp(`(<${tag}>)[^<]*(</\\s*${tag}>)`, "g");
|
|
1036
1087
|
return content.replace(re, `$1${value}$2`);
|
|
1037
1088
|
}
|
|
1038
1089
|
async function sfdcDeployInline(params) {
|
|
1039
|
-
const { appUrl, engineUrl,
|
|
1040
|
-
const s =
|
|
1041
|
-
const {
|
|
1042
|
-
|
|
1043
|
-
["org", "display", "--target-org", orgAlias, "--json"],
|
|
1044
|
-
{ reject: false }
|
|
1045
|
-
);
|
|
1046
|
-
const alreadyAuthed = authCheck === 0;
|
|
1047
|
-
let sfCredEnv = {};
|
|
1048
|
-
let targetOrgArgs = ["--target-org", orgAlias];
|
|
1049
|
-
if (alreadyAuthed) {
|
|
1050
|
-
log7.success("Using existing Salesforce authentication");
|
|
1051
|
-
} else {
|
|
1052
|
-
const { accessToken, instanceUrl, aliasStored } = await loginViaAppBridge(appUrl, orgAlias);
|
|
1053
|
-
sfCredEnv = { SF_ACCESS_TOKEN: accessToken, SF_ORG_INSTANCE_URL: instanceUrl };
|
|
1054
|
-
if (!aliasStored) {
|
|
1055
|
-
targetOrgArgs = [];
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1090
|
+
const { appUrl, engineUrl, installDir } = params;
|
|
1091
|
+
const s = spinner5();
|
|
1092
|
+
const { accessToken, instanceUrl } = await loginViaAppBridge(appUrl);
|
|
1093
|
+
const sf = new SalesforceApi(instanceUrl, accessToken);
|
|
1058
1094
|
s.start("Copying Salesforce package\u2026");
|
|
1059
|
-
const inDist = join5(
|
|
1060
|
-
const nextToDist = join5(
|
|
1061
|
-
const bundledPkg =
|
|
1095
|
+
const inDist = join5(__dirname2, "sfdc-package");
|
|
1096
|
+
const nextToDist = join5(__dirname2, "..", "sfdc-package");
|
|
1097
|
+
const bundledPkg = existsSync3(inDist) ? inDist : nextToDist;
|
|
1062
1098
|
const destPkg = join5(installDir ?? tmpdir(), "lead-routing-sfdc-package");
|
|
1063
|
-
if (!
|
|
1099
|
+
if (!existsSync3(bundledPkg)) {
|
|
1064
1100
|
s.stop("sfdc-package not found in CLI bundle");
|
|
1065
1101
|
throw new Error(
|
|
1066
1102
|
`Expected bundle at: ${inDist}
|
|
1067
1103
|
The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
1068
1104
|
);
|
|
1069
1105
|
}
|
|
1070
|
-
if (
|
|
1106
|
+
if (existsSync3(destPkg)) rmSync(destPkg, { recursive: true, force: true });
|
|
1071
1107
|
cpSync(bundledPkg, destPkg, { recursive: true });
|
|
1072
1108
|
s.stop("Package copied");
|
|
1073
1109
|
const ncPath = join5(
|
|
@@ -1078,8 +1114,8 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1078
1114
|
"namedCredentials",
|
|
1079
1115
|
"RoutingEngine.namedCredential-meta.xml"
|
|
1080
1116
|
);
|
|
1081
|
-
if (
|
|
1082
|
-
const nc = patchXml(
|
|
1117
|
+
if (existsSync3(ncPath)) {
|
|
1118
|
+
const nc = patchXml(readFileSync3(ncPath, "utf8"), "endpoint", engineUrl);
|
|
1083
1119
|
writeFileSync3(ncPath, nc, "utf8");
|
|
1084
1120
|
}
|
|
1085
1121
|
const rssEnginePath = join5(
|
|
@@ -1090,8 +1126,8 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1090
1126
|
"remoteSiteSettings",
|
|
1091
1127
|
"LeadRouterEngine.remoteSite-meta.xml"
|
|
1092
1128
|
);
|
|
1093
|
-
if (
|
|
1094
|
-
let rss = patchXml(
|
|
1129
|
+
if (existsSync3(rssEnginePath)) {
|
|
1130
|
+
let rss = patchXml(readFileSync3(rssEnginePath, "utf8"), "url", engineUrl);
|
|
1095
1131
|
rss = patchXml(rss, "description", "Lead Router Engine endpoint");
|
|
1096
1132
|
writeFileSync3(rssEnginePath, rss, "utf8");
|
|
1097
1133
|
}
|
|
@@ -1103,45 +1139,67 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1103
1139
|
"remoteSiteSettings",
|
|
1104
1140
|
"LeadRouterApp.remoteSite-meta.xml"
|
|
1105
1141
|
);
|
|
1106
|
-
if (
|
|
1107
|
-
let rss = patchXml(
|
|
1142
|
+
if (existsSync3(rssAppPath)) {
|
|
1143
|
+
let rss = patchXml(readFileSync3(rssAppPath, "utf8"), "url", appUrl);
|
|
1108
1144
|
rss = patchXml(rss, "description", "Lead Router App URL");
|
|
1109
1145
|
writeFileSync3(rssAppPath, rss, "utf8");
|
|
1110
1146
|
}
|
|
1111
1147
|
log7.success("Remote Site Settings patched");
|
|
1112
1148
|
s.start("Deploying Salesforce package (this may take ~2 min)\u2026");
|
|
1113
1149
|
try {
|
|
1114
|
-
await
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1150
|
+
const zipBuffer = await zipSourcePackage(destPkg);
|
|
1151
|
+
const deployId = await sf.deployMetadata(zipBuffer);
|
|
1152
|
+
const result = await sf.waitForDeploy(deployId);
|
|
1153
|
+
if (!result.success) {
|
|
1154
|
+
const failures = result.details?.componentFailures ?? [];
|
|
1155
|
+
const failureMsg = failures.map((f) => ` ${f.componentType}/${f.fullName}: ${f.problem}`).join("\n");
|
|
1156
|
+
s.stop("Deployment failed");
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
`Metadata deploy failed (${result.numberComponentErrors} error(s)):
|
|
1159
|
+
${failureMsg || result.errorMessage || "Unknown error"}`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
s.stop(`Package deployed (${result.numberComponentsDeployed} components)`);
|
|
1120
1163
|
} catch (err) {
|
|
1164
|
+
if (err instanceof Error && err.message.startsWith("Metadata deploy failed")) {
|
|
1165
|
+
throw err;
|
|
1166
|
+
}
|
|
1121
1167
|
s.stop("Deployment failed");
|
|
1122
1168
|
throw new Error(
|
|
1123
|
-
`
|
|
1169
|
+
`Metadata deploy failed: ${String(err)}
|
|
1124
1170
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
sf project deploy start --target-org ${orgAlias} --source-dir force-app`
|
|
1171
|
+
The patched package is at: ${destPkg}
|
|
1172
|
+
You can retry with: sf project deploy start --source-dir force-app`
|
|
1128
1173
|
);
|
|
1129
1174
|
}
|
|
1130
1175
|
s.start("Assigning LeadRouterAdmin permission set\u2026");
|
|
1131
1176
|
try {
|
|
1132
|
-
await
|
|
1133
|
-
"
|
|
1134
|
-
["org", "assign", "permset", "--name", "LeadRouterAdmin", ...targetOrgArgs],
|
|
1135
|
-
{ stdio: "inherit", env: { ...process.env, ...sfCredEnv } }
|
|
1177
|
+
const permSets = await sf.query(
|
|
1178
|
+
"SELECT Id FROM PermissionSet WHERE Name = 'LeadRouterAdmin' LIMIT 1"
|
|
1136
1179
|
);
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (msg.includes("Duplicate PermissionSetAssignment")) {
|
|
1141
|
-
s.stop("Permission set already assigned");
|
|
1180
|
+
if (permSets.length === 0) {
|
|
1181
|
+
s.stop("LeadRouterAdmin permission set not found (non-fatal)");
|
|
1182
|
+
log7.warn("The permission set may not have been included in the deploy.");
|
|
1142
1183
|
} else {
|
|
1184
|
+
const userId = await sf.getCurrentUserId();
|
|
1185
|
+
try {
|
|
1186
|
+
await sf.create("PermissionSetAssignment", {
|
|
1187
|
+
AssigneeId: userId,
|
|
1188
|
+
PermissionSetId: permSets[0].Id
|
|
1189
|
+
});
|
|
1190
|
+
s.stop("Permission set assigned \u2014 Lead Router Setup will appear in the App Launcher");
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
if (err instanceof DuplicateError) {
|
|
1193
|
+
s.stop("Permission set already assigned");
|
|
1194
|
+
} else {
|
|
1195
|
+
throw err;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
if (!(err instanceof DuplicateError)) {
|
|
1143
1201
|
s.stop("Permission set assignment failed (non-fatal)");
|
|
1144
|
-
log7.warn(
|
|
1202
|
+
log7.warn(String(err));
|
|
1145
1203
|
log7.info(
|
|
1146
1204
|
"Grant access manually:\n Salesforce Setup \u2192 Users \u2192 Permission Sets \u2192 Lead Router Admin \u2192 Manage Assignments"
|
|
1147
1205
|
);
|
|
@@ -1149,44 +1207,19 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1149
1207
|
}
|
|
1150
1208
|
s.start("Writing org settings to Routing_Settings__c\u2026");
|
|
1151
1209
|
try {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
"--json"
|
|
1161
|
-
], { env: { ...process.env, ...sfCredEnv } });
|
|
1162
|
-
const parsed = JSON.parse(qr.stdout);
|
|
1163
|
-
existingId = parsed?.result?.records?.[0]?.Id;
|
|
1164
|
-
} catch {
|
|
1165
|
-
}
|
|
1166
|
-
if (existingId) {
|
|
1167
|
-
await execa3("sf", [
|
|
1168
|
-
"data",
|
|
1169
|
-
"update",
|
|
1170
|
-
"record",
|
|
1171
|
-
...targetOrgArgs,
|
|
1172
|
-
"--sobject",
|
|
1173
|
-
"Routing_Settings__c",
|
|
1174
|
-
"--record-id",
|
|
1175
|
-
existingId,
|
|
1176
|
-
"--values",
|
|
1177
|
-
`App_Url__c='${appUrl}' Engine_Endpoint__c='${engineUrl}'`
|
|
1178
|
-
], { stdio: "inherit", env: { ...process.env, ...sfCredEnv } });
|
|
1210
|
+
const existing = await sf.query(
|
|
1211
|
+
"SELECT Id FROM Routing_Settings__c LIMIT 1"
|
|
1212
|
+
);
|
|
1213
|
+
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
|
+
});
|
|
1179
1218
|
} else {
|
|
1180
|
-
await
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
...targetOrgArgs,
|
|
1185
|
-
"--sobject",
|
|
1186
|
-
"Routing_Settings__c",
|
|
1187
|
-
"--values",
|
|
1188
|
-
`App_Url__c='${appUrl}' Engine_Endpoint__c='${engineUrl}'`
|
|
1189
|
-
], { stdio: "inherit", env: { ...process.env, ...sfCredEnv } });
|
|
1219
|
+
await sf.create("Routing_Settings__c", {
|
|
1220
|
+
App_Url__c: appUrl,
|
|
1221
|
+
Engine_Endpoint__c: engineUrl
|
|
1222
|
+
});
|
|
1190
1223
|
}
|
|
1191
1224
|
s.stop("Org settings written");
|
|
1192
1225
|
} catch (err) {
|
|
@@ -1195,9 +1228,9 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1195
1228
|
log7.info("Set manually: Salesforce \u2192 Custom Settings \u2192 Routing Settings \u2192 Manage");
|
|
1196
1229
|
}
|
|
1197
1230
|
}
|
|
1198
|
-
async function loginViaAppBridge(rawAppUrl
|
|
1231
|
+
async function loginViaAppBridge(rawAppUrl) {
|
|
1199
1232
|
const appUrl = rawAppUrl.replace(/\/+$/, "");
|
|
1200
|
-
const s =
|
|
1233
|
+
const s = spinner5();
|
|
1201
1234
|
s.start("Starting Salesforce authentication via your Lead Router app\u2026");
|
|
1202
1235
|
let sessionId;
|
|
1203
1236
|
let authUrl;
|
|
@@ -1227,8 +1260,10 @@ Ensure the app is running and the URL is correct.`
|
|
|
1227
1260
|
`);
|
|
1228
1261
|
log7.info('If Chrome shows a "Dangerous site" warning with no proceed option, paste the URL into Safari or Firefox.');
|
|
1229
1262
|
const opener = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
1230
|
-
|
|
1231
|
-
|
|
1263
|
+
try {
|
|
1264
|
+
execSync(`${opener} "${authUrl}"`, { stdio: "ignore" });
|
|
1265
|
+
} catch {
|
|
1266
|
+
}
|
|
1232
1267
|
s.start("Waiting for Salesforce authentication in browser\u2026");
|
|
1233
1268
|
const maxPolls = 150;
|
|
1234
1269
|
let accessToken;
|
|
@@ -1258,20 +1293,7 @@ Ensure the app is running and the URL is correct.`
|
|
|
1258
1293
|
);
|
|
1259
1294
|
}
|
|
1260
1295
|
s.stop("Authenticated with Salesforce");
|
|
1261
|
-
|
|
1262
|
-
try {
|
|
1263
|
-
await execa3(
|
|
1264
|
-
"sf",
|
|
1265
|
-
["org", "login", "access-token", "--instance-url", instanceUrl, "--alias", orgAlias, "--no-prompt"],
|
|
1266
|
-
{ env: { ...process.env, SFDX_ACCESS_TOKEN: accessToken } }
|
|
1267
|
-
);
|
|
1268
|
-
log7.success(`Salesforce org saved as "${orgAlias}"`);
|
|
1269
|
-
aliasStored = true;
|
|
1270
|
-
} catch (err) {
|
|
1271
|
-
log7.warn(`Could not persist sf CLI credentials: ${String(err)}`);
|
|
1272
|
-
log7.info("Continuing with direct token auth for this session.");
|
|
1273
|
-
}
|
|
1274
|
-
return { accessToken, instanceUrl, aliasStored };
|
|
1296
|
+
return { accessToken, instanceUrl };
|
|
1275
1297
|
}
|
|
1276
1298
|
|
|
1277
1299
|
// src/steps/app-launcher-guide.ts
|
|
@@ -1412,10 +1434,10 @@ ${result.stderr || result.stdout}`
|
|
|
1412
1434
|
}
|
|
1413
1435
|
);
|
|
1414
1436
|
});
|
|
1415
|
-
return new Promise((
|
|
1437
|
+
return new Promise((resolve, reject) => {
|
|
1416
1438
|
server.listen(0, "127.0.0.1", () => {
|
|
1417
1439
|
const { port } = server.address();
|
|
1418
|
-
|
|
1440
|
+
resolve({
|
|
1419
1441
|
localPort: port,
|
|
1420
1442
|
close: () => server.close()
|
|
1421
1443
|
});
|
|
@@ -1490,9 +1512,9 @@ async function runInit(options = {}) {
|
|
|
1490
1512
|
});
|
|
1491
1513
|
log9.success(`Connected to ${saved.ssh.host}`);
|
|
1492
1514
|
const remoteDir = await ssh.resolveHome(saved.remoteDir);
|
|
1493
|
-
log9.step("Step 8
|
|
1515
|
+
log9.step("Step 7/8 Verifying health");
|
|
1494
1516
|
await verifyHealth(saved.appUrl, saved.engineUrl, ssh, remoteDir);
|
|
1495
|
-
log9.step("Step
|
|
1517
|
+
log9.step("Step 8/8 Deploying Salesforce package");
|
|
1496
1518
|
await sfdcDeployInline({
|
|
1497
1519
|
appUrl: saved.appUrl,
|
|
1498
1520
|
engineUrl: saved.engineUrl,
|
|
@@ -1524,9 +1546,9 @@ async function runInit(options = {}) {
|
|
|
1524
1546
|
return;
|
|
1525
1547
|
}
|
|
1526
1548
|
try {
|
|
1527
|
-
log9.step("Step 1/
|
|
1549
|
+
log9.step("Step 1/8 Checking local prerequisites");
|
|
1528
1550
|
await checkPrerequisites();
|
|
1529
|
-
log9.step("Step 2/
|
|
1551
|
+
log9.step("Step 2/8 SSH connection");
|
|
1530
1552
|
const sshCfg = await collectSshConfig({
|
|
1531
1553
|
sshPort: options.sshPort,
|
|
1532
1554
|
sshUser: options.sshUser,
|
|
@@ -1543,14 +1565,14 @@ async function runInit(options = {}) {
|
|
|
1543
1565
|
process.exit(1);
|
|
1544
1566
|
}
|
|
1545
1567
|
}
|
|
1546
|
-
log9.step("Step 3/
|
|
1568
|
+
log9.step("Step 3/8 Configuration");
|
|
1547
1569
|
const cfg = await collectConfig({
|
|
1548
1570
|
sandbox: options.sandbox,
|
|
1549
1571
|
externalDb: options.externalDb,
|
|
1550
1572
|
externalRedis: options.externalRedis
|
|
1551
1573
|
});
|
|
1552
1574
|
await checkDnsResolvable(cfg.appUrl, cfg.engineUrl);
|
|
1553
|
-
log9.step("Step 4/
|
|
1575
|
+
log9.step("Step 4/8 Generating config files");
|
|
1554
1576
|
const { dir, adminSecret } = generateFiles(cfg, sshCfg);
|
|
1555
1577
|
note4(
|
|
1556
1578
|
`Local config directory: ${chalk2.cyan(dir)}
|
|
@@ -1567,17 +1589,15 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1567
1589
|
);
|
|
1568
1590
|
return;
|
|
1569
1591
|
}
|
|
1570
|
-
log9.step("Step 5/
|
|
1592
|
+
log9.step("Step 5/8 Remote setup");
|
|
1571
1593
|
const remoteDir = await ssh.resolveHome(sshCfg.remoteDir);
|
|
1572
1594
|
await checkRemotePrerequisites(ssh);
|
|
1573
1595
|
await uploadFiles(ssh, dir, remoteDir);
|
|
1574
|
-
log9.step("Step 6/
|
|
1596
|
+
log9.step("Step 6/8 Starting services");
|
|
1575
1597
|
await startServices(ssh, remoteDir);
|
|
1576
|
-
log9.step("Step 7/
|
|
1577
|
-
await runMigrations(ssh, dir, cfg.adminEmail, cfg.adminPassword);
|
|
1578
|
-
log9.step("Step 8/9 Verifying health");
|
|
1598
|
+
log9.step("Step 7/8 Verifying health");
|
|
1579
1599
|
await verifyHealth(cfg.appUrl, cfg.engineUrl, ssh, remoteDir);
|
|
1580
|
-
log9.step("Step
|
|
1600
|
+
log9.step("Step 8/8 Deploying Salesforce package");
|
|
1581
1601
|
await sfdcDeployInline({
|
|
1582
1602
|
appUrl: cfg.appUrl,
|
|
1583
1603
|
engineUrl: cfg.engineUrl,
|
|
@@ -1668,8 +1688,6 @@ async function runDeploy() {
|
|
|
1668
1688
|
log10.step("Restarting services");
|
|
1669
1689
|
await ssh.exec("docker compose up -d --remove-orphans", remoteDir);
|
|
1670
1690
|
log10.success("Services restarted");
|
|
1671
|
-
log10.step("Running database migrations");
|
|
1672
|
-
await runMigrations(ssh, dir);
|
|
1673
1691
|
outro2(
|
|
1674
1692
|
chalk3.green("\u2714 Deployment complete!") + `
|
|
1675
1693
|
|
|
@@ -1687,7 +1705,7 @@ async function runDeploy() {
|
|
|
1687
1705
|
// src/commands/doctor.ts
|
|
1688
1706
|
import { intro as intro3, outro as outro3, log as log11 } from "@clack/prompts";
|
|
1689
1707
|
import chalk4 from "chalk";
|
|
1690
|
-
import { execa
|
|
1708
|
+
import { execa } from "execa";
|
|
1691
1709
|
async function runDoctor() {
|
|
1692
1710
|
console.log();
|
|
1693
1711
|
intro3(chalk4.bold.cyan("Lead Routing \u2014 Health Check"));
|
|
@@ -1725,7 +1743,7 @@ async function runDoctor() {
|
|
|
1725
1743
|
}
|
|
1726
1744
|
async function checkDockerDaemon() {
|
|
1727
1745
|
try {
|
|
1728
|
-
await
|
|
1746
|
+
await execa("docker", ["info"], { reject: true });
|
|
1729
1747
|
return { label: "Docker daemon", pass: true };
|
|
1730
1748
|
} catch {
|
|
1731
1749
|
return { label: "Docker daemon", pass: false, detail: "not running" };
|
|
@@ -1733,7 +1751,7 @@ async function checkDockerDaemon() {
|
|
|
1733
1751
|
}
|
|
1734
1752
|
async function checkContainer(name, dir) {
|
|
1735
1753
|
try {
|
|
1736
|
-
const result = await
|
|
1754
|
+
const result = await execa(
|
|
1737
1755
|
"docker",
|
|
1738
1756
|
["compose", "ps", "--format", "json", name],
|
|
1739
1757
|
{ cwd: dir, reject: false }
|
|
@@ -1777,7 +1795,7 @@ async function checkEndpoint(label, url) {
|
|
|
1777
1795
|
|
|
1778
1796
|
// src/commands/logs.ts
|
|
1779
1797
|
import { log as log12 } from "@clack/prompts";
|
|
1780
|
-
import { execa as
|
|
1798
|
+
import { execa as execa2 } from "execa";
|
|
1781
1799
|
var VALID_SERVICES = ["web", "engine", "postgres", "redis"];
|
|
1782
1800
|
async function runLogs(service = "engine") {
|
|
1783
1801
|
if (!VALID_SERVICES.includes(service)) {
|
|
@@ -1792,7 +1810,7 @@ async function runLogs(service = "engine") {
|
|
|
1792
1810
|
console.log(`
|
|
1793
1811
|
Streaming logs for ${service} (Ctrl+C to stop)...
|
|
1794
1812
|
`);
|
|
1795
|
-
const child =
|
|
1813
|
+
const child = execa2("docker", ["compose", "logs", "-f", "--tail=100", service], {
|
|
1796
1814
|
cwd: dir,
|
|
1797
1815
|
stdio: "inherit",
|
|
1798
1816
|
reject: false
|
|
@@ -1802,14 +1820,14 @@ Streaming logs for ${service} (Ctrl+C to stop)...
|
|
|
1802
1820
|
|
|
1803
1821
|
// src/commands/status.ts
|
|
1804
1822
|
import { log as log13 } from "@clack/prompts";
|
|
1805
|
-
import { execa as
|
|
1823
|
+
import { execa as execa3 } from "execa";
|
|
1806
1824
|
async function runStatus() {
|
|
1807
1825
|
const dir = findInstallDir();
|
|
1808
1826
|
if (!dir) {
|
|
1809
1827
|
log13.error("No lead-routing.json found. Run `lead-routing init` first.");
|
|
1810
1828
|
process.exit(1);
|
|
1811
1829
|
}
|
|
1812
|
-
const result = await
|
|
1830
|
+
const result = await execa3("docker", ["compose", "ps"], {
|
|
1813
1831
|
cwd: dir,
|
|
1814
1832
|
stdio: "inherit",
|
|
1815
1833
|
reject: false
|
|
@@ -1821,15 +1839,15 @@ async function runStatus() {
|
|
|
1821
1839
|
}
|
|
1822
1840
|
|
|
1823
1841
|
// src/commands/config.ts
|
|
1824
|
-
import { readFileSync as
|
|
1842
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync4 } from "fs";
|
|
1825
1843
|
import { join as join7 } from "path";
|
|
1826
|
-
import { intro as intro4, outro as outro4, text as text3, password as password3, spinner as
|
|
1844
|
+
import { intro as intro4, outro as outro4, text as text3, password as password3, spinner as spinner6, log as log14 } from "@clack/prompts";
|
|
1827
1845
|
import chalk5 from "chalk";
|
|
1828
|
-
import { execa as
|
|
1846
|
+
import { execa as execa4 } from "execa";
|
|
1829
1847
|
function parseEnv(filePath) {
|
|
1830
1848
|
const map = /* @__PURE__ */ new Map();
|
|
1831
|
-
if (!
|
|
1832
|
-
for (const line of
|
|
1849
|
+
if (!existsSync4(filePath)) return map;
|
|
1850
|
+
for (const line of readFileSync4(filePath, "utf8").split("\n")) {
|
|
1833
1851
|
const trimmed = line.trim();
|
|
1834
1852
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1835
1853
|
const eq = trimmed.indexOf("=");
|
|
@@ -1839,7 +1857,7 @@ function parseEnv(filePath) {
|
|
|
1839
1857
|
return map;
|
|
1840
1858
|
}
|
|
1841
1859
|
function writeEnv(filePath, updates) {
|
|
1842
|
-
const lines =
|
|
1860
|
+
const lines = existsSync4(filePath) ? readFileSync4(filePath, "utf8").split("\n") : [];
|
|
1843
1861
|
const updated = /* @__PURE__ */ new Set();
|
|
1844
1862
|
const result = lines.map((line) => {
|
|
1845
1863
|
const trimmed = line.trim();
|
|
@@ -1899,10 +1917,10 @@ Callback URL for your Connected App: ${callbackUrl}`
|
|
|
1899
1917
|
writeEnv(envWeb, updates);
|
|
1900
1918
|
writeEnv(envEngine, updates);
|
|
1901
1919
|
log14.success("Updated .env.web and .env.engine");
|
|
1902
|
-
const s =
|
|
1920
|
+
const s = spinner6();
|
|
1903
1921
|
s.start("Restarting web and engine containers\u2026");
|
|
1904
1922
|
try {
|
|
1905
|
-
await
|
|
1923
|
+
await execa4("docker", ["compose", "up", "-d", "--force-recreate", "web", "engine"], {
|
|
1906
1924
|
cwd: dir
|
|
1907
1925
|
});
|
|
1908
1926
|
s.stop("Containers restarted");
|
|
@@ -1936,9 +1954,8 @@ function runConfigShow() {
|
|
|
1936
1954
|
}
|
|
1937
1955
|
|
|
1938
1956
|
// src/commands/sfdc.ts
|
|
1939
|
-
import { intro as intro5, outro as outro5, text as text4,
|
|
1957
|
+
import { intro as intro5, outro as outro5, text as text4, log as log15 } from "@clack/prompts";
|
|
1940
1958
|
import chalk6 from "chalk";
|
|
1941
|
-
import { execa as execa8 } from "execa";
|
|
1942
1959
|
async function runSfdcDeploy() {
|
|
1943
1960
|
intro5("Lead Routing \u2014 Deploy Salesforce Package");
|
|
1944
1961
|
let appUrl;
|
|
@@ -1964,18 +1981,6 @@ async function runSfdcDeploy() {
|
|
|
1964
1981
|
if (typeof rawEngine === "symbol") process.exit(0);
|
|
1965
1982
|
engineUrl = rawEngine.trim();
|
|
1966
1983
|
}
|
|
1967
|
-
const s = spinner9();
|
|
1968
|
-
s.start("Checking Salesforce CLI\u2026");
|
|
1969
|
-
try {
|
|
1970
|
-
await execa8("sf", ["--version"], { all: true });
|
|
1971
|
-
s.stop("Salesforce CLI found");
|
|
1972
|
-
} catch {
|
|
1973
|
-
s.stop("Salesforce CLI (sf) not found");
|
|
1974
|
-
log15.error(
|
|
1975
|
-
"Install the Salesforce CLI and re-run this command:\n https://developer.salesforce.com/tools/salesforcecli"
|
|
1976
|
-
);
|
|
1977
|
-
process.exit(1);
|
|
1978
|
-
}
|
|
1979
1984
|
const alias = await text4({
|
|
1980
1985
|
message: "Salesforce org alias (used to log in)",
|
|
1981
1986
|
placeholder: "lead-routing",
|
|
@@ -1988,7 +1993,7 @@ async function runSfdcDeploy() {
|
|
|
1988
1993
|
appUrl,
|
|
1989
1994
|
engineUrl,
|
|
1990
1995
|
orgAlias: alias,
|
|
1991
|
-
// Read from config if available
|
|
1996
|
+
// Read from config if available
|
|
1992
1997
|
sfdcClientId: config2?.sfdcClientId ?? "",
|
|
1993
1998
|
sfdcLoginUrl: config2?.sfdcLoginUrl ?? "https://login.salesforce.com",
|
|
1994
1999
|
installDir: dir ?? void 0
|
|
@@ -2006,7 +2011,7 @@ async function runSfdcDeploy() {
|
|
|
2006
2011
|
}
|
|
2007
2012
|
|
|
2008
2013
|
// src/commands/uninstall.ts
|
|
2009
|
-
import { rmSync as rmSync2, existsSync as
|
|
2014
|
+
import { rmSync as rmSync2, existsSync as existsSync5 } from "fs";
|
|
2010
2015
|
import { intro as intro6, outro as outro6, log as log16, confirm as confirm3, password as promptPassword3, isCancel as isCancel5 } from "@clack/prompts";
|
|
2011
2016
|
import chalk7 from "chalk";
|
|
2012
2017
|
async function runUninstall() {
|
|
@@ -2082,7 +2087,7 @@ async function runUninstall() {
|
|
|
2082
2087
|
await ssh.disconnect();
|
|
2083
2088
|
}
|
|
2084
2089
|
log16.step("Removing local config directory");
|
|
2085
|
-
if (
|
|
2090
|
+
if (existsSync5(dir)) {
|
|
2086
2091
|
rmSync2(dir, { recursive: true, force: true });
|
|
2087
2092
|
log16.success(`Removed ${dir}`);
|
|
2088
2093
|
}
|