@lead-routing/cli 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -491,6 +491,8 @@ function writeConfig(dir, config2) {
|
|
|
491
491
|
function findInstallDir(startDir = process.cwd()) {
|
|
492
492
|
const candidate = join(startDir, "lead-routing.json");
|
|
493
493
|
if (existsSync2(candidate)) return startDir;
|
|
494
|
+
const dev = join(startDir, "lead-routing-dev", "lead-routing.json");
|
|
495
|
+
if (existsSync2(dev)) return join(startDir, "lead-routing-dev");
|
|
494
496
|
const nested = join(startDir, "lead-routing", "lead-routing.json");
|
|
495
497
|
if (existsSync2(nested)) return join(startDir, "lead-routing");
|
|
496
498
|
return null;
|
|
@@ -1589,8 +1591,10 @@ function runConfigShow() {
|
|
|
1589
1591
|
}
|
|
1590
1592
|
|
|
1591
1593
|
// src/commands/sfdc.ts
|
|
1592
|
-
import { intro as intro5, outro as outro5, text as text3, log as
|
|
1593
|
-
import
|
|
1594
|
+
import { intro as intro5, outro as outro5, text as text3, note as note4, confirm as confirm2, log as log14, isCancel as isCancel4, cancel as cancel4 } from "@clack/prompts";
|
|
1595
|
+
import chalk6 from "chalk";
|
|
1596
|
+
import { exec as exec2 } from "child_process";
|
|
1597
|
+
import { platform as platform2 } from "os";
|
|
1594
1598
|
|
|
1595
1599
|
// src/steps/sfdc-deploy-inline.ts
|
|
1596
1600
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync5, cpSync, rmSync } from "fs";
|
|
@@ -1719,9 +1723,9 @@ Content-Type: application/zip\r
|
|
|
1719
1723
|
body
|
|
1720
1724
|
});
|
|
1721
1725
|
if (!res.ok) {
|
|
1722
|
-
const
|
|
1726
|
+
const text7 = await res.text();
|
|
1723
1727
|
throw new Error(
|
|
1724
|
-
`Metadata deploy request failed (${res.status}): ${
|
|
1728
|
+
`Metadata deploy request failed (${res.status}): ${text7}`
|
|
1725
1729
|
);
|
|
1726
1730
|
}
|
|
1727
1731
|
const result = await res.json();
|
|
@@ -1738,9 +1742,9 @@ Content-Type: application/zip\r
|
|
|
1738
1742
|
const url = `${this.baseUrl}/metadata/deployRequest/${deployId}?includeDetails=true`;
|
|
1739
1743
|
const res = await fetch(url, { headers: this.headers() });
|
|
1740
1744
|
if (!res.ok) {
|
|
1741
|
-
const
|
|
1745
|
+
const text7 = await res.text();
|
|
1742
1746
|
throw new Error(
|
|
1743
|
-
`Deploy status check failed (${res.status}): ${
|
|
1747
|
+
`Deploy status check failed (${res.status}): ${text7}`
|
|
1744
1748
|
);
|
|
1745
1749
|
}
|
|
1746
1750
|
const data = await res.json();
|
|
@@ -2090,50 +2094,12 @@ Ensure the app is running and the URL is correct.`
|
|
|
2090
2094
|
return { accessToken, instanceUrl };
|
|
2091
2095
|
}
|
|
2092
2096
|
|
|
2093
|
-
// src/steps/app-launcher-guide.ts
|
|
2094
|
-
import { note as note4, confirm as confirm2, isCancel as isCancel4, log as log14 } from "@clack/prompts";
|
|
2095
|
-
import chalk6 from "chalk";
|
|
2096
|
-
async function guideAppLauncherSetup(appUrl) {
|
|
2097
|
-
note4(
|
|
2098
|
-
`Complete the following steps in Salesforce now:
|
|
2099
|
-
|
|
2100
|
-
${chalk6.cyan("1.")} Open ${chalk6.bold("App Launcher")} (grid icon, top-left in Salesforce)
|
|
2101
|
-
${chalk6.cyan("2.")} Search for ${chalk6.white('"Lead Router Setup"')} and click it
|
|
2102
|
-
${chalk6.cyan("3.")} Click ${chalk6.white('"Connect to Lead Router"')}
|
|
2103
|
-
\u2192 You will be redirected to ${chalk6.dim(appUrl)} and back
|
|
2104
|
-
\u2192 Authorize the OAuth connection when prompted
|
|
2105
|
-
|
|
2106
|
-
${chalk6.cyan("4.")} ${chalk6.bold("Step 1")} \u2014 wait for the ${chalk6.green('"Connected"')} checkmark (~5 sec)
|
|
2107
|
-
${chalk6.cyan("5.")} ${chalk6.bold("Step 2")} \u2014 click ${chalk6.white("Activate")} to enable Lead triggers
|
|
2108
|
-
${chalk6.cyan("6.")} ${chalk6.bold("Step 3")} \u2014 click ${chalk6.white("Sync Fields")} to index your Lead field schema
|
|
2109
|
-
${chalk6.cyan("7.")} ${chalk6.bold("Step 4")} \u2014 click ${chalk6.white("Send Test")} to fire a test routing event
|
|
2110
|
-
\u2192 ${chalk6.dim('"Test successful"')} or ${chalk6.dim('"No matching rule"')} are both valid
|
|
2111
|
-
|
|
2112
|
-
` + chalk6.dim("Keep this terminal open while you complete the wizard."),
|
|
2113
|
-
"Complete Salesforce setup"
|
|
2114
|
-
);
|
|
2115
|
-
const done = await confirm2({
|
|
2116
|
-
message: "Have you completed the App Launcher wizard?",
|
|
2117
|
-
initialValue: false
|
|
2118
|
-
});
|
|
2119
|
-
if (isCancel4(done)) {
|
|
2120
|
-
log14.warn(
|
|
2121
|
-
"Wizard skipped. Run `lead-routing sfdc deploy` to retry the Salesforce setup."
|
|
2122
|
-
);
|
|
2123
|
-
return;
|
|
2124
|
-
}
|
|
2125
|
-
if (!done) {
|
|
2126
|
-
log14.warn(
|
|
2127
|
-
`No problem \u2014 complete it at your own pace.
|
|
2128
|
-
Open App Launcher \u2192 Lead Router Setup \u2192 Connect to Lead Router
|
|
2129
|
-
Dashboard: ${appUrl}`
|
|
2130
|
-
);
|
|
2131
|
-
} else {
|
|
2132
|
-
log14.success("Salesforce setup complete");
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
2097
|
// src/commands/sfdc.ts
|
|
2098
|
+
var MANAGED_PACKAGE_INSTALL_URL2 = "https://login.salesforce.com/packaging/installPackage.apexp?p0=04tgL000000CTnp";
|
|
2099
|
+
function openBrowser2(url) {
|
|
2100
|
+
const cmd = platform2() === "darwin" ? "open" : "xdg-open";
|
|
2101
|
+
exec2(`${cmd} ${JSON.stringify(url)}`);
|
|
2102
|
+
}
|
|
2137
2103
|
async function runSfdcDeploy() {
|
|
2138
2104
|
intro5("Lead Routing \u2014 Deploy Salesforce Package");
|
|
2139
2105
|
let appUrl;
|
|
@@ -2143,9 +2109,9 @@ async function runSfdcDeploy() {
|
|
|
2143
2109
|
if (config2?.appUrl && config2?.engineUrl) {
|
|
2144
2110
|
appUrl = config2.appUrl;
|
|
2145
2111
|
engineUrl = config2.engineUrl;
|
|
2146
|
-
|
|
2112
|
+
log14.info(`Using config from ${dir}/lead-routing.json`);
|
|
2147
2113
|
} else {
|
|
2148
|
-
|
|
2114
|
+
log14.warn("No lead-routing.json found \u2014 enter the URLs from your installation.");
|
|
2149
2115
|
const rawApp = await text3({
|
|
2150
2116
|
message: "App URL (e.g. https://leads.acme.com)",
|
|
2151
2117
|
validate: (v) => !v ? "Required" : void 0
|
|
@@ -2159,6 +2125,29 @@ async function runSfdcDeploy() {
|
|
|
2159
2125
|
if (typeof rawEngine === "symbol") process.exit(0);
|
|
2160
2126
|
engineUrl = rawEngine.trim();
|
|
2161
2127
|
}
|
|
2128
|
+
note4(
|
|
2129
|
+
`The Lead Router managed package installs the required Connected App,
|
|
2130
|
+
triggers, and custom objects in your Salesforce org.
|
|
2131
|
+
|
|
2132
|
+
Install URL: ${chalk6.cyan(MANAGED_PACKAGE_INSTALL_URL2)}`,
|
|
2133
|
+
"Salesforce Package"
|
|
2134
|
+
);
|
|
2135
|
+
log14.info("Opening install URL in your browser...");
|
|
2136
|
+
openBrowser2(MANAGED_PACKAGE_INSTALL_URL2);
|
|
2137
|
+
log14.info(chalk6.dim("If the browser didn't open, visit the URL above manually."));
|
|
2138
|
+
const installed = await confirm2({
|
|
2139
|
+
message: 'Have you installed the package? (Click "Install for All Users" in Salesforce)',
|
|
2140
|
+
initialValue: false
|
|
2141
|
+
});
|
|
2142
|
+
if (isCancel4(installed)) {
|
|
2143
|
+
cancel4("Setup cancelled.");
|
|
2144
|
+
process.exit(0);
|
|
2145
|
+
}
|
|
2146
|
+
if (!installed) {
|
|
2147
|
+
log14.warn("You can install the package later \u2014 routing will not work until it is installed.");
|
|
2148
|
+
} else {
|
|
2149
|
+
log14.success("Salesforce package installed");
|
|
2150
|
+
}
|
|
2162
2151
|
const alias = await text3({
|
|
2163
2152
|
message: "Salesforce org alias (used to log in)",
|
|
2164
2153
|
placeholder: "lead-routing",
|
|
@@ -2175,49 +2164,48 @@ async function runSfdcDeploy() {
|
|
|
2175
2164
|
webhookSecret: config2?.engineWebhookSecret
|
|
2176
2165
|
});
|
|
2177
2166
|
} catch (err) {
|
|
2178
|
-
|
|
2167
|
+
log14.error(err instanceof Error ? err.message : String(err));
|
|
2179
2168
|
process.exit(1);
|
|
2180
2169
|
}
|
|
2181
|
-
await guideAppLauncherSetup(appUrl);
|
|
2182
2170
|
outro5(
|
|
2183
|
-
|
|
2171
|
+
chalk6.green("\u2714 Salesforce package deployed!") + `
|
|
2184
2172
|
|
|
2185
|
-
Your Lead Router dashboard: ${
|
|
2173
|
+
Your Lead Router dashboard: ${chalk6.cyan(appUrl)}`
|
|
2186
2174
|
);
|
|
2187
2175
|
}
|
|
2188
2176
|
|
|
2189
2177
|
// src/commands/uninstall.ts
|
|
2190
2178
|
import { rmSync as rmSync2, existsSync as existsSync6 } from "fs";
|
|
2191
|
-
import { intro as intro6, outro as outro6, log as
|
|
2192
|
-
import
|
|
2179
|
+
import { intro as intro6, outro as outro6, log as log15, confirm as confirm3, password as promptPassword3, isCancel as isCancel5 } from "@clack/prompts";
|
|
2180
|
+
import chalk7 from "chalk";
|
|
2193
2181
|
async function runUninstall() {
|
|
2194
2182
|
console.log();
|
|
2195
|
-
intro6(
|
|
2183
|
+
intro6(chalk7.bold.red("Lead Routing \u2014 Uninstall"));
|
|
2196
2184
|
const dir = findInstallDir();
|
|
2197
2185
|
if (!dir) {
|
|
2198
|
-
|
|
2186
|
+
log15.error(
|
|
2199
2187
|
"No lead-routing.json found. Run this command from your install directory."
|
|
2200
2188
|
);
|
|
2201
2189
|
process.exit(1);
|
|
2202
2190
|
}
|
|
2203
2191
|
const cfg = readConfig(dir);
|
|
2204
2192
|
if (!cfg.ssh) {
|
|
2205
|
-
|
|
2193
|
+
log15.error(
|
|
2206
2194
|
"This lead-routing.json was created with an older CLI version and has no SSH config. Please manually SSH into your server and run:\n\n cd ~/lead-routing && docker compose down -v && cd ~ && rm -rf ~/lead-routing"
|
|
2207
2195
|
);
|
|
2208
2196
|
process.exit(1);
|
|
2209
2197
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2198
|
+
log15.warn(chalk7.red("This will permanently destroy:"));
|
|
2199
|
+
log15.warn(` \u2022 Remote: ${cfg.ssh.username}@${cfg.ssh.host}:${cfg.remoteDir}`);
|
|
2200
|
+
log15.warn(" \u2514\u2500 All containers, Postgres data, Redis data, config files");
|
|
2201
|
+
log15.warn(` \u2022 Local: ${dir}/`);
|
|
2202
|
+
log15.warn(" \u2514\u2500 docker-compose.yml, .env.web, .env.engine, Caddyfile, lead-routing.json");
|
|
2215
2203
|
const confirmed = await confirm3({
|
|
2216
|
-
message:
|
|
2204
|
+
message: chalk7.bold("Are you sure you want to uninstall? This cannot be undone."),
|
|
2217
2205
|
initialValue: false
|
|
2218
2206
|
});
|
|
2219
2207
|
if (isCancel5(confirmed) || !confirmed) {
|
|
2220
|
-
|
|
2208
|
+
log15.info("Uninstall cancelled.");
|
|
2221
2209
|
process.exit(0);
|
|
2222
2210
|
}
|
|
2223
2211
|
const ssh = new SshConnection();
|
|
@@ -2238,47 +2226,47 @@ async function runUninstall() {
|
|
|
2238
2226
|
password: sshPassword,
|
|
2239
2227
|
remoteDir: cfg.remoteDir
|
|
2240
2228
|
});
|
|
2241
|
-
|
|
2229
|
+
log15.success(`Connected to ${cfg.ssh.host}`);
|
|
2242
2230
|
} catch (err) {
|
|
2243
|
-
|
|
2231
|
+
log15.error(`SSH connection failed: ${String(err)}`);
|
|
2244
2232
|
process.exit(1);
|
|
2245
2233
|
}
|
|
2246
2234
|
try {
|
|
2247
2235
|
const remoteDir = await ssh.resolveHome(cfg.remoteDir);
|
|
2248
|
-
|
|
2236
|
+
log15.step("Stopping containers and removing volumes");
|
|
2249
2237
|
const { code } = await ssh.execSilent("docker compose down -v", remoteDir);
|
|
2250
2238
|
if (code !== 0) {
|
|
2251
|
-
|
|
2239
|
+
log15.warn("docker compose down reported an error \u2014 directory may already be partially removed");
|
|
2252
2240
|
} else {
|
|
2253
|
-
|
|
2241
|
+
log15.success("Containers and volumes removed");
|
|
2254
2242
|
}
|
|
2255
|
-
|
|
2243
|
+
log15.step(`Removing remote directory ${remoteDir}`);
|
|
2256
2244
|
await ssh.exec(`rm -rf ${remoteDir}`);
|
|
2257
|
-
|
|
2245
|
+
log15.success("Remote directory removed");
|
|
2258
2246
|
} catch (err) {
|
|
2259
2247
|
const message = err instanceof Error ? err.message : String(err);
|
|
2260
|
-
|
|
2248
|
+
log15.error(`Remote cleanup failed: ${message}`);
|
|
2261
2249
|
process.exit(1);
|
|
2262
2250
|
} finally {
|
|
2263
2251
|
await ssh.disconnect();
|
|
2264
2252
|
}
|
|
2265
|
-
|
|
2253
|
+
log15.step("Removing local config directory");
|
|
2266
2254
|
if (existsSync6(dir)) {
|
|
2267
2255
|
rmSync2(dir, { recursive: true, force: true });
|
|
2268
|
-
|
|
2256
|
+
log15.success(`Removed ${dir}`);
|
|
2269
2257
|
}
|
|
2270
2258
|
outro6(
|
|
2271
|
-
|
|
2259
|
+
chalk7.green("\u2714 Uninstall complete.") + `
|
|
2272
2260
|
|
|
2273
|
-
Run ${
|
|
2261
|
+
Run ${chalk7.cyan("npx @lead-routing/cli init")} to start fresh.`
|
|
2274
2262
|
);
|
|
2275
2263
|
}
|
|
2276
2264
|
|
|
2277
2265
|
// src/commands/signup.ts
|
|
2278
2266
|
import * as p from "@clack/prompts";
|
|
2279
|
-
import
|
|
2267
|
+
import chalk8 from "chalk";
|
|
2280
2268
|
async function runSignup() {
|
|
2281
|
-
p.intro(
|
|
2269
|
+
p.intro(chalk8.bgBlue.white(" lead-routing signup "));
|
|
2282
2270
|
const firstName = await p.text({ message: "First name", placeholder: "John", validate: (v) => v.trim() ? void 0 : "Required" });
|
|
2283
2271
|
if (p.isCancel(firstName)) process.exit(0);
|
|
2284
2272
|
const lastName = await p.text({ message: "Last name", placeholder: "Smith", validate: (v) => v.trim() ? void 0 : "Required" });
|
|
@@ -2287,10 +2275,10 @@ async function runSignup() {
|
|
|
2287
2275
|
if (p.isCancel(email)) process.exit(0);
|
|
2288
2276
|
const password5 = await p.password({ message: "Password (min 8 characters)", validate: (v) => v.length >= 8 ? void 0 : "Must be at least 8 characters" });
|
|
2289
2277
|
if (p.isCancel(password5)) process.exit(0);
|
|
2290
|
-
const
|
|
2291
|
-
if (p.isCancel(
|
|
2292
|
-
const
|
|
2293
|
-
|
|
2278
|
+
const confirm6 = await p.password({ message: "Confirm password", validate: (v) => v === password5 ? void 0 : "Passwords do not match" });
|
|
2279
|
+
if (p.isCancel(confirm6)) process.exit(0);
|
|
2280
|
+
const spinner9 = p.spinner();
|
|
2281
|
+
spinner9.start("Creating account...");
|
|
2294
2282
|
try {
|
|
2295
2283
|
const message = await apiSignup({
|
|
2296
2284
|
firstName: firstName.trim(),
|
|
@@ -2298,20 +2286,20 @@ async function runSignup() {
|
|
|
2298
2286
|
email: email.trim(),
|
|
2299
2287
|
password: password5
|
|
2300
2288
|
});
|
|
2301
|
-
|
|
2289
|
+
spinner9.stop("Account created!");
|
|
2302
2290
|
p.note(
|
|
2303
2291
|
`Check your email (${email.trim()}) for a verification link.
|
|
2304
2292
|
|
|
2305
2293
|
After verifying, run:
|
|
2306
2294
|
|
|
2307
|
-
${
|
|
2295
|
+
${chalk8.cyan("lead-routing login")}`,
|
|
2308
2296
|
"Next Steps"
|
|
2309
2297
|
);
|
|
2310
2298
|
} catch (err) {
|
|
2311
2299
|
const msg = err instanceof Error ? err.message : "Signup failed";
|
|
2312
|
-
|
|
2300
|
+
spinner9.stop(msg);
|
|
2313
2301
|
if (msg.includes("already")) {
|
|
2314
|
-
p.log.info(`Try ${
|
|
2302
|
+
p.log.info(`Try ${chalk8.cyan("lead-routing login")} instead.`);
|
|
2315
2303
|
}
|
|
2316
2304
|
process.exit(1);
|
|
2317
2305
|
}
|
|
@@ -2320,19 +2308,19 @@ After verifying, run:
|
|
|
2320
2308
|
|
|
2321
2309
|
// src/commands/login.ts
|
|
2322
2310
|
import * as p2 from "@clack/prompts";
|
|
2323
|
-
import
|
|
2311
|
+
import chalk9 from "chalk";
|
|
2324
2312
|
async function runLogin() {
|
|
2325
|
-
p2.intro(
|
|
2313
|
+
p2.intro(chalk9.bgBlue.white(" lead-routing login "));
|
|
2326
2314
|
const email = await p2.text({ message: "Email", placeholder: "john@acme.com" });
|
|
2327
2315
|
if (p2.isCancel(email)) process.exit(0);
|
|
2328
2316
|
const password5 = await p2.password({ message: "Password" });
|
|
2329
2317
|
if (p2.isCancel(password5)) process.exit(0);
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2318
|
+
const spinner9 = p2.spinner();
|
|
2319
|
+
spinner9.start("Authenticating...");
|
|
2332
2320
|
try {
|
|
2333
2321
|
const { token, customer } = await apiLogin(email.trim(), password5);
|
|
2334
2322
|
if (!customer.emailVerified) {
|
|
2335
|
-
|
|
2323
|
+
spinner9.stop("Email not verified");
|
|
2336
2324
|
p2.log.warn(`Your email (${customer.email}) hasn't been verified yet.`);
|
|
2337
2325
|
const resend = await p2.confirm({ message: "Resend verification email?" });
|
|
2338
2326
|
if (resend && !p2.isCancel(resend)) {
|
|
@@ -2345,19 +2333,421 @@ async function runLogin() {
|
|
|
2345
2333
|
}
|
|
2346
2334
|
p2.note(`Verify your email, then run:
|
|
2347
2335
|
|
|
2348
|
-
${
|
|
2336
|
+
${chalk9.cyan("lead-routing login")}`, "Next Steps");
|
|
2349
2337
|
process.exit(1);
|
|
2350
2338
|
}
|
|
2351
2339
|
saveCredentials({ token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2352
|
-
|
|
2340
|
+
spinner9.stop(`Logged in as ${customer.firstName} ${customer.lastName} \u2014 ${formatTierBadge(customer.tier)}`);
|
|
2353
2341
|
p2.note(`Credentials saved to ~/.lead-routing/credentials.json`, "Saved");
|
|
2354
2342
|
} catch (err) {
|
|
2355
|
-
|
|
2343
|
+
spinner9.stop(err instanceof Error ? err.message : "Login failed");
|
|
2356
2344
|
process.exit(1);
|
|
2357
2345
|
}
|
|
2358
2346
|
p2.outro("Done!");
|
|
2359
2347
|
}
|
|
2360
2348
|
|
|
2349
|
+
// src/commands/dev.ts
|
|
2350
|
+
import {
|
|
2351
|
+
intro as intro9,
|
|
2352
|
+
outro as outro9,
|
|
2353
|
+
spinner as spinner8,
|
|
2354
|
+
text as text6,
|
|
2355
|
+
password as promptPassword4,
|
|
2356
|
+
note as note7,
|
|
2357
|
+
log as log18,
|
|
2358
|
+
isCancel as isCancel8,
|
|
2359
|
+
cancel as cancel5,
|
|
2360
|
+
confirm as confirm5
|
|
2361
|
+
} from "@clack/prompts";
|
|
2362
|
+
import { execa as execa4 } from "execa";
|
|
2363
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync8, readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
|
|
2364
|
+
import { join as join10 } from "path";
|
|
2365
|
+
function getDevDir() {
|
|
2366
|
+
return join10(process.cwd(), "lead-routing-dev");
|
|
2367
|
+
}
|
|
2368
|
+
function renderDevDockerCompose(dbPassword, redisPassword) {
|
|
2369
|
+
return `# Lead Routing \u2014 Local Dev Docker Compose
|
|
2370
|
+
# Generated by lead-routing dev \u2014 do not edit manually.
|
|
2371
|
+
|
|
2372
|
+
services:
|
|
2373
|
+
|
|
2374
|
+
postgres:
|
|
2375
|
+
image: postgres:16-alpine
|
|
2376
|
+
restart: unless-stopped
|
|
2377
|
+
ports:
|
|
2378
|
+
- "127.0.0.1:5432:5432"
|
|
2379
|
+
environment:
|
|
2380
|
+
POSTGRES_DB: leadrouting
|
|
2381
|
+
POSTGRES_USER: leadrouting
|
|
2382
|
+
POSTGRES_PASSWORD: ${dbPassword}
|
|
2383
|
+
volumes:
|
|
2384
|
+
- postgres_data:/var/lib/postgresql/data
|
|
2385
|
+
healthcheck:
|
|
2386
|
+
test: ["CMD-SHELL", "pg_isready -U leadrouting"]
|
|
2387
|
+
interval: 5s
|
|
2388
|
+
timeout: 5s
|
|
2389
|
+
retries: 10
|
|
2390
|
+
|
|
2391
|
+
redis:
|
|
2392
|
+
image: redis:7-alpine
|
|
2393
|
+
restart: unless-stopped
|
|
2394
|
+
command: redis-server --requirepass ${redisPassword}
|
|
2395
|
+
volumes:
|
|
2396
|
+
- redis_data:/data
|
|
2397
|
+
healthcheck:
|
|
2398
|
+
test: ["CMD-SHELL", "redis-cli -a ${redisPassword} ping | grep PONG"]
|
|
2399
|
+
interval: 5s
|
|
2400
|
+
timeout: 3s
|
|
2401
|
+
retries: 10
|
|
2402
|
+
|
|
2403
|
+
web:
|
|
2404
|
+
image: ghcr.io/atgatzby/lead-routing-web:latest
|
|
2405
|
+
restart: unless-stopped
|
|
2406
|
+
ports:
|
|
2407
|
+
- "127.0.0.1:3000:3000"
|
|
2408
|
+
env_file: .env.web
|
|
2409
|
+
healthcheck:
|
|
2410
|
+
test: ["CMD-SHELL", "wget -qO- http://$(hostname -i):3000/api/health || exit 1"]
|
|
2411
|
+
interval: 10s
|
|
2412
|
+
timeout: 5s
|
|
2413
|
+
retries: 6
|
|
2414
|
+
depends_on:
|
|
2415
|
+
postgres:
|
|
2416
|
+
condition: service_healthy
|
|
2417
|
+
redis:
|
|
2418
|
+
condition: service_healthy
|
|
2419
|
+
|
|
2420
|
+
engine:
|
|
2421
|
+
image: ghcr.io/atgatzby/lead-routing-engine:latest
|
|
2422
|
+
restart: unless-stopped
|
|
2423
|
+
ports:
|
|
2424
|
+
- "127.0.0.1:3001:3001"
|
|
2425
|
+
env_file: .env.engine
|
|
2426
|
+
healthcheck:
|
|
2427
|
+
test: ["CMD-SHELL", "wget -qO- http://$(hostname -i):3001/health || exit 1"]
|
|
2428
|
+
interval: 10s
|
|
2429
|
+
timeout: 5s
|
|
2430
|
+
retries: 6
|
|
2431
|
+
depends_on:
|
|
2432
|
+
postgres:
|
|
2433
|
+
condition: service_healthy
|
|
2434
|
+
redis:
|
|
2435
|
+
condition: service_healthy
|
|
2436
|
+
|
|
2437
|
+
volumes:
|
|
2438
|
+
postgres_data:
|
|
2439
|
+
redis_data:
|
|
2440
|
+
`;
|
|
2441
|
+
}
|
|
2442
|
+
function bail3(value) {
|
|
2443
|
+
if (isCancel8(value)) {
|
|
2444
|
+
cancel5("Setup cancelled.");
|
|
2445
|
+
process.exit(0);
|
|
2446
|
+
}
|
|
2447
|
+
throw new Error("Unexpected cancel");
|
|
2448
|
+
}
|
|
2449
|
+
async function pollHealth2(url, attempts = 30, intervalMs = 3e3) {
|
|
2450
|
+
for (let i = 0; i < attempts; i++) {
|
|
2451
|
+
try {
|
|
2452
|
+
const res = await fetch(url);
|
|
2453
|
+
if (res.ok) return true;
|
|
2454
|
+
} catch {
|
|
2455
|
+
}
|
|
2456
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2457
|
+
}
|
|
2458
|
+
return false;
|
|
2459
|
+
}
|
|
2460
|
+
async function runDev(opts = {}) {
|
|
2461
|
+
if (opts.sfdc) {
|
|
2462
|
+
const devDir2 = getDevDir();
|
|
2463
|
+
const configFile2 = join10(devDir2, "dev.json");
|
|
2464
|
+
if (!existsSync7(configFile2)) {
|
|
2465
|
+
console.error("No dev environment found. Run `lead-routing dev` first.");
|
|
2466
|
+
process.exit(1);
|
|
2467
|
+
}
|
|
2468
|
+
intro9("Lead Routing \u2014 Update Salesforce Credentials");
|
|
2469
|
+
note7(
|
|
2470
|
+
"You need a Salesforce Connected App with callback URL:\n http://localhost:3000/api/auth/sfdc/callback",
|
|
2471
|
+
"Before you begin"
|
|
2472
|
+
);
|
|
2473
|
+
const clientId = await text6({
|
|
2474
|
+
message: "Consumer Key (Client ID)",
|
|
2475
|
+
validate: (v) => !v ? "Required" : void 0
|
|
2476
|
+
});
|
|
2477
|
+
if (isCancel8(clientId)) {
|
|
2478
|
+
cancel5("Cancelled.");
|
|
2479
|
+
process.exit(0);
|
|
2480
|
+
}
|
|
2481
|
+
const clientSecret = await promptPassword4({ message: "Consumer Secret" });
|
|
2482
|
+
if (isCancel8(clientSecret)) {
|
|
2483
|
+
cancel5("Cancelled.");
|
|
2484
|
+
process.exit(0);
|
|
2485
|
+
}
|
|
2486
|
+
const loginUrl = await text6({
|
|
2487
|
+
message: "Login URL",
|
|
2488
|
+
initialValue: "https://login.salesforce.com",
|
|
2489
|
+
validate: (v) => !v ? "Required" : void 0
|
|
2490
|
+
});
|
|
2491
|
+
if (isCancel8(loginUrl)) {
|
|
2492
|
+
cancel5("Cancelled.");
|
|
2493
|
+
process.exit(0);
|
|
2494
|
+
}
|
|
2495
|
+
const envPath = join10(devDir2, ".env.web");
|
|
2496
|
+
const lines = readFileSync8(envPath, "utf8").split("\n");
|
|
2497
|
+
const updates = {
|
|
2498
|
+
SFDC_CLIENT_ID: clientId.trim(),
|
|
2499
|
+
SFDC_CLIENT_SECRET: clientSecret.trim(),
|
|
2500
|
+
SFDC_LOGIN_URL: loginUrl.trim(),
|
|
2501
|
+
SFDC_REDIRECT_URI: "http://localhost:3000/api/auth/sfdc/callback"
|
|
2502
|
+
};
|
|
2503
|
+
const updated = /* @__PURE__ */ new Set();
|
|
2504
|
+
const result = lines.map((line) => {
|
|
2505
|
+
const eq = line.indexOf("=");
|
|
2506
|
+
if (eq === -1) return line;
|
|
2507
|
+
const key = line.slice(0, eq);
|
|
2508
|
+
if (key in updates) {
|
|
2509
|
+
updated.add(key);
|
|
2510
|
+
return `${key}=${updates[key]}`;
|
|
2511
|
+
}
|
|
2512
|
+
return line;
|
|
2513
|
+
});
|
|
2514
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
2515
|
+
if (!updated.has(k)) result.push(`${k}=${v}`);
|
|
2516
|
+
}
|
|
2517
|
+
writeFileSync8(envPath, result.join("\n"), "utf8");
|
|
2518
|
+
const cfg2 = JSON.parse(readFileSync8(configFile2, "utf8"));
|
|
2519
|
+
cfg2.sfdcClientId = clientId.trim();
|
|
2520
|
+
cfg2.sfdcClientSecret = clientSecret.trim();
|
|
2521
|
+
cfg2.sfdcLoginUrl = loginUrl.trim();
|
|
2522
|
+
writeFileSync8(configFile2, JSON.stringify(cfg2, null, 2), "utf8");
|
|
2523
|
+
log18.success("Credentials saved");
|
|
2524
|
+
const s = spinner8();
|
|
2525
|
+
s.start("Restarting web container...");
|
|
2526
|
+
await execa4("docker", ["compose", "restart", "web"], { cwd: devDir2, stdio: "ignore" });
|
|
2527
|
+
s.stop("Web container restarted");
|
|
2528
|
+
outro9("Done! Connect Salesforce at http://localhost:3000/integrations/salesforce");
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
if (opts.logs !== void 0) {
|
|
2532
|
+
const service = opts.logs || "engine";
|
|
2533
|
+
const devDir2 = getDevDir();
|
|
2534
|
+
if (!existsSync7(join10(devDir2, "docker-compose.yml"))) {
|
|
2535
|
+
console.error("No dev environment found. Run `lead-routing dev` first.");
|
|
2536
|
+
process.exit(1);
|
|
2537
|
+
}
|
|
2538
|
+
const valid = ["web", "engine", "postgres", "redis"];
|
|
2539
|
+
if (!valid.includes(service)) {
|
|
2540
|
+
console.error(`Unknown service "${service}". Valid: ${valid.join(", ")}`);
|
|
2541
|
+
process.exit(1);
|
|
2542
|
+
}
|
|
2543
|
+
await execa4("docker", ["compose", "logs", "-f", "--tail=100", service], {
|
|
2544
|
+
cwd: devDir2,
|
|
2545
|
+
stdio: "inherit"
|
|
2546
|
+
});
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
intro9("Lead Routing \u2014 Local Dev");
|
|
2550
|
+
try {
|
|
2551
|
+
await execa4("docker", ["info"], { stdio: "ignore" });
|
|
2552
|
+
} catch {
|
|
2553
|
+
log18.error("Docker is not running. Please start Docker Desktop and try again.");
|
|
2554
|
+
process.exit(1);
|
|
2555
|
+
}
|
|
2556
|
+
const devDir = getDevDir();
|
|
2557
|
+
const configFile = join10(devDir, "dev.json");
|
|
2558
|
+
const isFirstRun = !existsSync7(configFile);
|
|
2559
|
+
let cfg;
|
|
2560
|
+
let adminPassword = "";
|
|
2561
|
+
if (!isFirstRun && !opts.reset) {
|
|
2562
|
+
cfg = JSON.parse(readFileSync8(configFile, "utf8"));
|
|
2563
|
+
log18.info("Existing dev environment found \u2014 starting services...");
|
|
2564
|
+
} else {
|
|
2565
|
+
if (opts.reset && !isFirstRun) {
|
|
2566
|
+
const ok = await confirm5({
|
|
2567
|
+
message: "This will wipe all local dev data (database, redis). Continue?"
|
|
2568
|
+
});
|
|
2569
|
+
if (isCancel8(ok) || !ok) {
|
|
2570
|
+
cancel5("Reset cancelled.");
|
|
2571
|
+
process.exit(0);
|
|
2572
|
+
}
|
|
2573
|
+
const s = spinner8();
|
|
2574
|
+
s.start("Stopping and removing volumes...");
|
|
2575
|
+
try {
|
|
2576
|
+
await execa4("docker", ["compose", "down", "-v"], { cwd: devDir, stdio: "ignore" });
|
|
2577
|
+
} catch {
|
|
2578
|
+
}
|
|
2579
|
+
s.stop("Volumes removed");
|
|
2580
|
+
}
|
|
2581
|
+
note7(
|
|
2582
|
+
"Sets up a full Lead Routing stack locally using Docker.\nNo SSH, no VPS, no manual steps.",
|
|
2583
|
+
"First-time setup"
|
|
2584
|
+
);
|
|
2585
|
+
const adminEmail = await text6({
|
|
2586
|
+
message: "Admin email",
|
|
2587
|
+
placeholder: "you@company.com",
|
|
2588
|
+
validate: (v) => {
|
|
2589
|
+
if (!v) return "Required";
|
|
2590
|
+
if (!v.includes("@")) return "Must be a valid email";
|
|
2591
|
+
}
|
|
2592
|
+
});
|
|
2593
|
+
if (isCancel8(adminEmail)) bail3(adminEmail);
|
|
2594
|
+
const pw = await promptPassword4({
|
|
2595
|
+
message: "Admin password (min 8 characters)",
|
|
2596
|
+
validate: (v) => {
|
|
2597
|
+
if (!v) return "Required";
|
|
2598
|
+
if (v.length < 8) return "Must be at least 8 characters";
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
if (isCancel8(pw)) bail3(pw);
|
|
2602
|
+
adminPassword = pw;
|
|
2603
|
+
note7(
|
|
2604
|
+
"Optional \u2014 needed to connect Salesforce.\nPress Enter to skip; add credentials later via `lead-routing config sfdc`.",
|
|
2605
|
+
"Salesforce Connected App"
|
|
2606
|
+
);
|
|
2607
|
+
const sfdcClientId = await text6({
|
|
2608
|
+
message: "Consumer Key (Client ID)",
|
|
2609
|
+
placeholder: "Leave blank to skip"
|
|
2610
|
+
});
|
|
2611
|
+
if (isCancel8(sfdcClientId)) bail3(sfdcClientId);
|
|
2612
|
+
let sfdcClientSecret = "";
|
|
2613
|
+
if (sfdcClientId?.trim()) {
|
|
2614
|
+
const s = await promptPassword4({ message: "Consumer Secret" });
|
|
2615
|
+
if (isCancel8(s)) bail3(s);
|
|
2616
|
+
sfdcClientSecret = s.trim();
|
|
2617
|
+
}
|
|
2618
|
+
const tunnelUrl = await text6({
|
|
2619
|
+
message: "Engine tunnel URL (for Salesforce webhooks)",
|
|
2620
|
+
placeholder: "Leave blank \u2014 use `ssh -R 80:localhost:3001 localhost.run` to get one"
|
|
2621
|
+
});
|
|
2622
|
+
if (isCancel8(tunnelUrl)) bail3(tunnelUrl);
|
|
2623
|
+
cfg = {
|
|
2624
|
+
adminEmail: adminEmail.trim(),
|
|
2625
|
+
dbPassword: generateSecret(16),
|
|
2626
|
+
redisPassword: generateSecret(16),
|
|
2627
|
+
sessionSecret: generateSecret(32),
|
|
2628
|
+
engineWebhookSecret: generateSecret(32),
|
|
2629
|
+
internalApiKey: generateSecret(32),
|
|
2630
|
+
sfdcClientId: sfdcClientId?.trim() ?? "",
|
|
2631
|
+
sfdcClientSecret,
|
|
2632
|
+
sfdcLoginUrl: "https://login.salesforce.com",
|
|
2633
|
+
engineTunnelUrl: tunnelUrl?.trim() ?? ""
|
|
2634
|
+
};
|
|
2635
|
+
mkdirSync3(devDir, { recursive: true });
|
|
2636
|
+
const dbUrl = `postgresql://leadrouting:${cfg.dbPassword}@postgres:5432/leadrouting`;
|
|
2637
|
+
const redisUrl = `redis://:${cfg.redisPassword}@redis:6379`;
|
|
2638
|
+
const publicEngineUrl = cfg.engineTunnelUrl || "http://localhost:3001";
|
|
2639
|
+
writeFileSync8(
|
|
2640
|
+
join10(devDir, "docker-compose.yml"),
|
|
2641
|
+
renderDevDockerCompose(cfg.dbPassword, cfg.redisPassword),
|
|
2642
|
+
"utf8"
|
|
2643
|
+
);
|
|
2644
|
+
const envWebBase = renderEnvWeb({
|
|
2645
|
+
appUrl: "http://localhost:3000",
|
|
2646
|
+
engineUrl: "http://engine:3001",
|
|
2647
|
+
publicEngineUrl,
|
|
2648
|
+
databaseUrl: dbUrl,
|
|
2649
|
+
redisUrl,
|
|
2650
|
+
sessionSecret: cfg.sessionSecret,
|
|
2651
|
+
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
2652
|
+
adminEmail: cfg.adminEmail,
|
|
2653
|
+
adminPassword,
|
|
2654
|
+
internalApiKey: cfg.internalApiKey,
|
|
2655
|
+
licenseKey: void 0,
|
|
2656
|
+
licenseTier: "pro"
|
|
2657
|
+
});
|
|
2658
|
+
writeFileSync8(
|
|
2659
|
+
join10(devDir, ".env.web"),
|
|
2660
|
+
envWebBase + `
|
|
2661
|
+
# Salesforce Connected App
|
|
2662
|
+
SFDC_CLIENT_ID=${cfg.sfdcClientId}
|
|
2663
|
+
SFDC_CLIENT_SECRET=${cfg.sfdcClientSecret}
|
|
2664
|
+
SFDC_REDIRECT_URI=http://localhost:3000/api/auth/sfdc/callback
|
|
2665
|
+
SFDC_LOGIN_URL=${cfg.sfdcLoginUrl}
|
|
2666
|
+
`,
|
|
2667
|
+
"utf8"
|
|
2668
|
+
);
|
|
2669
|
+
writeFileSync8(
|
|
2670
|
+
join10(devDir, ".env.engine"),
|
|
2671
|
+
renderEnvEngine({
|
|
2672
|
+
databaseUrl: dbUrl,
|
|
2673
|
+
redisUrl,
|
|
2674
|
+
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
2675
|
+
internalApiKey: cfg.internalApiKey,
|
|
2676
|
+
licenseKey: void 0,
|
|
2677
|
+
licenseTier: "pro"
|
|
2678
|
+
}),
|
|
2679
|
+
"utf8"
|
|
2680
|
+
);
|
|
2681
|
+
writeFileSync8(configFile, JSON.stringify(cfg, null, 2), "utf8");
|
|
2682
|
+
writeConfig(devDir, {
|
|
2683
|
+
appUrl: "http://localhost:3000",
|
|
2684
|
+
engineUrl: publicEngineUrl,
|
|
2685
|
+
installDir: devDir,
|
|
2686
|
+
remoteDir: devDir,
|
|
2687
|
+
ssh: { host: "localhost", port: 22, username: "local" },
|
|
2688
|
+
dockerManaged: { db: true, redis: true },
|
|
2689
|
+
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
2690
|
+
licenseTier: "pro",
|
|
2691
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2692
|
+
version: "0.0.0"
|
|
2693
|
+
});
|
|
2694
|
+
log18.success("Generated config files in ./lead-routing-dev/");
|
|
2695
|
+
}
|
|
2696
|
+
const pullS = spinner8();
|
|
2697
|
+
pullS.start("Pulling latest images...");
|
|
2698
|
+
try {
|
|
2699
|
+
await execa4("docker", ["compose", "pull"], { cwd: devDir, stdio: "ignore" });
|
|
2700
|
+
pullS.stop("Images up to date");
|
|
2701
|
+
} catch {
|
|
2702
|
+
pullS.stop("Using cached images");
|
|
2703
|
+
}
|
|
2704
|
+
const startS = spinner8();
|
|
2705
|
+
startS.start("Starting services...");
|
|
2706
|
+
try {
|
|
2707
|
+
await execa4("docker", ["compose", "up", "-d", "--remove-orphans"], {
|
|
2708
|
+
cwd: devDir,
|
|
2709
|
+
stdio: "ignore"
|
|
2710
|
+
});
|
|
2711
|
+
startS.stop("Services started");
|
|
2712
|
+
} catch (err) {
|
|
2713
|
+
startS.stop("Failed to start services");
|
|
2714
|
+
log18.error(err instanceof Error ? err.message : String(err));
|
|
2715
|
+
process.exit(1);
|
|
2716
|
+
}
|
|
2717
|
+
const healthS = spinner8();
|
|
2718
|
+
healthS.start("Waiting for web app...");
|
|
2719
|
+
const webOk = await pollHealth2("http://localhost:3000/api/health");
|
|
2720
|
+
if (!webOk) {
|
|
2721
|
+
healthS.stop("Web app did not start in time");
|
|
2722
|
+
log18.warn("Run `lead-routing dev logs web` to diagnose.");
|
|
2723
|
+
process.exit(1);
|
|
2724
|
+
}
|
|
2725
|
+
healthS.stop("Web app ready");
|
|
2726
|
+
const engineS = spinner8();
|
|
2727
|
+
engineS.start("Waiting for engine...");
|
|
2728
|
+
const engineOk = await pollHealth2("http://localhost:3001/health");
|
|
2729
|
+
engineS.stop(engineOk ? "Engine ready" : "Engine slow to start (check: lead-routing dev logs engine)");
|
|
2730
|
+
note7(
|
|
2731
|
+
` Web: http://localhost:3000
|
|
2732
|
+
Engine: http://localhost:3001
|
|
2733
|
+
|
|
2734
|
+
Login: ${cfg.adminEmail}`,
|
|
2735
|
+
"\u2713 Lead Routing running locally"
|
|
2736
|
+
);
|
|
2737
|
+
if (cfg.sfdcClientId) {
|
|
2738
|
+
if (cfg.engineTunnelUrl) {
|
|
2739
|
+
log18.info("Next: run `lead-routing sfdc deploy` to deploy the Salesforce package (same as prod)");
|
|
2740
|
+
} else {
|
|
2741
|
+
log18.warn(
|
|
2742
|
+
"No engine tunnel URL set \u2014 Salesforce cannot reach localhost:3001.\nTo get a tunnel: ssh -R 80:localhost:3001 localhost.run\nThen re-run: lead-routing dev --reset (to update the tunnel URL)\nThen deploy: lead-routing sfdc deploy"
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2745
|
+
} else {
|
|
2746
|
+
log18.info("No Salesforce creds set. Add via: lead-routing dev --sfdc");
|
|
2747
|
+
}
|
|
2748
|
+
outro9("Run `lead-routing dev logs` to stream logs \u2022 `lead-routing dev --reset` to wipe and restart");
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2361
2751
|
// src/index.ts
|
|
2362
2752
|
var program = new Command();
|
|
2363
2753
|
program.name("lead-routing").description("Self-hosted Lead Routing \u2014 scaffold, deploy, and manage your installation").version("0.1.13");
|
|
@@ -2381,6 +2771,7 @@ config.command("sfdc").description("Update Salesforce Connected App credentials
|
|
|
2381
2771
|
var sfdc = program.command("sfdc").description("Manage the Salesforce package for this installation");
|
|
2382
2772
|
sfdc.command("deploy").description("Deploy (or redeploy) the Lead Router Salesforce package to your Salesforce org").action(runSfdcDeploy);
|
|
2383
2773
|
program.command("uninstall").description("Stop all containers, remove all data, and delete the remote installation").action(runUninstall);
|
|
2774
|
+
program.command("dev").description("Start a local dev environment (Docker-based, no SSH required)").option("--reset", "Wipe all local data and start fresh").option("--logs [service]", "Stream logs from a service (web, engine, postgres, redis)").option("--sfdc", "Update Salesforce Connected App credentials").action((opts) => runDev(opts));
|
|
2384
2775
|
program.command("signup").description("Create a new Lead Routing account").action(runSignup);
|
|
2385
2776
|
program.command("login").description("Log in to your Lead Routing account").action(runLogin);
|
|
2386
2777
|
program.parseAsync(process.argv).catch((err) => {
|