@lead-routing/cli 0.1.5 → 0.1.6
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 +198 -277
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import { promises as dns } from "dns";
|
|
8
|
-
import { intro, outro, note as note4, log as
|
|
8
|
+
import { intro, outro, note as note4, log as log9, confirm as confirm2, cancel as cancel3, isCancel as isCancel4, password as promptPassword } from "@clack/prompts";
|
|
9
9
|
import chalk2 from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/steps/prerequisites.ts
|
|
@@ -69,7 +69,16 @@ async function checkSalesforceCLI() {
|
|
|
69
69
|
// src/steps/collect-ssh-config.ts
|
|
70
70
|
import { existsSync } from "fs";
|
|
71
71
|
import { homedir } from "os";
|
|
72
|
-
import {
|
|
72
|
+
import { join } from "path";
|
|
73
|
+
import { text, password, note, log as log2, cancel, isCancel } from "@clack/prompts";
|
|
74
|
+
var DEFAULT_KEYS = [
|
|
75
|
+
join(homedir(), ".ssh", "id_ed25519"),
|
|
76
|
+
join(homedir(), ".ssh", "id_rsa"),
|
|
77
|
+
join(homedir(), ".ssh", "id_ecdsa")
|
|
78
|
+
];
|
|
79
|
+
function detectDefaultKey() {
|
|
80
|
+
return DEFAULT_KEYS.find(existsSync);
|
|
81
|
+
}
|
|
73
82
|
function bail(value) {
|
|
74
83
|
if (isCancel(value)) {
|
|
75
84
|
cancel("Setup cancelled.");
|
|
@@ -77,9 +86,9 @@ function bail(value) {
|
|
|
77
86
|
}
|
|
78
87
|
throw new Error("Unexpected cancel");
|
|
79
88
|
}
|
|
80
|
-
async function collectSshConfig() {
|
|
89
|
+
async function collectSshConfig(opts = {}) {
|
|
81
90
|
note(
|
|
82
|
-
"The CLI will SSH into your server to deploy the full stack.\nYou will need:\n \u2022 Server hostname or IP address\n \u2022 SSH access (key
|
|
91
|
+
"The CLI will SSH into your server to deploy the full stack.\nYou will need:\n \u2022 Server hostname or IP address\n \u2022 SSH access (key auto-detected, or password)\n \u2022 Docker 24+ already installed on the server",
|
|
83
92
|
"Server connection"
|
|
84
93
|
);
|
|
85
94
|
const host = await text({
|
|
@@ -88,82 +97,43 @@ async function collectSshConfig() {
|
|
|
88
97
|
validate: (v) => !v ? "Required" : void 0
|
|
89
98
|
});
|
|
90
99
|
if (isCancel(host)) bail(host);
|
|
91
|
-
const portRaw = await text({
|
|
92
|
-
message: "SSH port",
|
|
93
|
-
placeholder: "22",
|
|
94
|
-
initialValue: "22",
|
|
95
|
-
validate: (v) => {
|
|
96
|
-
const n = parseInt(v, 10);
|
|
97
|
-
if (isNaN(n) || n < 1 || n > 65535) return "Must be a valid port (1\u201365535)";
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
if (isCancel(portRaw)) bail(portRaw);
|
|
101
|
-
const username = await text({
|
|
102
|
-
message: "SSH username",
|
|
103
|
-
placeholder: "root",
|
|
104
|
-
initialValue: "root",
|
|
105
|
-
validate: (v) => !v ? "Required" : void 0
|
|
106
|
-
});
|
|
107
|
-
if (isCancel(username)) bail(username);
|
|
108
|
-
const authMethod = await select({
|
|
109
|
-
message: "SSH authentication method",
|
|
110
|
-
options: [
|
|
111
|
-
{ value: "key", label: "SSH key file (recommended)" },
|
|
112
|
-
{ value: "password", label: "Password" }
|
|
113
|
-
]
|
|
114
|
-
});
|
|
115
|
-
if (isCancel(authMethod)) bail(authMethod);
|
|
116
100
|
let privateKeyPath;
|
|
117
101
|
let pwd;
|
|
118
|
-
if (
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!existsSync(resolved)) return `Key file not found: ${resolved}`;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
if (isCancel(keyPath)) bail(keyPath);
|
|
130
|
-
const raw = keyPath;
|
|
131
|
-
privateKeyPath = raw.startsWith("~") ? homedir() + raw.slice(1) : raw;
|
|
102
|
+
if (opts.sshKey) {
|
|
103
|
+
const resolved = opts.sshKey.startsWith("~") ? homedir() + opts.sshKey.slice(1) : opts.sshKey;
|
|
104
|
+
if (!existsSync(resolved)) {
|
|
105
|
+
log2.error(`SSH key not found: ${resolved}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
privateKeyPath = resolved;
|
|
109
|
+
log2.info(`Using SSH key: ${opts.sshKey}`);
|
|
132
110
|
} else {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
111
|
+
const detected = detectDefaultKey();
|
|
112
|
+
if (detected) {
|
|
113
|
+
privateKeyPath = detected;
|
|
114
|
+
log2.info(`Using SSH key: ${detected.replace(homedir(), "~")}`);
|
|
115
|
+
} else {
|
|
116
|
+
log2.warn("No SSH key found at ~/.ssh/id_ed25519, ~/.ssh/id_rsa, or ~/.ssh/id_ecdsa");
|
|
117
|
+
const p = await password({
|
|
118
|
+
message: `SSH password for ${opts.sshUser ?? "root"}@${host}`,
|
|
119
|
+
validate: (v) => !v ? "Required" : void 0
|
|
120
|
+
});
|
|
121
|
+
if (isCancel(p)) bail(p);
|
|
122
|
+
pwd = p;
|
|
123
|
+
}
|
|
139
124
|
}
|
|
140
|
-
const remoteDir = await text({
|
|
141
|
-
message: "Remote install directory on server",
|
|
142
|
-
placeholder: "~/lead-routing",
|
|
143
|
-
initialValue: "~/lead-routing",
|
|
144
|
-
validate: (v) => !v ? "Required" : void 0
|
|
145
|
-
});
|
|
146
|
-
if (isCancel(remoteDir)) bail(remoteDir);
|
|
147
125
|
return {
|
|
148
126
|
host,
|
|
149
|
-
port:
|
|
150
|
-
username,
|
|
127
|
+
port: opts.sshPort ?? 22,
|
|
128
|
+
username: opts.sshUser ?? "root",
|
|
151
129
|
privateKeyPath,
|
|
152
130
|
password: pwd,
|
|
153
|
-
remoteDir
|
|
131
|
+
remoteDir: opts.remoteDir ?? "~/lead-routing"
|
|
154
132
|
};
|
|
155
133
|
}
|
|
156
134
|
|
|
157
135
|
// src/steps/collect-config.ts
|
|
158
|
-
import {
|
|
159
|
-
text as text2,
|
|
160
|
-
password as password2,
|
|
161
|
-
select as select2,
|
|
162
|
-
confirm,
|
|
163
|
-
note as note2,
|
|
164
|
-
cancel as cancel2,
|
|
165
|
-
isCancel as isCancel2
|
|
166
|
-
} from "@clack/prompts";
|
|
136
|
+
import { text as text2, password as password2, note as note2, cancel as cancel2, isCancel as isCancel2 } from "@clack/prompts";
|
|
167
137
|
|
|
168
138
|
// src/utils/crypto.ts
|
|
169
139
|
import { randomBytes } from "crypto";
|
|
@@ -179,9 +149,9 @@ function bail2(value) {
|
|
|
179
149
|
}
|
|
180
150
|
throw new Error("Unexpected cancel");
|
|
181
151
|
}
|
|
182
|
-
async function collectConfig() {
|
|
152
|
+
async function collectConfig(opts = {}) {
|
|
183
153
|
note2(
|
|
184
|
-
"You will need:\n \u2022 A Salesforce Connected App (Client ID + Secret) \u2014 instructions below\n \u2022
|
|
154
|
+
"You will need:\n \u2022 A Salesforce Connected App (Client ID + Secret) \u2014 instructions below\n \u2022 Public HTTPS URLs for the web app and routing engine",
|
|
185
155
|
"Before you begin"
|
|
186
156
|
);
|
|
187
157
|
const appUrl = await text2({
|
|
@@ -200,8 +170,7 @@ async function collectConfig() {
|
|
|
200
170
|
if (isCancel2(appUrl)) bail2(appUrl);
|
|
201
171
|
const engineUrl = await text2({
|
|
202
172
|
message: "Engine URL (public URL Salesforce will use to route leads)",
|
|
203
|
-
placeholder: "https://engine.acme.com",
|
|
204
|
-
hint: "Subdomain: https://engine.acme.com \u2022 Same domain + port: https://acme.com:3001",
|
|
173
|
+
placeholder: "https://engine.acme.com or https://acme.com:3001",
|
|
205
174
|
validate: (v) => {
|
|
206
175
|
if (!v) return "Required";
|
|
207
176
|
try {
|
|
@@ -239,66 +208,12 @@ async function collectConfig() {
|
|
|
239
208
|
validate: (v) => !v ? "Required" : void 0
|
|
240
209
|
});
|
|
241
210
|
if (isCancel2(sfdcClientSecret)) bail2(sfdcClientSecret);
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
249
|
-
if (isCancel2(sfdcLoginUrlChoice)) bail2(sfdcLoginUrlChoice);
|
|
250
|
-
const sfdcLoginUrl = sfdcLoginUrlChoice;
|
|
251
|
-
const orgAlias = await text2({
|
|
252
|
-
message: "Salesforce org alias (used by the sf CLI to identify this org)",
|
|
253
|
-
placeholder: "lead-routing",
|
|
254
|
-
initialValue: "lead-routing",
|
|
255
|
-
validate: (v) => !v ? "Required" : void 0
|
|
256
|
-
});
|
|
257
|
-
if (isCancel2(orgAlias)) bail2(orgAlias);
|
|
258
|
-
const managedDb = await confirm({
|
|
259
|
-
message: "Manage PostgreSQL with Docker? (recommended \u2014 choose No to provide your own URL)",
|
|
260
|
-
initialValue: true
|
|
261
|
-
});
|
|
262
|
-
if (isCancel2(managedDb)) bail2(managedDb);
|
|
263
|
-
let databaseUrl = "";
|
|
264
|
-
let dbPassword = generateSecret(16);
|
|
265
|
-
if (managedDb) {
|
|
266
|
-
databaseUrl = "postgresql://leadrouting:" + dbPassword + "@postgres:5432/leadrouting";
|
|
267
|
-
} else {
|
|
268
|
-
const url = await text2({
|
|
269
|
-
message: "PostgreSQL connection URL",
|
|
270
|
-
placeholder: "postgresql://user:pass@host:5432/dbname",
|
|
271
|
-
validate: (v) => {
|
|
272
|
-
if (!v) return "Required";
|
|
273
|
-
if (!v.startsWith("postgresql://") && !v.startsWith("postgres://"))
|
|
274
|
-
return "Must start with postgresql:// or postgres://";
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
if (isCancel2(url)) bail2(url);
|
|
278
|
-
databaseUrl = url;
|
|
279
|
-
dbPassword = "";
|
|
280
|
-
}
|
|
281
|
-
const managedRedis = await confirm({
|
|
282
|
-
message: "Manage Redis with Docker? (recommended \u2014 choose No to provide your own URL)",
|
|
283
|
-
initialValue: true
|
|
284
|
-
});
|
|
285
|
-
if (isCancel2(managedRedis)) bail2(managedRedis);
|
|
286
|
-
let redisUrl = "";
|
|
287
|
-
if (managedRedis) {
|
|
288
|
-
redisUrl = "redis://redis:6379";
|
|
289
|
-
} else {
|
|
290
|
-
const url = await text2({
|
|
291
|
-
message: "Redis connection URL",
|
|
292
|
-
placeholder: "redis://user:pass@host:6379",
|
|
293
|
-
validate: (v) => {
|
|
294
|
-
if (!v) return "Required";
|
|
295
|
-
if (!v.startsWith("redis://") && !v.startsWith("rediss://"))
|
|
296
|
-
return "Must start with redis:// or rediss://";
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
if (isCancel2(url)) bail2(url);
|
|
300
|
-
redisUrl = url;
|
|
301
|
-
}
|
|
211
|
+
const sfdcLoginUrl = opts.sandbox ? "https://test.salesforce.com" : "https://login.salesforce.com";
|
|
212
|
+
const dbPassword = generateSecret(16);
|
|
213
|
+
const managedDb = !opts.externalDb;
|
|
214
|
+
const databaseUrl = opts.externalDb ?? `postgresql://leadrouting:${dbPassword}@postgres:5432/leadrouting`;
|
|
215
|
+
const managedRedis = !opts.externalRedis;
|
|
216
|
+
const redisUrl = opts.externalRedis ?? "redis://redis:6379";
|
|
302
217
|
note2("This creates the first admin user for the web app.", "Admin Account");
|
|
303
218
|
const adminEmail = await text2({
|
|
304
219
|
message: "Admin email address",
|
|
@@ -317,27 +232,6 @@ async function collectConfig() {
|
|
|
317
232
|
}
|
|
318
233
|
});
|
|
319
234
|
if (isCancel2(adminPassword)) bail2(adminPassword);
|
|
320
|
-
const wantResend = await confirm({
|
|
321
|
-
message: "Configure Resend for email invites? (optional)",
|
|
322
|
-
initialValue: false
|
|
323
|
-
});
|
|
324
|
-
if (isCancel2(wantResend)) bail2(wantResend);
|
|
325
|
-
let resendApiKey = "";
|
|
326
|
-
let feedbackToEmail = "";
|
|
327
|
-
if (wantResend) {
|
|
328
|
-
const key = await text2({
|
|
329
|
-
message: "Resend API key",
|
|
330
|
-
placeholder: "re_..."
|
|
331
|
-
});
|
|
332
|
-
if (isCancel2(key)) bail2(key);
|
|
333
|
-
resendApiKey = key ?? "";
|
|
334
|
-
const email = await text2({
|
|
335
|
-
message: "Email address to receive feedback",
|
|
336
|
-
placeholder: "feedback@acme.com"
|
|
337
|
-
});
|
|
338
|
-
if (isCancel2(email)) bail2(email);
|
|
339
|
-
feedbackToEmail = email ?? "";
|
|
340
|
-
}
|
|
341
235
|
const sessionSecret = generateSecret(32);
|
|
342
236
|
const engineWebhookSecret = generateSecret(32);
|
|
343
237
|
const adminSecret = generateSecret(16);
|
|
@@ -347,16 +241,15 @@ async function collectConfig() {
|
|
|
347
241
|
sfdcClientId: sfdcClientId.trim(),
|
|
348
242
|
sfdcClientSecret: sfdcClientSecret.trim(),
|
|
349
243
|
sfdcLoginUrl,
|
|
350
|
-
orgAlias,
|
|
351
244
|
managedDb,
|
|
352
245
|
databaseUrl,
|
|
353
|
-
dbPassword,
|
|
246
|
+
dbPassword: managedDb ? dbPassword : "",
|
|
354
247
|
managedRedis,
|
|
355
248
|
redisUrl,
|
|
356
249
|
adminEmail,
|
|
357
250
|
adminPassword,
|
|
358
|
-
resendApiKey,
|
|
359
|
-
feedbackToEmail,
|
|
251
|
+
resendApiKey: "",
|
|
252
|
+
feedbackToEmail: "",
|
|
360
253
|
sessionSecret,
|
|
361
254
|
engineWebhookSecret,
|
|
362
255
|
adminSecret
|
|
@@ -365,9 +258,9 @@ async function collectConfig() {
|
|
|
365
258
|
|
|
366
259
|
// src/steps/generate-files.ts
|
|
367
260
|
import { mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
|
|
368
|
-
import { join as
|
|
261
|
+
import { join as join3, dirname } from "path";
|
|
369
262
|
import { fileURLToPath } from "url";
|
|
370
|
-
import { log as
|
|
263
|
+
import { log as log3 } from "@clack/prompts";
|
|
371
264
|
|
|
372
265
|
// src/templates/docker-compose.ts
|
|
373
266
|
function renderDockerCompose(c) {
|
|
@@ -596,9 +489,9 @@ function renderCaddyfile(appUrl, engineUrl) {
|
|
|
596
489
|
|
|
597
490
|
// src/utils/config.ts
|
|
598
491
|
import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
599
|
-
import { join } from "path";
|
|
492
|
+
import { join as join2 } from "path";
|
|
600
493
|
function getConfigPath(dir) {
|
|
601
|
-
return
|
|
494
|
+
return join2(dir, "lead-routing.json");
|
|
602
495
|
}
|
|
603
496
|
function readConfig(dir) {
|
|
604
497
|
const path2 = getConfigPath(dir);
|
|
@@ -613,10 +506,10 @@ function writeConfig(dir, config2) {
|
|
|
613
506
|
writeFileSync(getConfigPath(dir), JSON.stringify(config2, null, 2), "utf8");
|
|
614
507
|
}
|
|
615
508
|
function findInstallDir(startDir = process.cwd()) {
|
|
616
|
-
const candidate =
|
|
509
|
+
const candidate = join2(startDir, "lead-routing.json");
|
|
617
510
|
if (existsSync2(candidate)) return startDir;
|
|
618
|
-
const nested =
|
|
619
|
-
if (existsSync2(nested)) return
|
|
511
|
+
const nested = join2(startDir, "lead-routing", "lead-routing.json");
|
|
512
|
+
if (existsSync2(nested)) return join2(startDir, "lead-routing");
|
|
620
513
|
return null;
|
|
621
514
|
}
|
|
622
515
|
|
|
@@ -624,14 +517,14 @@ function findInstallDir(startDir = process.cwd()) {
|
|
|
624
517
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
625
518
|
function getCliVersion() {
|
|
626
519
|
try {
|
|
627
|
-
const pkg = JSON.parse(readFileSync2(
|
|
520
|
+
const pkg = JSON.parse(readFileSync2(join3(__dirname, "../../package.json"), "utf8"));
|
|
628
521
|
return pkg.version ?? "0.1.0";
|
|
629
522
|
} catch {
|
|
630
523
|
return "0.1.0";
|
|
631
524
|
}
|
|
632
525
|
}
|
|
633
526
|
function generateFiles(cfg, sshCfg) {
|
|
634
|
-
const dir =
|
|
527
|
+
const dir = join3(process.cwd(), "lead-routing");
|
|
635
528
|
mkdirSync(dir, { recursive: true });
|
|
636
529
|
const dockerEngineUrl = `http://engine:3001`;
|
|
637
530
|
const composeContent = renderDockerCompose({
|
|
@@ -639,12 +532,12 @@ function generateFiles(cfg, sshCfg) {
|
|
|
639
532
|
managedRedis: cfg.managedRedis,
|
|
640
533
|
dbPassword: cfg.dbPassword
|
|
641
534
|
});
|
|
642
|
-
const composeFile =
|
|
535
|
+
const composeFile = join3(dir, "docker-compose.yml");
|
|
643
536
|
writeFileSync2(composeFile, composeContent, "utf8");
|
|
644
|
-
|
|
537
|
+
log3.success("Generated docker-compose.yml");
|
|
645
538
|
const caddyfileContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl);
|
|
646
|
-
writeFileSync2(
|
|
647
|
-
|
|
539
|
+
writeFileSync2(join3(dir, "Caddyfile"), caddyfileContent, "utf8");
|
|
540
|
+
log3.success("Generated Caddyfile");
|
|
648
541
|
const envWebContent = renderEnvWeb({
|
|
649
542
|
appUrl: cfg.appUrl,
|
|
650
543
|
engineUrl: dockerEngineUrl,
|
|
@@ -659,9 +552,9 @@ function generateFiles(cfg, sshCfg) {
|
|
|
659
552
|
resendApiKey: cfg.resendApiKey || void 0,
|
|
660
553
|
feedbackToEmail: cfg.feedbackToEmail || void 0
|
|
661
554
|
});
|
|
662
|
-
const envWeb =
|
|
555
|
+
const envWeb = join3(dir, ".env.web");
|
|
663
556
|
writeFileSync2(envWeb, envWebContent, "utf8");
|
|
664
|
-
|
|
557
|
+
log3.success("Generated .env.web");
|
|
665
558
|
const envEngineContent = renderEnvEngine({
|
|
666
559
|
databaseUrl: cfg.databaseUrl,
|
|
667
560
|
redisUrl: cfg.redisUrl,
|
|
@@ -670,9 +563,9 @@ function generateFiles(cfg, sshCfg) {
|
|
|
670
563
|
sfdcLoginUrl: cfg.sfdcLoginUrl,
|
|
671
564
|
engineWebhookSecret: cfg.engineWebhookSecret
|
|
672
565
|
});
|
|
673
|
-
const envEngine =
|
|
566
|
+
const envEngine = join3(dir, ".env.engine");
|
|
674
567
|
writeFileSync2(envEngine, envEngineContent, "utf8");
|
|
675
|
-
|
|
568
|
+
log3.success("Generated .env.engine");
|
|
676
569
|
writeConfig(dir, {
|
|
677
570
|
appUrl: cfg.appUrl,
|
|
678
571
|
engineUrl: cfg.engineUrl,
|
|
@@ -695,12 +588,12 @@ function generateFiles(cfg, sshCfg) {
|
|
|
695
588
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
696
589
|
version: getCliVersion()
|
|
697
590
|
});
|
|
698
|
-
|
|
591
|
+
log3.success("Generated lead-routing.json");
|
|
699
592
|
return { dir, composeFile, envWeb, envEngine, adminSecret: cfg.adminSecret };
|
|
700
593
|
}
|
|
701
594
|
|
|
702
595
|
// src/steps/check-remote-prerequisites.ts
|
|
703
|
-
import { log as
|
|
596
|
+
import { log as log4 } from "@clack/prompts";
|
|
704
597
|
async function checkRemotePrerequisites(ssh) {
|
|
705
598
|
const results = await Promise.all([
|
|
706
599
|
checkRemoteDocker(ssh),
|
|
@@ -712,15 +605,15 @@ async function checkRemotePrerequisites(ssh) {
|
|
|
712
605
|
const warnings = results.filter((r) => !r.ok && r.warn);
|
|
713
606
|
for (const r of results) {
|
|
714
607
|
if (r.ok) {
|
|
715
|
-
|
|
608
|
+
log4.success(r.label);
|
|
716
609
|
} else if (r.warn) {
|
|
717
|
-
|
|
610
|
+
log4.warn(r.label);
|
|
718
611
|
} else {
|
|
719
|
-
|
|
612
|
+
log4.error(r.label);
|
|
720
613
|
}
|
|
721
614
|
}
|
|
722
615
|
if (warnings.length > 0) {
|
|
723
|
-
|
|
616
|
+
log4.warn("Non-blocking warnings above \u2014 setup will continue.");
|
|
724
617
|
}
|
|
725
618
|
if (failed.length > 0) {
|
|
726
619
|
throw new Error(
|
|
@@ -794,7 +687,7 @@ async function checkRemotePort(ssh, port) {
|
|
|
794
687
|
}
|
|
795
688
|
|
|
796
689
|
// src/steps/upload-files.ts
|
|
797
|
-
import { join as
|
|
690
|
+
import { join as join4 } from "path";
|
|
798
691
|
import { spinner as spinner2 } from "@clack/prompts";
|
|
799
692
|
async function uploadFiles(ssh, localDir, remoteDir) {
|
|
800
693
|
const s = spinner2();
|
|
@@ -810,7 +703,7 @@ async function uploadFiles(ssh, localDir, remoteDir) {
|
|
|
810
703
|
];
|
|
811
704
|
await ssh.upload(
|
|
812
705
|
filenames.map((f) => ({
|
|
813
|
-
local:
|
|
706
|
+
local: join4(localDir, f),
|
|
814
707
|
remote: `${remoteDir}/${f}`
|
|
815
708
|
}))
|
|
816
709
|
);
|
|
@@ -822,7 +715,7 @@ async function uploadFiles(ssh, localDir, remoteDir) {
|
|
|
822
715
|
}
|
|
823
716
|
|
|
824
717
|
// src/steps/start-services.ts
|
|
825
|
-
import { spinner as spinner3, log as
|
|
718
|
+
import { spinner as spinner3, log as log5 } from "@clack/prompts";
|
|
826
719
|
async function startServices(ssh, remoteDir) {
|
|
827
720
|
await wipeStalePostgresVolume(ssh, remoteDir);
|
|
828
721
|
await pullImages(ssh, remoteDir);
|
|
@@ -843,7 +736,7 @@ async function wipeStalePostgresVolume(ssh, remoteDir) {
|
|
|
843
736
|
s.stop("Old volumes removed \u2014 database will be initialised fresh");
|
|
844
737
|
} catch {
|
|
845
738
|
s.stop("Could not remove old volumes \u2014 proceeding anyway");
|
|
846
|
-
|
|
739
|
+
log5.warn(
|
|
847
740
|
`If migrations fail with "authentication error", remove the postgres_data volume manually:
|
|
848
741
|
ssh into your server and run: docker volume rm ${volumeName}`
|
|
849
742
|
);
|
|
@@ -857,7 +750,7 @@ async function pullImages(ssh, remoteDir) {
|
|
|
857
750
|
s.stop("Images pulled successfully");
|
|
858
751
|
} catch {
|
|
859
752
|
s.stop("Could not pull images from registry \u2014 using local images if available");
|
|
860
|
-
|
|
753
|
+
log5.warn(
|
|
861
754
|
"Registry pull failed. If images are available on the server locally, setup will continue."
|
|
862
755
|
);
|
|
863
756
|
}
|
|
@@ -892,7 +785,7 @@ async function waitForPostgres(ssh, remoteDir) {
|
|
|
892
785
|
}
|
|
893
786
|
if (!containerReady) {
|
|
894
787
|
s.stop("PostgreSQL readiness check timed out \u2014 continuing anyway");
|
|
895
|
-
|
|
788
|
+
log5.warn("PostgreSQL may not be fully ready. If migrations fail, try `lead-routing deploy`.");
|
|
896
789
|
return;
|
|
897
790
|
}
|
|
898
791
|
for (let j = 0; j < 8; j++) {
|
|
@@ -907,7 +800,7 @@ async function waitForPostgres(ssh, remoteDir) {
|
|
|
907
800
|
await sleep(1e3);
|
|
908
801
|
}
|
|
909
802
|
s.stop("PostgreSQL is ready");
|
|
910
|
-
|
|
803
|
+
log5.warn("Host TCP port check timed out \u2014 tunnel may have issues. Proceeding anyway.");
|
|
911
804
|
}
|
|
912
805
|
function sleep(ms) {
|
|
913
806
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -1022,7 +915,7 @@ ON CONFLICT ("orgId", email) DO NOTHING;
|
|
|
1022
915
|
}
|
|
1023
916
|
|
|
1024
917
|
// src/steps/verify-health.ts
|
|
1025
|
-
import { spinner as spinner5, log as
|
|
918
|
+
import { spinner as spinner5, log as log6 } from "@clack/prompts";
|
|
1026
919
|
async function verifyHealth(appUrl, engineUrl, ssh, remoteDir) {
|
|
1027
920
|
const checks = [
|
|
1028
921
|
{ service: "Web app", url: `${appUrl}/api/health` },
|
|
@@ -1031,17 +924,17 @@ async function verifyHealth(appUrl, engineUrl, ssh, remoteDir) {
|
|
|
1031
924
|
const results = await Promise.all(checks.map(({ service, url }) => pollHealth(service, url)));
|
|
1032
925
|
for (const r of results) {
|
|
1033
926
|
if (r.ok) {
|
|
1034
|
-
|
|
927
|
+
log6.success(`${r.service} \u2014 ${r.url}`);
|
|
1035
928
|
} else {
|
|
1036
|
-
|
|
929
|
+
log6.warn(`${r.service} \u2014 did not respond after ${r.detail}`);
|
|
1037
930
|
}
|
|
1038
931
|
}
|
|
1039
932
|
const failed = results.filter((r) => !r.ok);
|
|
1040
933
|
if (failed.length === 0) return;
|
|
1041
|
-
|
|
934
|
+
log6.info("Fetching remote diagnostics\u2026");
|
|
1042
935
|
try {
|
|
1043
936
|
const { stdout: ps } = await ssh.execSilent("docker compose ps --format table", remoteDir);
|
|
1044
|
-
if (ps.trim())
|
|
937
|
+
if (ps.trim()) log6.info(`Container status:
|
|
1045
938
|
${ps.trim()}`);
|
|
1046
939
|
} catch {
|
|
1047
940
|
}
|
|
@@ -1050,7 +943,7 @@ ${ps.trim()}`);
|
|
|
1050
943
|
"docker compose logs caddy --tail 30 --no-color 2>&1",
|
|
1051
944
|
remoteDir
|
|
1052
945
|
);
|
|
1053
|
-
if (caddyLogs.trim())
|
|
946
|
+
if (caddyLogs.trim()) log6.info(`Caddy logs (last 30 lines):
|
|
1054
947
|
${caddyLogs.trim()}`);
|
|
1055
948
|
} catch {
|
|
1056
949
|
}
|
|
@@ -1101,10 +994,10 @@ function sleep2(ms) {
|
|
|
1101
994
|
|
|
1102
995
|
// src/steps/sfdc-deploy-inline.ts
|
|
1103
996
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, cpSync, rmSync } from "fs";
|
|
1104
|
-
import { join as
|
|
997
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
1105
998
|
import { tmpdir } from "os";
|
|
1106
999
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1107
|
-
import { spinner as spinner6, log as
|
|
1000
|
+
import { spinner as spinner6, log as log7 } from "@clack/prompts";
|
|
1108
1001
|
import { execa as execa3 } from "execa";
|
|
1109
1002
|
var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
|
|
1110
1003
|
function patchXml(content, tag, value) {
|
|
@@ -1123,7 +1016,7 @@ async function sfdcDeployInline(params) {
|
|
|
1123
1016
|
let sfCredEnv = {};
|
|
1124
1017
|
let targetOrgArgs = ["--target-org", orgAlias];
|
|
1125
1018
|
if (alreadyAuthed) {
|
|
1126
|
-
|
|
1019
|
+
log7.success("Using existing Salesforce authentication");
|
|
1127
1020
|
} else {
|
|
1128
1021
|
const { accessToken, instanceUrl, aliasStored } = await loginViaAppBridge(appUrl, orgAlias);
|
|
1129
1022
|
sfCredEnv = { SF_ACCESS_TOKEN: accessToken, SF_ORG_INSTANCE_URL: instanceUrl };
|
|
@@ -1132,10 +1025,10 @@ async function sfdcDeployInline(params) {
|
|
|
1132
1025
|
}
|
|
1133
1026
|
}
|
|
1134
1027
|
s.start("Copying Salesforce package\u2026");
|
|
1135
|
-
const inDist =
|
|
1136
|
-
const nextToDist =
|
|
1028
|
+
const inDist = join6(__dirname3, "sfdc-package");
|
|
1029
|
+
const nextToDist = join6(__dirname3, "..", "sfdc-package");
|
|
1137
1030
|
const bundledPkg = existsSync4(inDist) ? inDist : nextToDist;
|
|
1138
|
-
const destPkg =
|
|
1031
|
+
const destPkg = join6(installDir ?? tmpdir(), "lead-routing-sfdc-package");
|
|
1139
1032
|
if (!existsSync4(bundledPkg)) {
|
|
1140
1033
|
s.stop("sfdc-package not found in CLI bundle");
|
|
1141
1034
|
throw new Error(
|
|
@@ -1146,7 +1039,7 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1146
1039
|
if (existsSync4(destPkg)) rmSync(destPkg, { recursive: true, force: true });
|
|
1147
1040
|
cpSync(bundledPkg, destPkg, { recursive: true });
|
|
1148
1041
|
s.stop("Package copied");
|
|
1149
|
-
const ncPath =
|
|
1042
|
+
const ncPath = join6(
|
|
1150
1043
|
destPkg,
|
|
1151
1044
|
"force-app",
|
|
1152
1045
|
"main",
|
|
@@ -1158,7 +1051,7 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1158
1051
|
const nc = patchXml(readFileSync4(ncPath, "utf8"), "endpoint", engineUrl);
|
|
1159
1052
|
writeFileSync3(ncPath, nc, "utf8");
|
|
1160
1053
|
}
|
|
1161
|
-
const rssEnginePath =
|
|
1054
|
+
const rssEnginePath = join6(
|
|
1162
1055
|
destPkg,
|
|
1163
1056
|
"force-app",
|
|
1164
1057
|
"main",
|
|
@@ -1171,7 +1064,7 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1171
1064
|
rss = patchXml(rss, "description", "Lead Router Engine endpoint");
|
|
1172
1065
|
writeFileSync3(rssEnginePath, rss, "utf8");
|
|
1173
1066
|
}
|
|
1174
|
-
const rssAppPath =
|
|
1067
|
+
const rssAppPath = join6(
|
|
1175
1068
|
destPkg,
|
|
1176
1069
|
"force-app",
|
|
1177
1070
|
"main",
|
|
@@ -1184,7 +1077,7 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1184
1077
|
rss = patchXml(rss, "description", "Lead Router App URL");
|
|
1185
1078
|
writeFileSync3(rssAppPath, rss, "utf8");
|
|
1186
1079
|
}
|
|
1187
|
-
|
|
1080
|
+
log7.success("Remote Site Settings patched");
|
|
1188
1081
|
s.start("Deploying Salesforce package (this may take ~2 min)\u2026");
|
|
1189
1082
|
try {
|
|
1190
1083
|
await execa3(
|
|
@@ -1217,8 +1110,8 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1217
1110
|
s.stop("Permission set already assigned");
|
|
1218
1111
|
} else {
|
|
1219
1112
|
s.stop("Permission set assignment failed (non-fatal)");
|
|
1220
|
-
|
|
1221
|
-
|
|
1113
|
+
log7.warn(msg);
|
|
1114
|
+
log7.info(
|
|
1222
1115
|
"Grant access manually:\n Salesforce Setup \u2192 Users \u2192 Permission Sets \u2192 Lead Router Admin \u2192 Manage Assignments"
|
|
1223
1116
|
);
|
|
1224
1117
|
}
|
|
@@ -1267,8 +1160,8 @@ The CLI may need to be reinstalled: npm i -g @lead-routing/cli`
|
|
|
1267
1160
|
s.stop("Org settings written");
|
|
1268
1161
|
} catch (err) {
|
|
1269
1162
|
s.stop("Org settings write failed (non-fatal)");
|
|
1270
|
-
|
|
1271
|
-
|
|
1163
|
+
log7.warn(String(err));
|
|
1164
|
+
log7.info("Set manually: Salesforce \u2192 Custom Settings \u2192 Routing Settings \u2192 Manage");
|
|
1272
1165
|
}
|
|
1273
1166
|
}
|
|
1274
1167
|
async function loginViaAppBridge(rawAppUrl, orgAlias) {
|
|
@@ -1297,11 +1190,11 @@ Ensure the app is running and the URL is correct.`
|
|
|
1297
1190
|
);
|
|
1298
1191
|
}
|
|
1299
1192
|
s.stop("Auth session started");
|
|
1300
|
-
|
|
1193
|
+
log7.info(`Open this URL in your browser to authenticate with Salesforce:
|
|
1301
1194
|
|
|
1302
1195
|
${authUrl}
|
|
1303
1196
|
`);
|
|
1304
|
-
|
|
1197
|
+
log7.info('If Chrome shows a "Dangerous site" warning with no proceed option, paste the URL into Safari or Firefox.');
|
|
1305
1198
|
const opener = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
1306
1199
|
await execa3(opener, [authUrl], { reject: false }).catch(() => {
|
|
1307
1200
|
});
|
|
@@ -1341,17 +1234,17 @@ Ensure the app is running and the URL is correct.`
|
|
|
1341
1234
|
["org", "login", "access-token", "--instance-url", instanceUrl, "--alias", orgAlias, "--no-prompt"],
|
|
1342
1235
|
{ env: { ...process.env, SFDX_ACCESS_TOKEN: accessToken } }
|
|
1343
1236
|
);
|
|
1344
|
-
|
|
1237
|
+
log7.success(`Salesforce org saved as "${orgAlias}"`);
|
|
1345
1238
|
aliasStored = true;
|
|
1346
1239
|
} catch (err) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1240
|
+
log7.warn(`Could not persist sf CLI credentials: ${String(err)}`);
|
|
1241
|
+
log7.info("Continuing with direct token auth for this session.");
|
|
1349
1242
|
}
|
|
1350
1243
|
return { accessToken, instanceUrl, aliasStored };
|
|
1351
1244
|
}
|
|
1352
1245
|
|
|
1353
1246
|
// src/steps/app-launcher-guide.ts
|
|
1354
|
-
import { note as note3, confirm
|
|
1247
|
+
import { note as note3, confirm, isCancel as isCancel3, log as log8 } from "@clack/prompts";
|
|
1355
1248
|
import chalk from "chalk";
|
|
1356
1249
|
async function guideAppLauncherSetup(appUrl) {
|
|
1357
1250
|
note3(
|
|
@@ -1372,24 +1265,24 @@ async function guideAppLauncherSetup(appUrl) {
|
|
|
1372
1265
|
` + chalk.dim("Keep this terminal open while you complete the wizard."),
|
|
1373
1266
|
"Complete Salesforce setup"
|
|
1374
1267
|
);
|
|
1375
|
-
const done = await
|
|
1268
|
+
const done = await confirm({
|
|
1376
1269
|
message: "Have you completed the App Launcher wizard?",
|
|
1377
1270
|
initialValue: false
|
|
1378
1271
|
});
|
|
1379
1272
|
if (isCancel3(done)) {
|
|
1380
|
-
|
|
1273
|
+
log8.warn(
|
|
1381
1274
|
"Wizard skipped. Run `lead-routing sfdc deploy` to retry the Salesforce setup."
|
|
1382
1275
|
);
|
|
1383
1276
|
return;
|
|
1384
1277
|
}
|
|
1385
1278
|
if (!done) {
|
|
1386
|
-
|
|
1279
|
+
log8.warn(
|
|
1387
1280
|
`No problem \u2014 complete it at your own pace.
|
|
1388
1281
|
Open App Launcher \u2192 Lead Router Setup \u2192 Connect to Lead Router
|
|
1389
1282
|
Dashboard: ${appUrl}`
|
|
1390
1283
|
);
|
|
1391
1284
|
} else {
|
|
1392
|
-
|
|
1285
|
+
log8.success("Salesforce setup complete");
|
|
1393
1286
|
}
|
|
1394
1287
|
}
|
|
1395
1288
|
|
|
@@ -1519,11 +1412,11 @@ async function checkDnsResolvable(appUrl, engineUrl) {
|
|
|
1519
1412
|
try {
|
|
1520
1413
|
await dns.lookup(host);
|
|
1521
1414
|
} catch {
|
|
1522
|
-
|
|
1415
|
+
log9.warn(
|
|
1523
1416
|
`${chalk2.yellow(host)} does not resolve in DNS yet.
|
|
1524
1417
|
Check for typos \u2014 a bad domain will cause a 2-minute timeout at step 8.`
|
|
1525
1418
|
);
|
|
1526
|
-
const go = await
|
|
1419
|
+
const go = await confirm2({ message: "Continue anyway?", initialValue: true });
|
|
1527
1420
|
if (isCancel4(go) || !go) {
|
|
1528
1421
|
cancel3("Setup cancelled.");
|
|
1529
1422
|
process.exit(0);
|
|
@@ -1543,7 +1436,7 @@ async function runInit(options = {}) {
|
|
|
1543
1436
|
try {
|
|
1544
1437
|
const dir = findInstallDir();
|
|
1545
1438
|
if (!dir) {
|
|
1546
|
-
|
|
1439
|
+
log9.error("No lead-routing.json found \u2014 run `lead-routing init` first.");
|
|
1547
1440
|
process.exit(1);
|
|
1548
1441
|
}
|
|
1549
1442
|
const saved = readConfig(dir);
|
|
@@ -1555,7 +1448,7 @@ async function runInit(options = {}) {
|
|
|
1555
1448
|
if (typeof pw === "symbol") process.exit(0);
|
|
1556
1449
|
sshPassword = pw;
|
|
1557
1450
|
}
|
|
1558
|
-
|
|
1451
|
+
log9.step("Connecting to server");
|
|
1559
1452
|
await ssh.connect({
|
|
1560
1453
|
host: saved.ssh.host,
|
|
1561
1454
|
port: saved.ssh.port,
|
|
@@ -1564,11 +1457,11 @@ async function runInit(options = {}) {
|
|
|
1564
1457
|
password: sshPassword,
|
|
1565
1458
|
remoteDir: saved.remoteDir
|
|
1566
1459
|
});
|
|
1567
|
-
|
|
1460
|
+
log9.success(`Connected to ${saved.ssh.host}`);
|
|
1568
1461
|
const remoteDir = await ssh.resolveHome(saved.remoteDir);
|
|
1569
|
-
|
|
1462
|
+
log9.step("Step 8/9 Verifying health");
|
|
1570
1463
|
await verifyHealth(saved.appUrl, saved.engineUrl, ssh, remoteDir);
|
|
1571
|
-
|
|
1464
|
+
log9.step("Step 9/9 Deploying Salesforce package");
|
|
1572
1465
|
await sfdcDeployInline({
|
|
1573
1466
|
appUrl: saved.appUrl,
|
|
1574
1467
|
engineUrl: saved.engineUrl,
|
|
@@ -1592,7 +1485,7 @@ async function runInit(options = {}) {
|
|
|
1592
1485
|
);
|
|
1593
1486
|
} catch (err) {
|
|
1594
1487
|
const message = err instanceof Error ? err.message : String(err);
|
|
1595
|
-
|
|
1488
|
+
log9.error(`Resume failed: ${message}`);
|
|
1596
1489
|
process.exit(1);
|
|
1597
1490
|
} finally {
|
|
1598
1491
|
await ssh.disconnect();
|
|
@@ -1600,14 +1493,33 @@ async function runInit(options = {}) {
|
|
|
1600
1493
|
return;
|
|
1601
1494
|
}
|
|
1602
1495
|
try {
|
|
1603
|
-
|
|
1496
|
+
log9.step("Step 1/9 Checking local prerequisites");
|
|
1604
1497
|
await checkPrerequisites();
|
|
1605
|
-
|
|
1606
|
-
const sshCfg = await collectSshConfig(
|
|
1607
|
-
|
|
1608
|
-
|
|
1498
|
+
log9.step("Step 2/9 SSH connection");
|
|
1499
|
+
const sshCfg = await collectSshConfig({
|
|
1500
|
+
sshPort: options.sshPort,
|
|
1501
|
+
sshUser: options.sshUser,
|
|
1502
|
+
sshKey: options.sshKey,
|
|
1503
|
+
remoteDir: options.remoteDir
|
|
1504
|
+
});
|
|
1505
|
+
if (!dryRun) {
|
|
1506
|
+
try {
|
|
1507
|
+
await ssh.connect(sshCfg);
|
|
1508
|
+
log9.success(`Connected to ${sshCfg.host}`);
|
|
1509
|
+
} catch (err) {
|
|
1510
|
+
log9.error(`SSH connection failed: ${String(err)}`);
|
|
1511
|
+
log9.info("Fix your SSH credentials and re-run `lead-routing init`.");
|
|
1512
|
+
process.exit(1);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
log9.step("Step 3/9 Configuration");
|
|
1516
|
+
const cfg = await collectConfig({
|
|
1517
|
+
sandbox: options.sandbox,
|
|
1518
|
+
externalDb: options.externalDb,
|
|
1519
|
+
externalRedis: options.externalRedis
|
|
1520
|
+
});
|
|
1609
1521
|
await checkDnsResolvable(cfg.appUrl, cfg.engineUrl);
|
|
1610
|
-
|
|
1522
|
+
log9.step("Step 4/9 Generating config files");
|
|
1611
1523
|
const { dir, adminSecret } = generateFiles(cfg, sshCfg);
|
|
1612
1524
|
note4(
|
|
1613
1525
|
`Local config directory: ${chalk2.cyan(dir)}
|
|
@@ -1624,22 +1536,21 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1624
1536
|
);
|
|
1625
1537
|
return;
|
|
1626
1538
|
}
|
|
1627
|
-
|
|
1628
|
-
await ssh.connect(sshCfg);
|
|
1539
|
+
log9.step("Step 5/9 Remote setup");
|
|
1629
1540
|
const remoteDir = await ssh.resolveHome(sshCfg.remoteDir);
|
|
1630
1541
|
await checkRemotePrerequisites(ssh);
|
|
1631
1542
|
await uploadFiles(ssh, dir, remoteDir);
|
|
1632
|
-
|
|
1543
|
+
log9.step("Step 6/9 Starting services");
|
|
1633
1544
|
await startServices(ssh, remoteDir);
|
|
1634
|
-
|
|
1545
|
+
log9.step("Step 7/9 Database migrations");
|
|
1635
1546
|
await runMigrations(ssh, dir, cfg.adminEmail, cfg.adminPassword);
|
|
1636
|
-
|
|
1547
|
+
log9.step("Step 8/9 Verifying health");
|
|
1637
1548
|
await verifyHealth(cfg.appUrl, cfg.engineUrl, ssh, remoteDir);
|
|
1638
|
-
|
|
1549
|
+
log9.step("Step 9/9 Deploying Salesforce package");
|
|
1639
1550
|
await sfdcDeployInline({
|
|
1640
1551
|
appUrl: cfg.appUrl,
|
|
1641
1552
|
engineUrl: cfg.engineUrl,
|
|
1642
|
-
orgAlias:
|
|
1553
|
+
orgAlias: "lead-routing",
|
|
1643
1554
|
sfdcClientId: cfg.sfdcClientId,
|
|
1644
1555
|
sfdcLoginUrl: cfg.sfdcLoginUrl,
|
|
1645
1556
|
installDir: dir
|
|
@@ -1663,7 +1574,7 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1663
1574
|
);
|
|
1664
1575
|
} catch (err) {
|
|
1665
1576
|
const message = err instanceof Error ? err.message : String(err);
|
|
1666
|
-
|
|
1577
|
+
log9.error(`Setup failed: ${message}`);
|
|
1667
1578
|
process.exit(1);
|
|
1668
1579
|
} finally {
|
|
1669
1580
|
await ssh.disconnect();
|
|
@@ -1672,16 +1583,16 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1672
1583
|
|
|
1673
1584
|
// src/commands/deploy.ts
|
|
1674
1585
|
import { writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
1675
|
-
import { join as
|
|
1586
|
+
import { join as join7 } from "path";
|
|
1676
1587
|
import { tmpdir as tmpdir2 } from "os";
|
|
1677
|
-
import { intro as intro2, outro as outro2, log as
|
|
1588
|
+
import { intro as intro2, outro as outro2, log as log10, password as promptPassword2 } from "@clack/prompts";
|
|
1678
1589
|
import chalk3 from "chalk";
|
|
1679
1590
|
async function runDeploy() {
|
|
1680
1591
|
console.log();
|
|
1681
1592
|
intro2(chalk3.bold.cyan("Lead Routing \u2014 Deploy"));
|
|
1682
1593
|
const dir = findInstallDir();
|
|
1683
1594
|
if (!dir) {
|
|
1684
|
-
|
|
1595
|
+
log10.error(
|
|
1685
1596
|
"No lead-routing.json found. Run `lead-routing init` first, or run this command from your install directory."
|
|
1686
1597
|
);
|
|
1687
1598
|
process.exit(1);
|
|
@@ -1705,28 +1616,28 @@ async function runDeploy() {
|
|
|
1705
1616
|
password: sshPassword,
|
|
1706
1617
|
remoteDir: cfg.remoteDir
|
|
1707
1618
|
});
|
|
1708
|
-
|
|
1619
|
+
log10.success(`Connected to ${cfg.ssh.host}`);
|
|
1709
1620
|
} catch (err) {
|
|
1710
|
-
|
|
1621
|
+
log10.error(`SSH connection failed: ${String(err)}`);
|
|
1711
1622
|
process.exit(1);
|
|
1712
1623
|
}
|
|
1713
1624
|
try {
|
|
1714
1625
|
const remoteDir = await ssh.resolveHome(cfg.remoteDir);
|
|
1715
|
-
|
|
1626
|
+
log10.step("Syncing Caddyfile");
|
|
1716
1627
|
const caddyContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl);
|
|
1717
|
-
const tmpCaddy =
|
|
1628
|
+
const tmpCaddy = join7(tmpdir2(), "lead-routing-Caddyfile");
|
|
1718
1629
|
writeFileSync4(tmpCaddy, caddyContent, "utf8");
|
|
1719
1630
|
await ssh.upload([{ local: tmpCaddy, remote: `${remoteDir}/Caddyfile` }]);
|
|
1720
1631
|
unlinkSync(tmpCaddy);
|
|
1721
1632
|
await ssh.exec("docker compose restart caddy", remoteDir);
|
|
1722
|
-
|
|
1723
|
-
|
|
1633
|
+
log10.success("Caddyfile synced \u2014 waiting for TLS cert (~30s)");
|
|
1634
|
+
log10.step("Pulling latest Docker images");
|
|
1724
1635
|
await ssh.exec("docker compose pull", remoteDir);
|
|
1725
|
-
|
|
1726
|
-
|
|
1636
|
+
log10.success("Images pulled");
|
|
1637
|
+
log10.step("Restarting services");
|
|
1727
1638
|
await ssh.exec("docker compose up -d --remove-orphans", remoteDir);
|
|
1728
|
-
|
|
1729
|
-
|
|
1639
|
+
log10.success("Services restarted");
|
|
1640
|
+
log10.step("Running database migrations");
|
|
1730
1641
|
await runMigrations(ssh, dir);
|
|
1731
1642
|
outro2(
|
|
1732
1643
|
chalk3.green("\u2714 Deployment complete!") + `
|
|
@@ -1735,7 +1646,7 @@ async function runDeploy() {
|
|
|
1735
1646
|
);
|
|
1736
1647
|
} catch (err) {
|
|
1737
1648
|
const message = err instanceof Error ? err.message : String(err);
|
|
1738
|
-
|
|
1649
|
+
log10.error(`Deploy failed: ${message}`);
|
|
1739
1650
|
process.exit(1);
|
|
1740
1651
|
} finally {
|
|
1741
1652
|
await ssh.disconnect();
|
|
@@ -1743,7 +1654,7 @@ async function runDeploy() {
|
|
|
1743
1654
|
}
|
|
1744
1655
|
|
|
1745
1656
|
// src/commands/doctor.ts
|
|
1746
|
-
import { intro as intro3, outro as outro3, log as
|
|
1657
|
+
import { intro as intro3, outro as outro3, log as log11 } from "@clack/prompts";
|
|
1747
1658
|
import chalk4 from "chalk";
|
|
1748
1659
|
import { execa as execa4 } from "execa";
|
|
1749
1660
|
async function runDoctor() {
|
|
@@ -1751,7 +1662,7 @@ async function runDoctor() {
|
|
|
1751
1662
|
intro3(chalk4.bold.cyan("Lead Routing \u2014 Health Check"));
|
|
1752
1663
|
const dir = findInstallDir();
|
|
1753
1664
|
if (!dir) {
|
|
1754
|
-
|
|
1665
|
+
log11.error("No lead-routing.json found. Run `lead-routing init` first.");
|
|
1755
1666
|
process.exit(1);
|
|
1756
1667
|
}
|
|
1757
1668
|
const cfg = readConfig(dir);
|
|
@@ -1834,17 +1745,17 @@ async function checkEndpoint(label, url) {
|
|
|
1834
1745
|
}
|
|
1835
1746
|
|
|
1836
1747
|
// src/commands/logs.ts
|
|
1837
|
-
import { log as
|
|
1748
|
+
import { log as log12 } from "@clack/prompts";
|
|
1838
1749
|
import { execa as execa5 } from "execa";
|
|
1839
1750
|
var VALID_SERVICES = ["web", "engine", "postgres", "redis"];
|
|
1840
1751
|
async function runLogs(service = "engine") {
|
|
1841
1752
|
if (!VALID_SERVICES.includes(service)) {
|
|
1842
|
-
|
|
1753
|
+
log12.error(`Unknown service "${service}". Valid options: ${VALID_SERVICES.join(", ")}`);
|
|
1843
1754
|
process.exit(1);
|
|
1844
1755
|
}
|
|
1845
1756
|
const dir = findInstallDir();
|
|
1846
1757
|
if (!dir) {
|
|
1847
|
-
|
|
1758
|
+
log12.error("No lead-routing.json found. Run `lead-routing init` first.");
|
|
1848
1759
|
process.exit(1);
|
|
1849
1760
|
}
|
|
1850
1761
|
console.log(`
|
|
@@ -1859,12 +1770,12 @@ Streaming logs for ${service} (Ctrl+C to stop)...
|
|
|
1859
1770
|
}
|
|
1860
1771
|
|
|
1861
1772
|
// src/commands/status.ts
|
|
1862
|
-
import { log as
|
|
1773
|
+
import { log as log13 } from "@clack/prompts";
|
|
1863
1774
|
import { execa as execa6 } from "execa";
|
|
1864
1775
|
async function runStatus() {
|
|
1865
1776
|
const dir = findInstallDir();
|
|
1866
1777
|
if (!dir) {
|
|
1867
|
-
|
|
1778
|
+
log13.error("No lead-routing.json found. Run `lead-routing init` first.");
|
|
1868
1779
|
process.exit(1);
|
|
1869
1780
|
}
|
|
1870
1781
|
const result = await execa6("docker", ["compose", "ps"], {
|
|
@@ -1873,15 +1784,15 @@ async function runStatus() {
|
|
|
1873
1784
|
reject: false
|
|
1874
1785
|
});
|
|
1875
1786
|
if (result.exitCode !== 0) {
|
|
1876
|
-
|
|
1787
|
+
log13.error("Failed to get container status. Is Docker running?");
|
|
1877
1788
|
process.exit(1);
|
|
1878
1789
|
}
|
|
1879
1790
|
}
|
|
1880
1791
|
|
|
1881
1792
|
// src/commands/config.ts
|
|
1882
1793
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5 } from "fs";
|
|
1883
|
-
import { join as
|
|
1884
|
-
import { intro as intro4, outro as outro4, text as text3, password as password3, spinner as spinner7, log as
|
|
1794
|
+
import { join as join8 } from "path";
|
|
1795
|
+
import { intro as intro4, outro as outro4, text as text3, password as password3, spinner as spinner7, log as log14 } from "@clack/prompts";
|
|
1885
1796
|
import chalk5 from "chalk";
|
|
1886
1797
|
import { execa as execa7 } from "execa";
|
|
1887
1798
|
function parseEnv(filePath) {
|
|
@@ -1920,18 +1831,18 @@ async function runConfigSfdc() {
|
|
|
1920
1831
|
intro4("Lead Routing \u2014 Update Salesforce Credentials");
|
|
1921
1832
|
const dir = findInstallDir();
|
|
1922
1833
|
if (!dir) {
|
|
1923
|
-
|
|
1924
|
-
|
|
1834
|
+
log14.error("No lead-routing installation found in the current directory.");
|
|
1835
|
+
log14.info("Run `lead-routing init` first, or cd into your installation directory.");
|
|
1925
1836
|
process.exit(1);
|
|
1926
1837
|
}
|
|
1927
|
-
const envWeb =
|
|
1928
|
-
const envEngine =
|
|
1838
|
+
const envWeb = join8(dir, ".env.web");
|
|
1839
|
+
const envEngine = join8(dir, ".env.engine");
|
|
1929
1840
|
const currentWeb = parseEnv(envWeb);
|
|
1930
1841
|
const currentClientId = currentWeb.get("SFDC_CLIENT_ID") ?? "";
|
|
1931
1842
|
const currentLoginUrl = currentWeb.get("SFDC_LOGIN_URL") ?? "https://login.salesforce.com";
|
|
1932
1843
|
const currentAppUrl = currentWeb.get("APP_URL") ?? "";
|
|
1933
1844
|
const callbackUrl = `${currentAppUrl}/api/auth/callback`;
|
|
1934
|
-
|
|
1845
|
+
log14.info(
|
|
1935
1846
|
`Paste the credentials from your Salesforce Connected App.
|
|
1936
1847
|
Callback URL for your Connected App: ${callbackUrl}`
|
|
1937
1848
|
);
|
|
@@ -1956,7 +1867,7 @@ Callback URL for your Connected App: ${callbackUrl}`
|
|
|
1956
1867
|
};
|
|
1957
1868
|
writeEnv(envWeb, updates);
|
|
1958
1869
|
writeEnv(envEngine, updates);
|
|
1959
|
-
|
|
1870
|
+
log14.success("Updated .env.web and .env.engine");
|
|
1960
1871
|
const s = spinner7();
|
|
1961
1872
|
s.start("Restarting web and engine containers\u2026");
|
|
1962
1873
|
try {
|
|
@@ -1966,7 +1877,7 @@ Callback URL for your Connected App: ${callbackUrl}`
|
|
|
1966
1877
|
s.stop("Containers restarted");
|
|
1967
1878
|
} catch (err) {
|
|
1968
1879
|
s.stop("Restart failed \u2014 run `docker compose up -d --force-recreate web engine` manually");
|
|
1969
|
-
|
|
1880
|
+
log14.warn(String(err));
|
|
1970
1881
|
}
|
|
1971
1882
|
outro4(
|
|
1972
1883
|
"Salesforce credentials updated!\n\nNext: go to the web app \u2192 Settings \u2192 Connect Salesforce to refresh your OAuth tokens."
|
|
@@ -1978,7 +1889,7 @@ function runConfigShow() {
|
|
|
1978
1889
|
console.error("No lead-routing installation found in the current directory.");
|
|
1979
1890
|
process.exit(1);
|
|
1980
1891
|
}
|
|
1981
|
-
const envWeb =
|
|
1892
|
+
const envWeb = join8(dir, ".env.web");
|
|
1982
1893
|
const cfg = parseEnv(envWeb);
|
|
1983
1894
|
const adminSecret = cfg.get("ADMIN_SECRET") ?? "(not found)";
|
|
1984
1895
|
const appUrl = cfg.get("APP_URL") ?? "(not found)";
|
|
@@ -1994,7 +1905,7 @@ function runConfigShow() {
|
|
|
1994
1905
|
}
|
|
1995
1906
|
|
|
1996
1907
|
// src/commands/sfdc.ts
|
|
1997
|
-
import { intro as intro5, outro as outro5, text as text4, spinner as spinner8, log as
|
|
1908
|
+
import { intro as intro5, outro as outro5, text as text4, spinner as spinner8, log as log15 } from "@clack/prompts";
|
|
1998
1909
|
import chalk6 from "chalk";
|
|
1999
1910
|
import { execa as execa8 } from "execa";
|
|
2000
1911
|
async function runSfdcDeploy() {
|
|
@@ -2006,9 +1917,9 @@ async function runSfdcDeploy() {
|
|
|
2006
1917
|
if (config2?.appUrl && config2?.engineUrl) {
|
|
2007
1918
|
appUrl = config2.appUrl;
|
|
2008
1919
|
engineUrl = config2.engineUrl;
|
|
2009
|
-
|
|
1920
|
+
log15.info(`Using config from ${dir}/lead-routing.json`);
|
|
2010
1921
|
} else {
|
|
2011
|
-
|
|
1922
|
+
log15.warn("No lead-routing.json found \u2014 enter the URLs from your installation.");
|
|
2012
1923
|
const rawApp = await text4({
|
|
2013
1924
|
message: "App URL (e.g. https://leads.acme.com)",
|
|
2014
1925
|
validate: (v) => !v ? "Required" : void 0
|
|
@@ -2029,7 +1940,7 @@ async function runSfdcDeploy() {
|
|
|
2029
1940
|
s.stop("Salesforce CLI found");
|
|
2030
1941
|
} catch {
|
|
2031
1942
|
s.stop("Salesforce CLI (sf) not found");
|
|
2032
|
-
|
|
1943
|
+
log15.error(
|
|
2033
1944
|
"Install the Salesforce CLI and re-run this command:\n https://developer.salesforce.com/tools/salesforcecli"
|
|
2034
1945
|
);
|
|
2035
1946
|
process.exit(1);
|
|
@@ -2052,7 +1963,7 @@ async function runSfdcDeploy() {
|
|
|
2052
1963
|
installDir: dir ?? void 0
|
|
2053
1964
|
});
|
|
2054
1965
|
} catch (err) {
|
|
2055
|
-
|
|
1966
|
+
log15.error(err instanceof Error ? err.message : String(err));
|
|
2056
1967
|
process.exit(1);
|
|
2057
1968
|
}
|
|
2058
1969
|
await guideAppLauncherSetup(appUrl);
|
|
@@ -2066,7 +1977,17 @@ async function runSfdcDeploy() {
|
|
|
2066
1977
|
// src/index.ts
|
|
2067
1978
|
var program = new Command();
|
|
2068
1979
|
program.name("lead-routing").description("Self-hosted Lead Routing \u2014 scaffold, deploy, and manage your installation").version("0.1.0");
|
|
2069
|
-
program.command("init").description("Interactive setup wizard \u2014 configure and deploy the full Lead Routing stack").option("--dry-run", "
|
|
1980
|
+
program.command("init").description("Interactive setup wizard \u2014 configure and deploy the full Lead Routing stack").option("--dry-run", "Generate config files without connecting or deploying").option("--resume", "Skip to health check using existing lead-routing.json (post-timeout recovery)").option("--sandbox", "Use Salesforce sandbox (test.salesforce.com) instead of production").option("--ssh-port <port>", "SSH port (default: 22)", parseInt).option("--ssh-user <user>", "SSH username (default: root)").option("--ssh-key <path>", "Path to SSH private key (overrides auto-detection)").option("--remote-dir <path>", "Remote install directory (default: ~/lead-routing)").option("--external-db <url>", "Use external PostgreSQL URL instead of managed Docker container").option("--external-redis <url>", "Use external Redis URL instead of managed Docker container").action((opts) => runInit({
|
|
1981
|
+
dryRun: opts.dryRun,
|
|
1982
|
+
resume: opts.resume,
|
|
1983
|
+
sandbox: opts.sandbox,
|
|
1984
|
+
sshPort: opts.sshPort,
|
|
1985
|
+
sshUser: opts.sshUser,
|
|
1986
|
+
sshKey: opts.sshKey,
|
|
1987
|
+
remoteDir: opts.remoteDir,
|
|
1988
|
+
externalDb: opts.externalDb,
|
|
1989
|
+
externalRedis: opts.externalRedis
|
|
1990
|
+
}));
|
|
2070
1991
|
program.command("deploy").description("Pull latest images, restart services, and run any pending migrations").action(runDeploy);
|
|
2071
1992
|
program.command("doctor").description("Check the health of all services in your installation").action(runDoctor);
|
|
2072
1993
|
program.command("logs [service]").description("Stream logs from a service (web, engine, postgres, redis). Defaults to engine.").action((service) => runLogs(service));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lead-routing/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Self-hosted deployment CLI for Lead Routing",
|
|
5
5
|
"homepage": "https://github.com/lead-routing/lead-routing",
|
|
6
6
|
"keywords": ["salesforce", "lead-routing", "self-hosted", "deployment", "cli"],
|