@lead-routing/cli 0.6.1 → 0.6.3
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
|
@@ -9,7 +9,7 @@ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "
|
|
|
9
9
|
import { exec } from "child_process";
|
|
10
10
|
import { platform } from "os";
|
|
11
11
|
import { join as join5 } from "path";
|
|
12
|
-
import { intro, outro, note as note3, log as log7, confirm, cancel as cancel3, isCancel as isCancel3, password as promptPassword } from "@clack/prompts";
|
|
12
|
+
import { intro, outro, note as note3, log as log7, confirm, cancel as cancel3, isCancel as isCancel3, password as promptPassword, select, text as text3 } from "@clack/prompts";
|
|
13
13
|
import chalk2 from "chalk";
|
|
14
14
|
|
|
15
15
|
// src/steps/prerequisites.ts
|
|
@@ -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;
|
|
@@ -1206,16 +1208,108 @@ async function runInit(options = {}) {
|
|
|
1206
1208
|
let auth;
|
|
1207
1209
|
try {
|
|
1208
1210
|
auth = await requireAuth();
|
|
1209
|
-
} catch
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
)
|
|
1218
|
-
|
|
1211
|
+
} catch {
|
|
1212
|
+
const action = await select({
|
|
1213
|
+
message: "You need an account to continue. What would you like to do?",
|
|
1214
|
+
options: [
|
|
1215
|
+
{ value: "signup", label: "Create a new account" },
|
|
1216
|
+
{ value: "login", label: "Log in to existing account" }
|
|
1217
|
+
]
|
|
1218
|
+
});
|
|
1219
|
+
if (isCancel3(action)) {
|
|
1220
|
+
cancel3("Setup cancelled.");
|
|
1221
|
+
process.exit(0);
|
|
1222
|
+
}
|
|
1223
|
+
if (action === "signup") {
|
|
1224
|
+
const firstName = await text3({ message: "First name", placeholder: "John", validate: (v) => v.trim() ? void 0 : "Required" });
|
|
1225
|
+
if (isCancel3(firstName)) {
|
|
1226
|
+
cancel3("Setup cancelled.");
|
|
1227
|
+
process.exit(0);
|
|
1228
|
+
}
|
|
1229
|
+
const lastName = await text3({ message: "Last name", placeholder: "Smith", validate: (v) => v.trim() ? void 0 : "Required" });
|
|
1230
|
+
if (isCancel3(lastName)) {
|
|
1231
|
+
cancel3("Setup cancelled.");
|
|
1232
|
+
process.exit(0);
|
|
1233
|
+
}
|
|
1234
|
+
const signupEmail = await text3({ message: "Email", placeholder: "john@acme.com", validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()) ? void 0 : "Invalid email" });
|
|
1235
|
+
if (isCancel3(signupEmail)) {
|
|
1236
|
+
cancel3("Setup cancelled.");
|
|
1237
|
+
process.exit(0);
|
|
1238
|
+
}
|
|
1239
|
+
const signupPw = await promptPassword({ message: "Password (min 8 characters)", validate: (v) => v.length >= 8 ? void 0 : "Must be at least 8 characters" });
|
|
1240
|
+
if (isCancel3(signupPw)) {
|
|
1241
|
+
cancel3("Setup cancelled.");
|
|
1242
|
+
process.exit(0);
|
|
1243
|
+
}
|
|
1244
|
+
const confirmPw = await promptPassword({ message: "Confirm password", validate: (v) => v === signupPw ? void 0 : "Passwords do not match" });
|
|
1245
|
+
if (isCancel3(confirmPw)) {
|
|
1246
|
+
cancel3("Setup cancelled.");
|
|
1247
|
+
process.exit(0);
|
|
1248
|
+
}
|
|
1249
|
+
log7.step("Creating account...");
|
|
1250
|
+
try {
|
|
1251
|
+
await apiSignup({ firstName: firstName.trim(), lastName: lastName.trim(), email: signupEmail.trim(), password: signupPw });
|
|
1252
|
+
log7.success("Account created!");
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
log7.error(err instanceof Error ? err.message : "Signup failed");
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
note3(
|
|
1258
|
+
`Check your email (${signupEmail.trim()}) for a verification link.
|
|
1259
|
+
After verifying, press Enter to continue.`,
|
|
1260
|
+
"Verify Email"
|
|
1261
|
+
);
|
|
1262
|
+
await text3({ message: "Press Enter once you've verified your email...", defaultValue: "" });
|
|
1263
|
+
log7.step("Logging in...");
|
|
1264
|
+
try {
|
|
1265
|
+
const { token, customer } = await apiLogin(signupEmail.trim(), signupPw);
|
|
1266
|
+
if (!customer.emailVerified) {
|
|
1267
|
+
log7.warn("Email not verified yet.");
|
|
1268
|
+
const resend = await confirm({ message: "Resend verification email?" });
|
|
1269
|
+
if (resend && !isCancel3(resend)) {
|
|
1270
|
+
await apiResendVerification(token).catch(() => {
|
|
1271
|
+
});
|
|
1272
|
+
log7.info("Verification email sent. Verify and re-run `lead-routing init`.");
|
|
1273
|
+
}
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
saveCredentials({ token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1277
|
+
auth = { token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
log7.error(err instanceof Error ? err.message : "Login failed");
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
} else {
|
|
1283
|
+
const loginEmail = await text3({ message: "Email", placeholder: "john@acme.com" });
|
|
1284
|
+
if (isCancel3(loginEmail)) {
|
|
1285
|
+
cancel3("Setup cancelled.");
|
|
1286
|
+
process.exit(0);
|
|
1287
|
+
}
|
|
1288
|
+
const loginPw = await promptPassword({ message: "Password" });
|
|
1289
|
+
if (isCancel3(loginPw)) {
|
|
1290
|
+
cancel3("Setup cancelled.");
|
|
1291
|
+
process.exit(0);
|
|
1292
|
+
}
|
|
1293
|
+
log7.step("Authenticating...");
|
|
1294
|
+
try {
|
|
1295
|
+
const { token, customer } = await apiLogin(loginEmail.trim(), loginPw);
|
|
1296
|
+
if (!customer.emailVerified) {
|
|
1297
|
+
log7.warn("Email not verified yet.");
|
|
1298
|
+
const resend = await confirm({ message: "Resend verification email?" });
|
|
1299
|
+
if (resend && !isCancel3(resend)) {
|
|
1300
|
+
await apiResendVerification(token).catch(() => {
|
|
1301
|
+
});
|
|
1302
|
+
log7.info("Verification email sent. Verify and re-run `lead-routing init`.");
|
|
1303
|
+
}
|
|
1304
|
+
process.exit(1);
|
|
1305
|
+
}
|
|
1306
|
+
saveCredentials({ token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1307
|
+
auth = { token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
log7.error(err instanceof Error ? err.message : "Login failed");
|
|
1310
|
+
process.exit(1);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1219
1313
|
}
|
|
1220
1314
|
log7.success(`Logged in as ${auth.customer.firstName} ${auth.customer.lastName} \u2014 ${formatTierBadge(auth.customer.tier)}`);
|
|
1221
1315
|
try {
|
|
@@ -1589,8 +1683,10 @@ function runConfigShow() {
|
|
|
1589
1683
|
}
|
|
1590
1684
|
|
|
1591
1685
|
// src/commands/sfdc.ts
|
|
1592
|
-
import { intro as intro5, outro as outro5, text as
|
|
1593
|
-
import
|
|
1686
|
+
import { intro as intro5, outro as outro5, text as text4, note as note4, confirm as confirm2, log as log14, isCancel as isCancel4, cancel as cancel4 } from "@clack/prompts";
|
|
1687
|
+
import chalk6 from "chalk";
|
|
1688
|
+
import { exec as exec2 } from "child_process";
|
|
1689
|
+
import { platform as platform2 } from "os";
|
|
1594
1690
|
|
|
1595
1691
|
// src/steps/sfdc-deploy-inline.ts
|
|
1596
1692
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync5, cpSync, rmSync } from "fs";
|
|
@@ -1719,9 +1815,9 @@ Content-Type: application/zip\r
|
|
|
1719
1815
|
body
|
|
1720
1816
|
});
|
|
1721
1817
|
if (!res.ok) {
|
|
1722
|
-
const
|
|
1818
|
+
const text8 = await res.text();
|
|
1723
1819
|
throw new Error(
|
|
1724
|
-
`Metadata deploy request failed (${res.status}): ${
|
|
1820
|
+
`Metadata deploy request failed (${res.status}): ${text8}`
|
|
1725
1821
|
);
|
|
1726
1822
|
}
|
|
1727
1823
|
const result = await res.json();
|
|
@@ -1738,9 +1834,9 @@ Content-Type: application/zip\r
|
|
|
1738
1834
|
const url = `${this.baseUrl}/metadata/deployRequest/${deployId}?includeDetails=true`;
|
|
1739
1835
|
const res = await fetch(url, { headers: this.headers() });
|
|
1740
1836
|
if (!res.ok) {
|
|
1741
|
-
const
|
|
1837
|
+
const text8 = await res.text();
|
|
1742
1838
|
throw new Error(
|
|
1743
|
-
`Deploy status check failed (${res.status}): ${
|
|
1839
|
+
`Deploy status check failed (${res.status}): ${text8}`
|
|
1744
1840
|
);
|
|
1745
1841
|
}
|
|
1746
1842
|
const data = await res.json();
|
|
@@ -2090,50 +2186,12 @@ Ensure the app is running and the URL is correct.`
|
|
|
2090
2186
|
return { accessToken, instanceUrl };
|
|
2091
2187
|
}
|
|
2092
2188
|
|
|
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
2189
|
// src/commands/sfdc.ts
|
|
2190
|
+
var MANAGED_PACKAGE_INSTALL_URL2 = "https://login.salesforce.com/packaging/installPackage.apexp?p0=04tgL000000CTnp";
|
|
2191
|
+
function openBrowser2(url) {
|
|
2192
|
+
const cmd = platform2() === "darwin" ? "open" : "xdg-open";
|
|
2193
|
+
exec2(`${cmd} ${JSON.stringify(url)}`);
|
|
2194
|
+
}
|
|
2137
2195
|
async function runSfdcDeploy() {
|
|
2138
2196
|
intro5("Lead Routing \u2014 Deploy Salesforce Package");
|
|
2139
2197
|
let appUrl;
|
|
@@ -2143,23 +2201,46 @@ async function runSfdcDeploy() {
|
|
|
2143
2201
|
if (config2?.appUrl && config2?.engineUrl) {
|
|
2144
2202
|
appUrl = config2.appUrl;
|
|
2145
2203
|
engineUrl = config2.engineUrl;
|
|
2146
|
-
|
|
2204
|
+
log14.info(`Using config from ${dir}/lead-routing.json`);
|
|
2147
2205
|
} else {
|
|
2148
|
-
|
|
2149
|
-
const rawApp = await
|
|
2206
|
+
log14.warn("No lead-routing.json found \u2014 enter the URLs from your installation.");
|
|
2207
|
+
const rawApp = await text4({
|
|
2150
2208
|
message: "App URL (e.g. https://leads.acme.com)",
|
|
2151
2209
|
validate: (v) => !v ? "Required" : void 0
|
|
2152
2210
|
});
|
|
2153
2211
|
if (typeof rawApp === "symbol") process.exit(0);
|
|
2154
2212
|
appUrl = rawApp.trim();
|
|
2155
|
-
const rawEngine = await
|
|
2213
|
+
const rawEngine = await text4({
|
|
2156
2214
|
message: "Engine URL (e.g. https://engine.acme.com or https://acme.com:3001)",
|
|
2157
2215
|
validate: (v) => !v ? "Required" : void 0
|
|
2158
2216
|
});
|
|
2159
2217
|
if (typeof rawEngine === "symbol") process.exit(0);
|
|
2160
2218
|
engineUrl = rawEngine.trim();
|
|
2161
2219
|
}
|
|
2162
|
-
|
|
2220
|
+
note4(
|
|
2221
|
+
`The Lead Router managed package installs the required Connected App,
|
|
2222
|
+
triggers, and custom objects in your Salesforce org.
|
|
2223
|
+
|
|
2224
|
+
Install URL: ${chalk6.cyan(MANAGED_PACKAGE_INSTALL_URL2)}`,
|
|
2225
|
+
"Salesforce Package"
|
|
2226
|
+
);
|
|
2227
|
+
log14.info("Opening install URL in your browser...");
|
|
2228
|
+
openBrowser2(MANAGED_PACKAGE_INSTALL_URL2);
|
|
2229
|
+
log14.info(chalk6.dim("If the browser didn't open, visit the URL above manually."));
|
|
2230
|
+
const installed = await confirm2({
|
|
2231
|
+
message: 'Have you installed the package? (Click "Install for All Users" in Salesforce)',
|
|
2232
|
+
initialValue: false
|
|
2233
|
+
});
|
|
2234
|
+
if (isCancel4(installed)) {
|
|
2235
|
+
cancel4("Setup cancelled.");
|
|
2236
|
+
process.exit(0);
|
|
2237
|
+
}
|
|
2238
|
+
if (!installed) {
|
|
2239
|
+
log14.warn("You can install the package later \u2014 routing will not work until it is installed.");
|
|
2240
|
+
} else {
|
|
2241
|
+
log14.success("Salesforce package installed");
|
|
2242
|
+
}
|
|
2243
|
+
const alias = await text4({
|
|
2163
2244
|
message: "Salesforce org alias (used to log in)",
|
|
2164
2245
|
placeholder: "lead-routing",
|
|
2165
2246
|
initialValue: "lead-routing",
|
|
@@ -2175,49 +2256,48 @@ async function runSfdcDeploy() {
|
|
|
2175
2256
|
webhookSecret: config2?.engineWebhookSecret
|
|
2176
2257
|
});
|
|
2177
2258
|
} catch (err) {
|
|
2178
|
-
|
|
2259
|
+
log14.error(err instanceof Error ? err.message : String(err));
|
|
2179
2260
|
process.exit(1);
|
|
2180
2261
|
}
|
|
2181
|
-
await guideAppLauncherSetup(appUrl);
|
|
2182
2262
|
outro5(
|
|
2183
|
-
|
|
2263
|
+
chalk6.green("\u2714 Salesforce package deployed!") + `
|
|
2184
2264
|
|
|
2185
|
-
Your Lead Router dashboard: ${
|
|
2265
|
+
Your Lead Router dashboard: ${chalk6.cyan(appUrl)}`
|
|
2186
2266
|
);
|
|
2187
2267
|
}
|
|
2188
2268
|
|
|
2189
2269
|
// src/commands/uninstall.ts
|
|
2190
2270
|
import { rmSync as rmSync2, existsSync as existsSync6 } from "fs";
|
|
2191
|
-
import { intro as intro6, outro as outro6, log as
|
|
2192
|
-
import
|
|
2271
|
+
import { intro as intro6, outro as outro6, log as log15, confirm as confirm3, password as promptPassword3, isCancel as isCancel5 } from "@clack/prompts";
|
|
2272
|
+
import chalk7 from "chalk";
|
|
2193
2273
|
async function runUninstall() {
|
|
2194
2274
|
console.log();
|
|
2195
|
-
intro6(
|
|
2275
|
+
intro6(chalk7.bold.red("Lead Routing \u2014 Uninstall"));
|
|
2196
2276
|
const dir = findInstallDir();
|
|
2197
2277
|
if (!dir) {
|
|
2198
|
-
|
|
2278
|
+
log15.error(
|
|
2199
2279
|
"No lead-routing.json found. Run this command from your install directory."
|
|
2200
2280
|
);
|
|
2201
2281
|
process.exit(1);
|
|
2202
2282
|
}
|
|
2203
2283
|
const cfg = readConfig(dir);
|
|
2204
2284
|
if (!cfg.ssh) {
|
|
2205
|
-
|
|
2285
|
+
log15.error(
|
|
2206
2286
|
"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
2287
|
);
|
|
2208
2288
|
process.exit(1);
|
|
2209
2289
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2290
|
+
log15.warn(chalk7.red("This will permanently destroy:"));
|
|
2291
|
+
log15.warn(` \u2022 Remote: ${cfg.ssh.username}@${cfg.ssh.host}:${cfg.remoteDir}`);
|
|
2292
|
+
log15.warn(" \u2514\u2500 All containers, Postgres data, Redis data, config files");
|
|
2293
|
+
log15.warn(` \u2022 Local: ${dir}/`);
|
|
2294
|
+
log15.warn(" \u2514\u2500 docker-compose.yml, .env.web, .env.engine, Caddyfile, lead-routing.json");
|
|
2215
2295
|
const confirmed = await confirm3({
|
|
2216
|
-
message:
|
|
2296
|
+
message: chalk7.bold("Are you sure you want to uninstall? This cannot be undone."),
|
|
2217
2297
|
initialValue: false
|
|
2218
2298
|
});
|
|
2219
2299
|
if (isCancel5(confirmed) || !confirmed) {
|
|
2220
|
-
|
|
2300
|
+
log15.info("Uninstall cancelled.");
|
|
2221
2301
|
process.exit(0);
|
|
2222
2302
|
}
|
|
2223
2303
|
const ssh = new SshConnection();
|
|
@@ -2238,47 +2318,47 @@ async function runUninstall() {
|
|
|
2238
2318
|
password: sshPassword,
|
|
2239
2319
|
remoteDir: cfg.remoteDir
|
|
2240
2320
|
});
|
|
2241
|
-
|
|
2321
|
+
log15.success(`Connected to ${cfg.ssh.host}`);
|
|
2242
2322
|
} catch (err) {
|
|
2243
|
-
|
|
2323
|
+
log15.error(`SSH connection failed: ${String(err)}`);
|
|
2244
2324
|
process.exit(1);
|
|
2245
2325
|
}
|
|
2246
2326
|
try {
|
|
2247
2327
|
const remoteDir = await ssh.resolveHome(cfg.remoteDir);
|
|
2248
|
-
|
|
2328
|
+
log15.step("Stopping containers and removing volumes");
|
|
2249
2329
|
const { code } = await ssh.execSilent("docker compose down -v", remoteDir);
|
|
2250
2330
|
if (code !== 0) {
|
|
2251
|
-
|
|
2331
|
+
log15.warn("docker compose down reported an error \u2014 directory may already be partially removed");
|
|
2252
2332
|
} else {
|
|
2253
|
-
|
|
2333
|
+
log15.success("Containers and volumes removed");
|
|
2254
2334
|
}
|
|
2255
|
-
|
|
2335
|
+
log15.step(`Removing remote directory ${remoteDir}`);
|
|
2256
2336
|
await ssh.exec(`rm -rf ${remoteDir}`);
|
|
2257
|
-
|
|
2337
|
+
log15.success("Remote directory removed");
|
|
2258
2338
|
} catch (err) {
|
|
2259
2339
|
const message = err instanceof Error ? err.message : String(err);
|
|
2260
|
-
|
|
2340
|
+
log15.error(`Remote cleanup failed: ${message}`);
|
|
2261
2341
|
process.exit(1);
|
|
2262
2342
|
} finally {
|
|
2263
2343
|
await ssh.disconnect();
|
|
2264
2344
|
}
|
|
2265
|
-
|
|
2345
|
+
log15.step("Removing local config directory");
|
|
2266
2346
|
if (existsSync6(dir)) {
|
|
2267
2347
|
rmSync2(dir, { recursive: true, force: true });
|
|
2268
|
-
|
|
2348
|
+
log15.success(`Removed ${dir}`);
|
|
2269
2349
|
}
|
|
2270
2350
|
outro6(
|
|
2271
|
-
|
|
2351
|
+
chalk7.green("\u2714 Uninstall complete.") + `
|
|
2272
2352
|
|
|
2273
|
-
Run ${
|
|
2353
|
+
Run ${chalk7.cyan("npx @lead-routing/cli init")} to start fresh.`
|
|
2274
2354
|
);
|
|
2275
2355
|
}
|
|
2276
2356
|
|
|
2277
2357
|
// src/commands/signup.ts
|
|
2278
2358
|
import * as p from "@clack/prompts";
|
|
2279
|
-
import
|
|
2359
|
+
import chalk8 from "chalk";
|
|
2280
2360
|
async function runSignup() {
|
|
2281
|
-
p.intro(
|
|
2361
|
+
p.intro(chalk8.bgBlue.white(" lead-routing signup "));
|
|
2282
2362
|
const firstName = await p.text({ message: "First name", placeholder: "John", validate: (v) => v.trim() ? void 0 : "Required" });
|
|
2283
2363
|
if (p.isCancel(firstName)) process.exit(0);
|
|
2284
2364
|
const lastName = await p.text({ message: "Last name", placeholder: "Smith", validate: (v) => v.trim() ? void 0 : "Required" });
|
|
@@ -2287,10 +2367,10 @@ async function runSignup() {
|
|
|
2287
2367
|
if (p.isCancel(email)) process.exit(0);
|
|
2288
2368
|
const password5 = await p.password({ message: "Password (min 8 characters)", validate: (v) => v.length >= 8 ? void 0 : "Must be at least 8 characters" });
|
|
2289
2369
|
if (p.isCancel(password5)) process.exit(0);
|
|
2290
|
-
const
|
|
2291
|
-
if (p.isCancel(
|
|
2292
|
-
const
|
|
2293
|
-
|
|
2370
|
+
const confirm6 = await p.password({ message: "Confirm password", validate: (v) => v === password5 ? void 0 : "Passwords do not match" });
|
|
2371
|
+
if (p.isCancel(confirm6)) process.exit(0);
|
|
2372
|
+
const spinner9 = p.spinner();
|
|
2373
|
+
spinner9.start("Creating account...");
|
|
2294
2374
|
try {
|
|
2295
2375
|
const message = await apiSignup({
|
|
2296
2376
|
firstName: firstName.trim(),
|
|
@@ -2298,20 +2378,20 @@ async function runSignup() {
|
|
|
2298
2378
|
email: email.trim(),
|
|
2299
2379
|
password: password5
|
|
2300
2380
|
});
|
|
2301
|
-
|
|
2381
|
+
spinner9.stop("Account created!");
|
|
2302
2382
|
p.note(
|
|
2303
2383
|
`Check your email (${email.trim()}) for a verification link.
|
|
2304
2384
|
|
|
2305
2385
|
After verifying, run:
|
|
2306
2386
|
|
|
2307
|
-
${
|
|
2387
|
+
${chalk8.cyan("lead-routing login")}`,
|
|
2308
2388
|
"Next Steps"
|
|
2309
2389
|
);
|
|
2310
2390
|
} catch (err) {
|
|
2311
2391
|
const msg = err instanceof Error ? err.message : "Signup failed";
|
|
2312
|
-
|
|
2392
|
+
spinner9.stop(msg);
|
|
2313
2393
|
if (msg.includes("already")) {
|
|
2314
|
-
p.log.info(`Try ${
|
|
2394
|
+
p.log.info(`Try ${chalk8.cyan("lead-routing login")} instead.`);
|
|
2315
2395
|
}
|
|
2316
2396
|
process.exit(1);
|
|
2317
2397
|
}
|
|
@@ -2320,19 +2400,19 @@ After verifying, run:
|
|
|
2320
2400
|
|
|
2321
2401
|
// src/commands/login.ts
|
|
2322
2402
|
import * as p2 from "@clack/prompts";
|
|
2323
|
-
import
|
|
2403
|
+
import chalk9 from "chalk";
|
|
2324
2404
|
async function runLogin() {
|
|
2325
|
-
p2.intro(
|
|
2405
|
+
p2.intro(chalk9.bgBlue.white(" lead-routing login "));
|
|
2326
2406
|
const email = await p2.text({ message: "Email", placeholder: "john@acme.com" });
|
|
2327
2407
|
if (p2.isCancel(email)) process.exit(0);
|
|
2328
2408
|
const password5 = await p2.password({ message: "Password" });
|
|
2329
2409
|
if (p2.isCancel(password5)) process.exit(0);
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2410
|
+
const spinner9 = p2.spinner();
|
|
2411
|
+
spinner9.start("Authenticating...");
|
|
2332
2412
|
try {
|
|
2333
2413
|
const { token, customer } = await apiLogin(email.trim(), password5);
|
|
2334
2414
|
if (!customer.emailVerified) {
|
|
2335
|
-
|
|
2415
|
+
spinner9.stop("Email not verified");
|
|
2336
2416
|
p2.log.warn(`Your email (${customer.email}) hasn't been verified yet.`);
|
|
2337
2417
|
const resend = await p2.confirm({ message: "Resend verification email?" });
|
|
2338
2418
|
if (resend && !p2.isCancel(resend)) {
|
|
@@ -2345,19 +2425,421 @@ async function runLogin() {
|
|
|
2345
2425
|
}
|
|
2346
2426
|
p2.note(`Verify your email, then run:
|
|
2347
2427
|
|
|
2348
|
-
${
|
|
2428
|
+
${chalk9.cyan("lead-routing login")}`, "Next Steps");
|
|
2349
2429
|
process.exit(1);
|
|
2350
2430
|
}
|
|
2351
2431
|
saveCredentials({ token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2352
|
-
|
|
2432
|
+
spinner9.stop(`Logged in as ${customer.firstName} ${customer.lastName} \u2014 ${formatTierBadge(customer.tier)}`);
|
|
2353
2433
|
p2.note(`Credentials saved to ~/.lead-routing/credentials.json`, "Saved");
|
|
2354
2434
|
} catch (err) {
|
|
2355
|
-
|
|
2435
|
+
spinner9.stop(err instanceof Error ? err.message : "Login failed");
|
|
2356
2436
|
process.exit(1);
|
|
2357
2437
|
}
|
|
2358
2438
|
p2.outro("Done!");
|
|
2359
2439
|
}
|
|
2360
2440
|
|
|
2441
|
+
// src/commands/dev.ts
|
|
2442
|
+
import {
|
|
2443
|
+
intro as intro9,
|
|
2444
|
+
outro as outro9,
|
|
2445
|
+
spinner as spinner8,
|
|
2446
|
+
text as text7,
|
|
2447
|
+
password as promptPassword4,
|
|
2448
|
+
note as note7,
|
|
2449
|
+
log as log18,
|
|
2450
|
+
isCancel as isCancel8,
|
|
2451
|
+
cancel as cancel5,
|
|
2452
|
+
confirm as confirm5
|
|
2453
|
+
} from "@clack/prompts";
|
|
2454
|
+
import { execa as execa4 } from "execa";
|
|
2455
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync8, readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
|
|
2456
|
+
import { join as join10 } from "path";
|
|
2457
|
+
function getDevDir() {
|
|
2458
|
+
return join10(process.cwd(), "lead-routing-dev");
|
|
2459
|
+
}
|
|
2460
|
+
function renderDevDockerCompose(dbPassword, redisPassword) {
|
|
2461
|
+
return `# Lead Routing \u2014 Local Dev Docker Compose
|
|
2462
|
+
# Generated by lead-routing dev \u2014 do not edit manually.
|
|
2463
|
+
|
|
2464
|
+
services:
|
|
2465
|
+
|
|
2466
|
+
postgres:
|
|
2467
|
+
image: postgres:16-alpine
|
|
2468
|
+
restart: unless-stopped
|
|
2469
|
+
ports:
|
|
2470
|
+
- "127.0.0.1:5432:5432"
|
|
2471
|
+
environment:
|
|
2472
|
+
POSTGRES_DB: leadrouting
|
|
2473
|
+
POSTGRES_USER: leadrouting
|
|
2474
|
+
POSTGRES_PASSWORD: ${dbPassword}
|
|
2475
|
+
volumes:
|
|
2476
|
+
- postgres_data:/var/lib/postgresql/data
|
|
2477
|
+
healthcheck:
|
|
2478
|
+
test: ["CMD-SHELL", "pg_isready -U leadrouting"]
|
|
2479
|
+
interval: 5s
|
|
2480
|
+
timeout: 5s
|
|
2481
|
+
retries: 10
|
|
2482
|
+
|
|
2483
|
+
redis:
|
|
2484
|
+
image: redis:7-alpine
|
|
2485
|
+
restart: unless-stopped
|
|
2486
|
+
command: redis-server --requirepass ${redisPassword}
|
|
2487
|
+
volumes:
|
|
2488
|
+
- redis_data:/data
|
|
2489
|
+
healthcheck:
|
|
2490
|
+
test: ["CMD-SHELL", "redis-cli -a ${redisPassword} ping | grep PONG"]
|
|
2491
|
+
interval: 5s
|
|
2492
|
+
timeout: 3s
|
|
2493
|
+
retries: 10
|
|
2494
|
+
|
|
2495
|
+
web:
|
|
2496
|
+
image: ghcr.io/atgatzby/lead-routing-web:latest
|
|
2497
|
+
restart: unless-stopped
|
|
2498
|
+
ports:
|
|
2499
|
+
- "127.0.0.1:3000:3000"
|
|
2500
|
+
env_file: .env.web
|
|
2501
|
+
healthcheck:
|
|
2502
|
+
test: ["CMD-SHELL", "wget -qO- http://$(hostname -i):3000/api/health || exit 1"]
|
|
2503
|
+
interval: 10s
|
|
2504
|
+
timeout: 5s
|
|
2505
|
+
retries: 6
|
|
2506
|
+
depends_on:
|
|
2507
|
+
postgres:
|
|
2508
|
+
condition: service_healthy
|
|
2509
|
+
redis:
|
|
2510
|
+
condition: service_healthy
|
|
2511
|
+
|
|
2512
|
+
engine:
|
|
2513
|
+
image: ghcr.io/atgatzby/lead-routing-engine:latest
|
|
2514
|
+
restart: unless-stopped
|
|
2515
|
+
ports:
|
|
2516
|
+
- "127.0.0.1:3001:3001"
|
|
2517
|
+
env_file: .env.engine
|
|
2518
|
+
healthcheck:
|
|
2519
|
+
test: ["CMD-SHELL", "wget -qO- http://$(hostname -i):3001/health || exit 1"]
|
|
2520
|
+
interval: 10s
|
|
2521
|
+
timeout: 5s
|
|
2522
|
+
retries: 6
|
|
2523
|
+
depends_on:
|
|
2524
|
+
postgres:
|
|
2525
|
+
condition: service_healthy
|
|
2526
|
+
redis:
|
|
2527
|
+
condition: service_healthy
|
|
2528
|
+
|
|
2529
|
+
volumes:
|
|
2530
|
+
postgres_data:
|
|
2531
|
+
redis_data:
|
|
2532
|
+
`;
|
|
2533
|
+
}
|
|
2534
|
+
function bail3(value) {
|
|
2535
|
+
if (isCancel8(value)) {
|
|
2536
|
+
cancel5("Setup cancelled.");
|
|
2537
|
+
process.exit(0);
|
|
2538
|
+
}
|
|
2539
|
+
throw new Error("Unexpected cancel");
|
|
2540
|
+
}
|
|
2541
|
+
async function pollHealth2(url, attempts = 30, intervalMs = 3e3) {
|
|
2542
|
+
for (let i = 0; i < attempts; i++) {
|
|
2543
|
+
try {
|
|
2544
|
+
const res = await fetch(url);
|
|
2545
|
+
if (res.ok) return true;
|
|
2546
|
+
} catch {
|
|
2547
|
+
}
|
|
2548
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2549
|
+
}
|
|
2550
|
+
return false;
|
|
2551
|
+
}
|
|
2552
|
+
async function runDev(opts = {}) {
|
|
2553
|
+
if (opts.sfdc) {
|
|
2554
|
+
const devDir2 = getDevDir();
|
|
2555
|
+
const configFile2 = join10(devDir2, "dev.json");
|
|
2556
|
+
if (!existsSync7(configFile2)) {
|
|
2557
|
+
console.error("No dev environment found. Run `lead-routing dev` first.");
|
|
2558
|
+
process.exit(1);
|
|
2559
|
+
}
|
|
2560
|
+
intro9("Lead Routing \u2014 Update Salesforce Credentials");
|
|
2561
|
+
note7(
|
|
2562
|
+
"You need a Salesforce Connected App with callback URL:\n http://localhost:3000/api/auth/sfdc/callback",
|
|
2563
|
+
"Before you begin"
|
|
2564
|
+
);
|
|
2565
|
+
const clientId = await text7({
|
|
2566
|
+
message: "Consumer Key (Client ID)",
|
|
2567
|
+
validate: (v) => !v ? "Required" : void 0
|
|
2568
|
+
});
|
|
2569
|
+
if (isCancel8(clientId)) {
|
|
2570
|
+
cancel5("Cancelled.");
|
|
2571
|
+
process.exit(0);
|
|
2572
|
+
}
|
|
2573
|
+
const clientSecret = await promptPassword4({ message: "Consumer Secret" });
|
|
2574
|
+
if (isCancel8(clientSecret)) {
|
|
2575
|
+
cancel5("Cancelled.");
|
|
2576
|
+
process.exit(0);
|
|
2577
|
+
}
|
|
2578
|
+
const loginUrl = await text7({
|
|
2579
|
+
message: "Login URL",
|
|
2580
|
+
initialValue: "https://login.salesforce.com",
|
|
2581
|
+
validate: (v) => !v ? "Required" : void 0
|
|
2582
|
+
});
|
|
2583
|
+
if (isCancel8(loginUrl)) {
|
|
2584
|
+
cancel5("Cancelled.");
|
|
2585
|
+
process.exit(0);
|
|
2586
|
+
}
|
|
2587
|
+
const envPath = join10(devDir2, ".env.web");
|
|
2588
|
+
const lines = readFileSync8(envPath, "utf8").split("\n");
|
|
2589
|
+
const updates = {
|
|
2590
|
+
SFDC_CLIENT_ID: clientId.trim(),
|
|
2591
|
+
SFDC_CLIENT_SECRET: clientSecret.trim(),
|
|
2592
|
+
SFDC_LOGIN_URL: loginUrl.trim(),
|
|
2593
|
+
SFDC_REDIRECT_URI: "http://localhost:3000/api/auth/sfdc/callback"
|
|
2594
|
+
};
|
|
2595
|
+
const updated = /* @__PURE__ */ new Set();
|
|
2596
|
+
const result = lines.map((line) => {
|
|
2597
|
+
const eq = line.indexOf("=");
|
|
2598
|
+
if (eq === -1) return line;
|
|
2599
|
+
const key = line.slice(0, eq);
|
|
2600
|
+
if (key in updates) {
|
|
2601
|
+
updated.add(key);
|
|
2602
|
+
return `${key}=${updates[key]}`;
|
|
2603
|
+
}
|
|
2604
|
+
return line;
|
|
2605
|
+
});
|
|
2606
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
2607
|
+
if (!updated.has(k)) result.push(`${k}=${v}`);
|
|
2608
|
+
}
|
|
2609
|
+
writeFileSync8(envPath, result.join("\n"), "utf8");
|
|
2610
|
+
const cfg2 = JSON.parse(readFileSync8(configFile2, "utf8"));
|
|
2611
|
+
cfg2.sfdcClientId = clientId.trim();
|
|
2612
|
+
cfg2.sfdcClientSecret = clientSecret.trim();
|
|
2613
|
+
cfg2.sfdcLoginUrl = loginUrl.trim();
|
|
2614
|
+
writeFileSync8(configFile2, JSON.stringify(cfg2, null, 2), "utf8");
|
|
2615
|
+
log18.success("Credentials saved");
|
|
2616
|
+
const s = spinner8();
|
|
2617
|
+
s.start("Restarting web container...");
|
|
2618
|
+
await execa4("docker", ["compose", "restart", "web"], { cwd: devDir2, stdio: "ignore" });
|
|
2619
|
+
s.stop("Web container restarted");
|
|
2620
|
+
outro9("Done! Connect Salesforce at http://localhost:3000/integrations/salesforce");
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
if (opts.logs !== void 0) {
|
|
2624
|
+
const service = opts.logs || "engine";
|
|
2625
|
+
const devDir2 = getDevDir();
|
|
2626
|
+
if (!existsSync7(join10(devDir2, "docker-compose.yml"))) {
|
|
2627
|
+
console.error("No dev environment found. Run `lead-routing dev` first.");
|
|
2628
|
+
process.exit(1);
|
|
2629
|
+
}
|
|
2630
|
+
const valid = ["web", "engine", "postgres", "redis"];
|
|
2631
|
+
if (!valid.includes(service)) {
|
|
2632
|
+
console.error(`Unknown service "${service}". Valid: ${valid.join(", ")}`);
|
|
2633
|
+
process.exit(1);
|
|
2634
|
+
}
|
|
2635
|
+
await execa4("docker", ["compose", "logs", "-f", "--tail=100", service], {
|
|
2636
|
+
cwd: devDir2,
|
|
2637
|
+
stdio: "inherit"
|
|
2638
|
+
});
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
intro9("Lead Routing \u2014 Local Dev");
|
|
2642
|
+
try {
|
|
2643
|
+
await execa4("docker", ["info"], { stdio: "ignore" });
|
|
2644
|
+
} catch {
|
|
2645
|
+
log18.error("Docker is not running. Please start Docker Desktop and try again.");
|
|
2646
|
+
process.exit(1);
|
|
2647
|
+
}
|
|
2648
|
+
const devDir = getDevDir();
|
|
2649
|
+
const configFile = join10(devDir, "dev.json");
|
|
2650
|
+
const isFirstRun = !existsSync7(configFile);
|
|
2651
|
+
let cfg;
|
|
2652
|
+
let adminPassword = "";
|
|
2653
|
+
if (!isFirstRun && !opts.reset) {
|
|
2654
|
+
cfg = JSON.parse(readFileSync8(configFile, "utf8"));
|
|
2655
|
+
log18.info("Existing dev environment found \u2014 starting services...");
|
|
2656
|
+
} else {
|
|
2657
|
+
if (opts.reset && !isFirstRun) {
|
|
2658
|
+
const ok = await confirm5({
|
|
2659
|
+
message: "This will wipe all local dev data (database, redis). Continue?"
|
|
2660
|
+
});
|
|
2661
|
+
if (isCancel8(ok) || !ok) {
|
|
2662
|
+
cancel5("Reset cancelled.");
|
|
2663
|
+
process.exit(0);
|
|
2664
|
+
}
|
|
2665
|
+
const s = spinner8();
|
|
2666
|
+
s.start("Stopping and removing volumes...");
|
|
2667
|
+
try {
|
|
2668
|
+
await execa4("docker", ["compose", "down", "-v"], { cwd: devDir, stdio: "ignore" });
|
|
2669
|
+
} catch {
|
|
2670
|
+
}
|
|
2671
|
+
s.stop("Volumes removed");
|
|
2672
|
+
}
|
|
2673
|
+
note7(
|
|
2674
|
+
"Sets up a full Lead Routing stack locally using Docker.\nNo SSH, no VPS, no manual steps.",
|
|
2675
|
+
"First-time setup"
|
|
2676
|
+
);
|
|
2677
|
+
const adminEmail = await text7({
|
|
2678
|
+
message: "Admin email",
|
|
2679
|
+
placeholder: "you@company.com",
|
|
2680
|
+
validate: (v) => {
|
|
2681
|
+
if (!v) return "Required";
|
|
2682
|
+
if (!v.includes("@")) return "Must be a valid email";
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
if (isCancel8(adminEmail)) bail3(adminEmail);
|
|
2686
|
+
const pw = await promptPassword4({
|
|
2687
|
+
message: "Admin password (min 8 characters)",
|
|
2688
|
+
validate: (v) => {
|
|
2689
|
+
if (!v) return "Required";
|
|
2690
|
+
if (v.length < 8) return "Must be at least 8 characters";
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
if (isCancel8(pw)) bail3(pw);
|
|
2694
|
+
adminPassword = pw;
|
|
2695
|
+
note7(
|
|
2696
|
+
"Optional \u2014 needed to connect Salesforce.\nPress Enter to skip; add credentials later via `lead-routing config sfdc`.",
|
|
2697
|
+
"Salesforce Connected App"
|
|
2698
|
+
);
|
|
2699
|
+
const sfdcClientId = await text7({
|
|
2700
|
+
message: "Consumer Key (Client ID)",
|
|
2701
|
+
placeholder: "Leave blank to skip"
|
|
2702
|
+
});
|
|
2703
|
+
if (isCancel8(sfdcClientId)) bail3(sfdcClientId);
|
|
2704
|
+
let sfdcClientSecret = "";
|
|
2705
|
+
if (sfdcClientId?.trim()) {
|
|
2706
|
+
const s = await promptPassword4({ message: "Consumer Secret" });
|
|
2707
|
+
if (isCancel8(s)) bail3(s);
|
|
2708
|
+
sfdcClientSecret = s.trim();
|
|
2709
|
+
}
|
|
2710
|
+
const tunnelUrl = await text7({
|
|
2711
|
+
message: "Engine tunnel URL (for Salesforce webhooks)",
|
|
2712
|
+
placeholder: "Leave blank \u2014 use `ssh -R 80:localhost:3001 localhost.run` to get one"
|
|
2713
|
+
});
|
|
2714
|
+
if (isCancel8(tunnelUrl)) bail3(tunnelUrl);
|
|
2715
|
+
cfg = {
|
|
2716
|
+
adminEmail: adminEmail.trim(),
|
|
2717
|
+
dbPassword: generateSecret(16),
|
|
2718
|
+
redisPassword: generateSecret(16),
|
|
2719
|
+
sessionSecret: generateSecret(32),
|
|
2720
|
+
engineWebhookSecret: generateSecret(32),
|
|
2721
|
+
internalApiKey: generateSecret(32),
|
|
2722
|
+
sfdcClientId: sfdcClientId?.trim() ?? "",
|
|
2723
|
+
sfdcClientSecret,
|
|
2724
|
+
sfdcLoginUrl: "https://login.salesforce.com",
|
|
2725
|
+
engineTunnelUrl: tunnelUrl?.trim() ?? ""
|
|
2726
|
+
};
|
|
2727
|
+
mkdirSync3(devDir, { recursive: true });
|
|
2728
|
+
const dbUrl = `postgresql://leadrouting:${cfg.dbPassword}@postgres:5432/leadrouting`;
|
|
2729
|
+
const redisUrl = `redis://:${cfg.redisPassword}@redis:6379`;
|
|
2730
|
+
const publicEngineUrl = cfg.engineTunnelUrl || "http://localhost:3001";
|
|
2731
|
+
writeFileSync8(
|
|
2732
|
+
join10(devDir, "docker-compose.yml"),
|
|
2733
|
+
renderDevDockerCompose(cfg.dbPassword, cfg.redisPassword),
|
|
2734
|
+
"utf8"
|
|
2735
|
+
);
|
|
2736
|
+
const envWebBase = renderEnvWeb({
|
|
2737
|
+
appUrl: "http://localhost:3000",
|
|
2738
|
+
engineUrl: "http://engine:3001",
|
|
2739
|
+
publicEngineUrl,
|
|
2740
|
+
databaseUrl: dbUrl,
|
|
2741
|
+
redisUrl,
|
|
2742
|
+
sessionSecret: cfg.sessionSecret,
|
|
2743
|
+
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
2744
|
+
adminEmail: cfg.adminEmail,
|
|
2745
|
+
adminPassword,
|
|
2746
|
+
internalApiKey: cfg.internalApiKey,
|
|
2747
|
+
licenseKey: void 0,
|
|
2748
|
+
licenseTier: "pro"
|
|
2749
|
+
});
|
|
2750
|
+
writeFileSync8(
|
|
2751
|
+
join10(devDir, ".env.web"),
|
|
2752
|
+
envWebBase + `
|
|
2753
|
+
# Salesforce Connected App
|
|
2754
|
+
SFDC_CLIENT_ID=${cfg.sfdcClientId}
|
|
2755
|
+
SFDC_CLIENT_SECRET=${cfg.sfdcClientSecret}
|
|
2756
|
+
SFDC_REDIRECT_URI=http://localhost:3000/api/auth/sfdc/callback
|
|
2757
|
+
SFDC_LOGIN_URL=${cfg.sfdcLoginUrl}
|
|
2758
|
+
`,
|
|
2759
|
+
"utf8"
|
|
2760
|
+
);
|
|
2761
|
+
writeFileSync8(
|
|
2762
|
+
join10(devDir, ".env.engine"),
|
|
2763
|
+
renderEnvEngine({
|
|
2764
|
+
databaseUrl: dbUrl,
|
|
2765
|
+
redisUrl,
|
|
2766
|
+
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
2767
|
+
internalApiKey: cfg.internalApiKey,
|
|
2768
|
+
licenseKey: void 0,
|
|
2769
|
+
licenseTier: "pro"
|
|
2770
|
+
}),
|
|
2771
|
+
"utf8"
|
|
2772
|
+
);
|
|
2773
|
+
writeFileSync8(configFile, JSON.stringify(cfg, null, 2), "utf8");
|
|
2774
|
+
writeConfig(devDir, {
|
|
2775
|
+
appUrl: "http://localhost:3000",
|
|
2776
|
+
engineUrl: publicEngineUrl,
|
|
2777
|
+
installDir: devDir,
|
|
2778
|
+
remoteDir: devDir,
|
|
2779
|
+
ssh: { host: "localhost", port: 22, username: "local" },
|
|
2780
|
+
dockerManaged: { db: true, redis: true },
|
|
2781
|
+
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
2782
|
+
licenseTier: "pro",
|
|
2783
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2784
|
+
version: "0.0.0"
|
|
2785
|
+
});
|
|
2786
|
+
log18.success("Generated config files in ./lead-routing-dev/");
|
|
2787
|
+
}
|
|
2788
|
+
const pullS = spinner8();
|
|
2789
|
+
pullS.start("Pulling latest images...");
|
|
2790
|
+
try {
|
|
2791
|
+
await execa4("docker", ["compose", "pull"], { cwd: devDir, stdio: "ignore" });
|
|
2792
|
+
pullS.stop("Images up to date");
|
|
2793
|
+
} catch {
|
|
2794
|
+
pullS.stop("Using cached images");
|
|
2795
|
+
}
|
|
2796
|
+
const startS = spinner8();
|
|
2797
|
+
startS.start("Starting services...");
|
|
2798
|
+
try {
|
|
2799
|
+
await execa4("docker", ["compose", "up", "-d", "--remove-orphans"], {
|
|
2800
|
+
cwd: devDir,
|
|
2801
|
+
stdio: "ignore"
|
|
2802
|
+
});
|
|
2803
|
+
startS.stop("Services started");
|
|
2804
|
+
} catch (err) {
|
|
2805
|
+
startS.stop("Failed to start services");
|
|
2806
|
+
log18.error(err instanceof Error ? err.message : String(err));
|
|
2807
|
+
process.exit(1);
|
|
2808
|
+
}
|
|
2809
|
+
const healthS = spinner8();
|
|
2810
|
+
healthS.start("Waiting for web app...");
|
|
2811
|
+
const webOk = await pollHealth2("http://localhost:3000/api/health");
|
|
2812
|
+
if (!webOk) {
|
|
2813
|
+
healthS.stop("Web app did not start in time");
|
|
2814
|
+
log18.warn("Run `lead-routing dev logs web` to diagnose.");
|
|
2815
|
+
process.exit(1);
|
|
2816
|
+
}
|
|
2817
|
+
healthS.stop("Web app ready");
|
|
2818
|
+
const engineS = spinner8();
|
|
2819
|
+
engineS.start("Waiting for engine...");
|
|
2820
|
+
const engineOk = await pollHealth2("http://localhost:3001/health");
|
|
2821
|
+
engineS.stop(engineOk ? "Engine ready" : "Engine slow to start (check: lead-routing dev logs engine)");
|
|
2822
|
+
note7(
|
|
2823
|
+
` Web: http://localhost:3000
|
|
2824
|
+
Engine: http://localhost:3001
|
|
2825
|
+
|
|
2826
|
+
Login: ${cfg.adminEmail}`,
|
|
2827
|
+
"\u2713 Lead Routing running locally"
|
|
2828
|
+
);
|
|
2829
|
+
if (cfg.sfdcClientId) {
|
|
2830
|
+
if (cfg.engineTunnelUrl) {
|
|
2831
|
+
log18.info("Next: run `lead-routing sfdc deploy` to deploy the Salesforce package (same as prod)");
|
|
2832
|
+
} else {
|
|
2833
|
+
log18.warn(
|
|
2834
|
+
"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"
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
} else {
|
|
2838
|
+
log18.info("No Salesforce creds set. Add via: lead-routing dev --sfdc");
|
|
2839
|
+
}
|
|
2840
|
+
outro9("Run `lead-routing dev logs` to stream logs \u2022 `lead-routing dev --reset` to wipe and restart");
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2361
2843
|
// src/index.ts
|
|
2362
2844
|
var program = new Command();
|
|
2363
2845
|
program.name("lead-routing").description("Self-hosted Lead Routing \u2014 scaffold, deploy, and manage your installation").version("0.1.13");
|
|
@@ -2381,6 +2863,7 @@ config.command("sfdc").description("Update Salesforce Connected App credentials
|
|
|
2381
2863
|
var sfdc = program.command("sfdc").description("Manage the Salesforce package for this installation");
|
|
2382
2864
|
sfdc.command("deploy").description("Deploy (or redeploy) the Lead Router Salesforce package to your Salesforce org").action(runSfdcDeploy);
|
|
2383
2865
|
program.command("uninstall").description("Stop all containers, remove all data, and delete the remote installation").action(runUninstall);
|
|
2866
|
+
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
2867
|
program.command("signup").description("Create a new Lead Routing account").action(runSignup);
|
|
2385
2868
|
program.command("login").description("Log in to your Lead Routing account").action(runLogin);
|
|
2386
2869
|
program.parseAsync(process.argv).catch((err) => {
|