@lead-routing/cli 0.1.5 → 0.1.7

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.
Files changed (2) hide show
  1. package/dist/index.js +224 -281
  2. 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 log8, confirm as confirm3, cancel as cancel3, isCancel as isCancel4, password as promptPassword } from "@clack/prompts";
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 { text, password, select, note, cancel, isCancel } from "@clack/prompts";
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 file recommended, password supported)\n \u2022 Docker 24+ already installed on the server",
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 (authMethod === "key") {
119
- const defaultKey = `${homedir()}/.ssh/id_rsa`;
120
- const keyPath = await text({
121
- message: "Path to SSH private key",
122
- placeholder: defaultKey,
123
- initialValue: `~/.ssh/id_rsa`,
124
- validate: (v) => {
125
- const resolved = v.startsWith("~") ? homedir() + v.slice(1) : v;
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 p = await password({
134
- message: "SSH password",
135
- validate: (v) => !v ? "Required" : void 0
136
- });
137
- if (isCancel(p)) bail(p);
138
- pwd = p;
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: parseInt(portRaw, 10),
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 A public URL or localhost for the app\n \u2022 PostgreSQL + Redis (or let Docker manage them)",
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 sfdcLoginUrlChoice = await select2({
243
- message: "Salesforce environment",
244
- options: [
245
- { value: "https://login.salesforce.com", label: "Production / Developer org" },
246
- { value: "https://test.salesforce.com", label: "Sandbox" }
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 join2, dirname } from "path";
261
+ import { join as join3, dirname } from "path";
369
262
  import { fileURLToPath } from "url";
370
- import { log as log2 } from "@clack/prompts";
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 join(dir, "lead-routing.json");
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 = join(startDir, "lead-routing.json");
509
+ const candidate = join2(startDir, "lead-routing.json");
617
510
  if (existsSync2(candidate)) return startDir;
618
- const nested = join(startDir, "lead-routing", "lead-routing.json");
619
- if (existsSync2(nested)) return join(startDir, "lead-routing");
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(join2(__dirname, "../../package.json"), "utf8"));
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 = join2(process.cwd(), "lead-routing");
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 = join2(dir, "docker-compose.yml");
535
+ const composeFile = join3(dir, "docker-compose.yml");
643
536
  writeFileSync2(composeFile, composeContent, "utf8");
644
- log2.success("Generated docker-compose.yml");
537
+ log3.success("Generated docker-compose.yml");
645
538
  const caddyfileContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl);
646
- writeFileSync2(join2(dir, "Caddyfile"), caddyfileContent, "utf8");
647
- log2.success("Generated Caddyfile");
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 = join2(dir, ".env.web");
555
+ const envWeb = join3(dir, ".env.web");
663
556
  writeFileSync2(envWeb, envWebContent, "utf8");
664
- log2.success("Generated .env.web");
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 = join2(dir, ".env.engine");
566
+ const envEngine = join3(dir, ".env.engine");
674
567
  writeFileSync2(envEngine, envEngineContent, "utf8");
675
- log2.success("Generated .env.engine");
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
- log2.success("Generated lead-routing.json");
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 log3 } from "@clack/prompts";
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
- log3.success(r.label);
608
+ log4.success(r.label);
716
609
  } else if (r.warn) {
717
- log3.warn(r.label);
610
+ log4.warn(r.label);
718
611
  } else {
719
- log3.error(r.label);
612
+ log4.error(r.label);
720
613
  }
721
614
  }
722
615
  if (warnings.length > 0) {
723
- log3.warn("Non-blocking warnings above \u2014 setup will continue.");
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 join3 } from "path";
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: join3(localDir, f),
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 log4 } from "@clack/prompts";
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
- log4.warn(
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
- log4.warn(
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
- log4.warn("PostgreSQL may not be fully ready. If migrations fail, try `lead-routing deploy`.");
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
- log4.warn("Host TCP port check timed out \u2014 tunnel may have issues. Proceeding anyway.");
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));
@@ -1000,9 +893,9 @@ async function seedAdminUser(localDir, localPort, adminEmail, adminPassword) {
1000
893
  const safeEmail = adminEmail.replace(/'/g, "''");
1001
894
  const safeWebhookSecret = webhookSecret.replace(/'/g, "''");
1002
895
  const sql = `
1003
- -- Create initial organisation if none exists
1004
- INSERT INTO organizations (id, "webhookSecret", "createdAt", "updatedAt")
1005
- SELECT gen_random_uuid(), '${safeWebhookSecret}', NOW(), NOW()
896
+ -- Create initial organisation if none exists (self-hosted defaults: PAID plan, unlimited seats/quota)
897
+ INSERT INTO organizations (id, "webhookSecret", plan, "seatsPurchased", "routingQuota", "isActive", "createdAt", "updatedAt")
898
+ SELECT gen_random_uuid(), '${safeWebhookSecret}', 'PAID', 9999, 999999, true, NOW(), NOW()
1006
899
  WHERE NOT EXISTS (SELECT 1 FROM organizations);
1007
900
 
1008
901
  -- Create admin AppUser under the first org (idempotent)
@@ -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 log5 } from "@clack/prompts";
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
- log5.success(`${r.service} \u2014 ${r.url}`);
927
+ log6.success(`${r.service} \u2014 ${r.url}`);
1035
928
  } else {
1036
- log5.warn(`${r.service} \u2014 did not respond after ${r.detail}`);
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
- log5.info("Fetching remote diagnostics\u2026");
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()) log5.info(`Container status:
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()) log5.info(`Caddy logs (last 30 lines):
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 join5, dirname as dirname3 } from "path";
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 log6 } from "@clack/prompts";
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
- log6.success("Using existing Salesforce authentication");
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 = join5(__dirname3, "sfdc-package");
1136
- const nextToDist = join5(__dirname3, "..", "sfdc-package");
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 = join5(installDir ?? tmpdir(), "lead-routing-sfdc-package");
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 = join5(
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 = join5(
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 = join5(
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
- log6.success("Remote Site Settings patched");
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
- log6.warn(msg);
1221
- log6.info(
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
- log6.warn(String(err));
1271
- log6.info("Set manually: Salesforce \u2192 Custom Settings \u2192 Routing Settings \u2192 Manage");
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
- log6.info(`Open this URL in your browser to authenticate with Salesforce:
1193
+ log7.info(`Open this URL in your browser to authenticate with Salesforce:
1301
1194
 
1302
1195
  ${authUrl}
1303
1196
  `);
1304
- log6.info('If Chrome shows a "Dangerous site" warning with no proceed option, paste the URL into Safari or Firefox.');
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
- log6.success(`Salesforce org saved as "${orgAlias}"`);
1237
+ log7.success(`Salesforce org saved as "${orgAlias}"`);
1345
1238
  aliasStored = true;
1346
1239
  } catch (err) {
1347
- log6.warn(`Could not persist sf CLI credentials: ${String(err)}`);
1348
- log6.info("Continuing with direct token auth for this session.");
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 as confirm2, isCancel as isCancel3, log as log7 } from "@clack/prompts";
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 confirm2({
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
- log7.warn(
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
- log7.warn(
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
- log7.success("Salesforce setup complete");
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
- log8.warn(
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 confirm3({ message: "Continue anyway?", initialValue: true });
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
- log8.error("No lead-routing.json found \u2014 run `lead-routing init` first.");
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
- log8.step("Connecting to server");
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
- log8.success(`Connected to ${saved.ssh.host}`);
1460
+ log9.success(`Connected to ${saved.ssh.host}`);
1568
1461
  const remoteDir = await ssh.resolveHome(saved.remoteDir);
1569
- log8.step("Step 8/9 Verifying health");
1462
+ log9.step("Step 8/9 Verifying health");
1570
1463
  await verifyHealth(saved.appUrl, saved.engineUrl, ssh, remoteDir);
1571
- log8.step("Step 9/9 Deploying Salesforce package");
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
- log8.error(`Resume failed: ${message}`);
1488
+ log9.error(`Resume failed: ${message}`);
1596
1489
  process.exit(1);
1597
1490
  } finally {
1598
1491
  await ssh.disconnect();
@@ -1600,14 +1493,55 @@ async function runInit(options = {}) {
1600
1493
  return;
1601
1494
  }
1602
1495
  try {
1603
- log8.step("Step 1/9 Checking local prerequisites");
1496
+ log9.step("Step 1/9 Checking local prerequisites");
1604
1497
  await checkPrerequisites();
1605
- log8.step("Step 2/9 Server connection");
1606
- const sshCfg = await collectSshConfig();
1607
- log8.step("Step 3/9 Configuration");
1608
- const cfg = await collectConfig();
1498
+ log9.step("Step 2/9 SSH connection");
1499
+ let 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
+ if (sshCfg.privateKeyPath && !sshCfg.password) {
1511
+ log9.warn(`Key auth failed \u2014 the server rejected the SSH key`);
1512
+ log9.info("Falling back to password auth\u2026");
1513
+ const pw = await promptPassword({
1514
+ message: `SSH password for ${sshCfg.username}@${sshCfg.host}`,
1515
+ validate: (v) => !v ? "Required" : void 0
1516
+ });
1517
+ if (isCancel4(pw)) {
1518
+ cancel3("Setup cancelled.");
1519
+ process.exit(0);
1520
+ }
1521
+ sshCfg = { ...sshCfg, privateKeyPath: void 0, password: pw };
1522
+ try {
1523
+ await ssh.connect(sshCfg);
1524
+ log9.success(`Connected to ${sshCfg.host}`);
1525
+ } catch (err2) {
1526
+ log9.error(`SSH connection failed: ${String(err2)}`);
1527
+ log9.info("Fix your SSH credentials and re-run `lead-routing init`.");
1528
+ process.exit(1);
1529
+ }
1530
+ } else {
1531
+ log9.error(`SSH connection failed: ${String(err)}`);
1532
+ log9.info("Fix your SSH credentials and re-run `lead-routing init`.");
1533
+ process.exit(1);
1534
+ }
1535
+ }
1536
+ }
1537
+ log9.step("Step 3/9 Configuration");
1538
+ const cfg = await collectConfig({
1539
+ sandbox: options.sandbox,
1540
+ externalDb: options.externalDb,
1541
+ externalRedis: options.externalRedis
1542
+ });
1609
1543
  await checkDnsResolvable(cfg.appUrl, cfg.engineUrl);
1610
- log8.step("Step 4/9 Generating config files");
1544
+ log9.step("Step 4/9 Generating config files");
1611
1545
  const { dir, adminSecret } = generateFiles(cfg, sshCfg);
1612
1546
  note4(
1613
1547
  `Local config directory: ${chalk2.cyan(dir)}
@@ -1624,22 +1558,21 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1624
1558
  );
1625
1559
  return;
1626
1560
  }
1627
- log8.step("Step 5/9 Connecting to server");
1628
- await ssh.connect(sshCfg);
1561
+ log9.step("Step 5/9 Remote setup");
1629
1562
  const remoteDir = await ssh.resolveHome(sshCfg.remoteDir);
1630
1563
  await checkRemotePrerequisites(ssh);
1631
1564
  await uploadFiles(ssh, dir, remoteDir);
1632
- log8.step("Step 6/9 Starting services");
1565
+ log9.step("Step 6/9 Starting services");
1633
1566
  await startServices(ssh, remoteDir);
1634
- log8.step("Step 7/9 Database migrations");
1567
+ log9.step("Step 7/9 Database migrations");
1635
1568
  await runMigrations(ssh, dir, cfg.adminEmail, cfg.adminPassword);
1636
- log8.step("Step 8/9 Verifying health");
1569
+ log9.step("Step 8/9 Verifying health");
1637
1570
  await verifyHealth(cfg.appUrl, cfg.engineUrl, ssh, remoteDir);
1638
- log8.step("Step 9/9 Deploying Salesforce package");
1571
+ log9.step("Step 9/9 Deploying Salesforce package");
1639
1572
  await sfdcDeployInline({
1640
1573
  appUrl: cfg.appUrl,
1641
1574
  engineUrl: cfg.engineUrl,
1642
- orgAlias: cfg.orgAlias,
1575
+ orgAlias: "lead-routing",
1643
1576
  sfdcClientId: cfg.sfdcClientId,
1644
1577
  sfdcLoginUrl: cfg.sfdcLoginUrl,
1645
1578
  installDir: dir
@@ -1663,7 +1596,7 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1663
1596
  );
1664
1597
  } catch (err) {
1665
1598
  const message = err instanceof Error ? err.message : String(err);
1666
- log8.error(`Setup failed: ${message}`);
1599
+ log9.error(`Setup failed: ${message}`);
1667
1600
  process.exit(1);
1668
1601
  } finally {
1669
1602
  await ssh.disconnect();
@@ -1672,16 +1605,16 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1672
1605
 
1673
1606
  // src/commands/deploy.ts
1674
1607
  import { writeFileSync as writeFileSync4, unlinkSync } from "fs";
1675
- import { join as join6 } from "path";
1608
+ import { join as join7 } from "path";
1676
1609
  import { tmpdir as tmpdir2 } from "os";
1677
- import { intro as intro2, outro as outro2, log as log9, password as promptPassword2 } from "@clack/prompts";
1610
+ import { intro as intro2, outro as outro2, log as log10, password as promptPassword2 } from "@clack/prompts";
1678
1611
  import chalk3 from "chalk";
1679
1612
  async function runDeploy() {
1680
1613
  console.log();
1681
1614
  intro2(chalk3.bold.cyan("Lead Routing \u2014 Deploy"));
1682
1615
  const dir = findInstallDir();
1683
1616
  if (!dir) {
1684
- log9.error(
1617
+ log10.error(
1685
1618
  "No lead-routing.json found. Run `lead-routing init` first, or run this command from your install directory."
1686
1619
  );
1687
1620
  process.exit(1);
@@ -1705,28 +1638,28 @@ async function runDeploy() {
1705
1638
  password: sshPassword,
1706
1639
  remoteDir: cfg.remoteDir
1707
1640
  });
1708
- log9.success(`Connected to ${cfg.ssh.host}`);
1641
+ log10.success(`Connected to ${cfg.ssh.host}`);
1709
1642
  } catch (err) {
1710
- log9.error(`SSH connection failed: ${String(err)}`);
1643
+ log10.error(`SSH connection failed: ${String(err)}`);
1711
1644
  process.exit(1);
1712
1645
  }
1713
1646
  try {
1714
1647
  const remoteDir = await ssh.resolveHome(cfg.remoteDir);
1715
- log9.step("Syncing Caddyfile");
1648
+ log10.step("Syncing Caddyfile");
1716
1649
  const caddyContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl);
1717
- const tmpCaddy = join6(tmpdir2(), "lead-routing-Caddyfile");
1650
+ const tmpCaddy = join7(tmpdir2(), "lead-routing-Caddyfile");
1718
1651
  writeFileSync4(tmpCaddy, caddyContent, "utf8");
1719
1652
  await ssh.upload([{ local: tmpCaddy, remote: `${remoteDir}/Caddyfile` }]);
1720
1653
  unlinkSync(tmpCaddy);
1721
1654
  await ssh.exec("docker compose restart caddy", remoteDir);
1722
- log9.success("Caddyfile synced \u2014 waiting for TLS cert (~30s)");
1723
- log9.step("Pulling latest Docker images");
1655
+ log10.success("Caddyfile synced \u2014 waiting for TLS cert (~30s)");
1656
+ log10.step("Pulling latest Docker images");
1724
1657
  await ssh.exec("docker compose pull", remoteDir);
1725
- log9.success("Images pulled");
1726
- log9.step("Restarting services");
1658
+ log10.success("Images pulled");
1659
+ log10.step("Restarting services");
1727
1660
  await ssh.exec("docker compose up -d --remove-orphans", remoteDir);
1728
- log9.success("Services restarted");
1729
- log9.step("Running database migrations");
1661
+ log10.success("Services restarted");
1662
+ log10.step("Running database migrations");
1730
1663
  await runMigrations(ssh, dir);
1731
1664
  outro2(
1732
1665
  chalk3.green("\u2714 Deployment complete!") + `
@@ -1735,7 +1668,7 @@ async function runDeploy() {
1735
1668
  );
1736
1669
  } catch (err) {
1737
1670
  const message = err instanceof Error ? err.message : String(err);
1738
- log9.error(`Deploy failed: ${message}`);
1671
+ log10.error(`Deploy failed: ${message}`);
1739
1672
  process.exit(1);
1740
1673
  } finally {
1741
1674
  await ssh.disconnect();
@@ -1743,7 +1676,7 @@ async function runDeploy() {
1743
1676
  }
1744
1677
 
1745
1678
  // src/commands/doctor.ts
1746
- import { intro as intro3, outro as outro3, log as log10 } from "@clack/prompts";
1679
+ import { intro as intro3, outro as outro3, log as log11 } from "@clack/prompts";
1747
1680
  import chalk4 from "chalk";
1748
1681
  import { execa as execa4 } from "execa";
1749
1682
  async function runDoctor() {
@@ -1751,7 +1684,7 @@ async function runDoctor() {
1751
1684
  intro3(chalk4.bold.cyan("Lead Routing \u2014 Health Check"));
1752
1685
  const dir = findInstallDir();
1753
1686
  if (!dir) {
1754
- log10.error("No lead-routing.json found. Run `lead-routing init` first.");
1687
+ log11.error("No lead-routing.json found. Run `lead-routing init` first.");
1755
1688
  process.exit(1);
1756
1689
  }
1757
1690
  const cfg = readConfig(dir);
@@ -1834,17 +1767,17 @@ async function checkEndpoint(label, url) {
1834
1767
  }
1835
1768
 
1836
1769
  // src/commands/logs.ts
1837
- import { log as log11 } from "@clack/prompts";
1770
+ import { log as log12 } from "@clack/prompts";
1838
1771
  import { execa as execa5 } from "execa";
1839
1772
  var VALID_SERVICES = ["web", "engine", "postgres", "redis"];
1840
1773
  async function runLogs(service = "engine") {
1841
1774
  if (!VALID_SERVICES.includes(service)) {
1842
- log11.error(`Unknown service "${service}". Valid options: ${VALID_SERVICES.join(", ")}`);
1775
+ log12.error(`Unknown service "${service}". Valid options: ${VALID_SERVICES.join(", ")}`);
1843
1776
  process.exit(1);
1844
1777
  }
1845
1778
  const dir = findInstallDir();
1846
1779
  if (!dir) {
1847
- log11.error("No lead-routing.json found. Run `lead-routing init` first.");
1780
+ log12.error("No lead-routing.json found. Run `lead-routing init` first.");
1848
1781
  process.exit(1);
1849
1782
  }
1850
1783
  console.log(`
@@ -1859,12 +1792,12 @@ Streaming logs for ${service} (Ctrl+C to stop)...
1859
1792
  }
1860
1793
 
1861
1794
  // src/commands/status.ts
1862
- import { log as log12 } from "@clack/prompts";
1795
+ import { log as log13 } from "@clack/prompts";
1863
1796
  import { execa as execa6 } from "execa";
1864
1797
  async function runStatus() {
1865
1798
  const dir = findInstallDir();
1866
1799
  if (!dir) {
1867
- log12.error("No lead-routing.json found. Run `lead-routing init` first.");
1800
+ log13.error("No lead-routing.json found. Run `lead-routing init` first.");
1868
1801
  process.exit(1);
1869
1802
  }
1870
1803
  const result = await execa6("docker", ["compose", "ps"], {
@@ -1873,15 +1806,15 @@ async function runStatus() {
1873
1806
  reject: false
1874
1807
  });
1875
1808
  if (result.exitCode !== 0) {
1876
- log12.error("Failed to get container status. Is Docker running?");
1809
+ log13.error("Failed to get container status. Is Docker running?");
1877
1810
  process.exit(1);
1878
1811
  }
1879
1812
  }
1880
1813
 
1881
1814
  // src/commands/config.ts
1882
1815
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5 } from "fs";
1883
- import { join as join7 } from "path";
1884
- import { intro as intro4, outro as outro4, text as text3, password as password3, spinner as spinner7, log as log13 } from "@clack/prompts";
1816
+ import { join as join8 } from "path";
1817
+ import { intro as intro4, outro as outro4, text as text3, password as password3, spinner as spinner7, log as log14 } from "@clack/prompts";
1885
1818
  import chalk5 from "chalk";
1886
1819
  import { execa as execa7 } from "execa";
1887
1820
  function parseEnv(filePath) {
@@ -1920,18 +1853,18 @@ async function runConfigSfdc() {
1920
1853
  intro4("Lead Routing \u2014 Update Salesforce Credentials");
1921
1854
  const dir = findInstallDir();
1922
1855
  if (!dir) {
1923
- log13.error("No lead-routing installation found in the current directory.");
1924
- log13.info("Run `lead-routing init` first, or cd into your installation directory.");
1856
+ log14.error("No lead-routing installation found in the current directory.");
1857
+ log14.info("Run `lead-routing init` first, or cd into your installation directory.");
1925
1858
  process.exit(1);
1926
1859
  }
1927
- const envWeb = join7(dir, ".env.web");
1928
- const envEngine = join7(dir, ".env.engine");
1860
+ const envWeb = join8(dir, ".env.web");
1861
+ const envEngine = join8(dir, ".env.engine");
1929
1862
  const currentWeb = parseEnv(envWeb);
1930
1863
  const currentClientId = currentWeb.get("SFDC_CLIENT_ID") ?? "";
1931
1864
  const currentLoginUrl = currentWeb.get("SFDC_LOGIN_URL") ?? "https://login.salesforce.com";
1932
1865
  const currentAppUrl = currentWeb.get("APP_URL") ?? "";
1933
1866
  const callbackUrl = `${currentAppUrl}/api/auth/callback`;
1934
- log13.info(
1867
+ log14.info(
1935
1868
  `Paste the credentials from your Salesforce Connected App.
1936
1869
  Callback URL for your Connected App: ${callbackUrl}`
1937
1870
  );
@@ -1956,7 +1889,7 @@ Callback URL for your Connected App: ${callbackUrl}`
1956
1889
  };
1957
1890
  writeEnv(envWeb, updates);
1958
1891
  writeEnv(envEngine, updates);
1959
- log13.success("Updated .env.web and .env.engine");
1892
+ log14.success("Updated .env.web and .env.engine");
1960
1893
  const s = spinner7();
1961
1894
  s.start("Restarting web and engine containers\u2026");
1962
1895
  try {
@@ -1966,7 +1899,7 @@ Callback URL for your Connected App: ${callbackUrl}`
1966
1899
  s.stop("Containers restarted");
1967
1900
  } catch (err) {
1968
1901
  s.stop("Restart failed \u2014 run `docker compose up -d --force-recreate web engine` manually");
1969
- log13.warn(String(err));
1902
+ log14.warn(String(err));
1970
1903
  }
1971
1904
  outro4(
1972
1905
  "Salesforce credentials updated!\n\nNext: go to the web app \u2192 Settings \u2192 Connect Salesforce to refresh your OAuth tokens."
@@ -1978,7 +1911,7 @@ function runConfigShow() {
1978
1911
  console.error("No lead-routing installation found in the current directory.");
1979
1912
  process.exit(1);
1980
1913
  }
1981
- const envWeb = join7(dir, ".env.web");
1914
+ const envWeb = join8(dir, ".env.web");
1982
1915
  const cfg = parseEnv(envWeb);
1983
1916
  const adminSecret = cfg.get("ADMIN_SECRET") ?? "(not found)";
1984
1917
  const appUrl = cfg.get("APP_URL") ?? "(not found)";
@@ -1994,7 +1927,7 @@ function runConfigShow() {
1994
1927
  }
1995
1928
 
1996
1929
  // src/commands/sfdc.ts
1997
- import { intro as intro5, outro as outro5, text as text4, spinner as spinner8, log as log14 } from "@clack/prompts";
1930
+ import { intro as intro5, outro as outro5, text as text4, spinner as spinner8, log as log15 } from "@clack/prompts";
1998
1931
  import chalk6 from "chalk";
1999
1932
  import { execa as execa8 } from "execa";
2000
1933
  async function runSfdcDeploy() {
@@ -2006,9 +1939,9 @@ async function runSfdcDeploy() {
2006
1939
  if (config2?.appUrl && config2?.engineUrl) {
2007
1940
  appUrl = config2.appUrl;
2008
1941
  engineUrl = config2.engineUrl;
2009
- log14.info(`Using config from ${dir}/lead-routing.json`);
1942
+ log15.info(`Using config from ${dir}/lead-routing.json`);
2010
1943
  } else {
2011
- log14.warn("No lead-routing.json found \u2014 enter the URLs from your installation.");
1944
+ log15.warn("No lead-routing.json found \u2014 enter the URLs from your installation.");
2012
1945
  const rawApp = await text4({
2013
1946
  message: "App URL (e.g. https://leads.acme.com)",
2014
1947
  validate: (v) => !v ? "Required" : void 0
@@ -2029,7 +1962,7 @@ async function runSfdcDeploy() {
2029
1962
  s.stop("Salesforce CLI found");
2030
1963
  } catch {
2031
1964
  s.stop("Salesforce CLI (sf) not found");
2032
- log14.error(
1965
+ log15.error(
2033
1966
  "Install the Salesforce CLI and re-run this command:\n https://developer.salesforce.com/tools/salesforcecli"
2034
1967
  );
2035
1968
  process.exit(1);
@@ -2052,7 +1985,7 @@ async function runSfdcDeploy() {
2052
1985
  installDir: dir ?? void 0
2053
1986
  });
2054
1987
  } catch (err) {
2055
- log14.error(err instanceof Error ? err.message : String(err));
1988
+ log15.error(err instanceof Error ? err.message : String(err));
2056
1989
  process.exit(1);
2057
1990
  }
2058
1991
  await guideAppLauncherSetup(appUrl);
@@ -2065,8 +1998,18 @@ async function runSfdcDeploy() {
2065
1998
 
2066
1999
  // src/index.ts
2067
2000
  var program = new Command();
2068
- 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", "Run the wizard and generate config files without starting Docker services").option("--resume", "Skip steps 1-7 and resume from health check using existing lead-routing.json").action((opts) => runInit({ dryRun: opts.dryRun, resume: opts.resume }));
2001
+ program.name("lead-routing").description("Self-hosted Lead Routing \u2014 scaffold, deploy, and manage your installation").version("0.1.7");
2002
+ 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({
2003
+ dryRun: opts.dryRun,
2004
+ resume: opts.resume,
2005
+ sandbox: opts.sandbox,
2006
+ sshPort: opts.sshPort,
2007
+ sshUser: opts.sshUser,
2008
+ sshKey: opts.sshKey,
2009
+ remoteDir: opts.remoteDir,
2010
+ externalDb: opts.externalDb,
2011
+ externalRedis: opts.externalRedis
2012
+ }));
2070
2013
  program.command("deploy").description("Pull latest images, restart services, and run any pending migrations").action(runDeploy);
2071
2014
  program.command("doctor").description("Check the health of all services in your installation").action(runDoctor);
2072
2015
  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.5",
3
+ "version": "0.1.7",
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"],