@crmy/cli 0.6.0 → 0.6.1
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 +378 -55
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command29 } from "commander";
|
|
5
5
|
import { createRequire as createRequire2 } from "module";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
-
import
|
|
7
|
+
import path6 from "path";
|
|
8
8
|
|
|
9
9
|
// src/commands/init.ts
|
|
10
10
|
import { Command } from "commander";
|
|
@@ -22,10 +22,10 @@ function createSpinner(initialMessage) {
|
|
|
22
22
|
if (message) console.log(` ... ${message}`);
|
|
23
23
|
},
|
|
24
24
|
succeed(message) {
|
|
25
|
-
console.log(` \u2713 ${message}`);
|
|
25
|
+
console.log(` \x1B[32m\u2713\x1B[0m ${message}`);
|
|
26
26
|
},
|
|
27
27
|
fail(message) {
|
|
28
|
-
console.log(` \u2717 ${message}`);
|
|
28
|
+
console.log(` \x1B[31m\u2717\x1B[0m ${message}`);
|
|
29
29
|
},
|
|
30
30
|
stop() {
|
|
31
31
|
}
|
|
@@ -137,8 +137,71 @@ function validateEmail(input) {
|
|
|
137
137
|
function validatePassword(input) {
|
|
138
138
|
return input.length >= 12 ? true : "Password must be at least 12 characters";
|
|
139
139
|
}
|
|
140
|
+
async function ensureDatabaseExists(databaseUrl, interactive) {
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
parsed = new URL(databaseUrl);
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const dbName = parsed.pathname.replace(/^\//, "");
|
|
148
|
+
if (!dbName) return;
|
|
149
|
+
const isLocal = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1";
|
|
150
|
+
if (!isLocal) return;
|
|
151
|
+
const maintUrl = new URL(databaseUrl);
|
|
152
|
+
maintUrl.pathname = "/postgres";
|
|
153
|
+
const pg = await import("pg");
|
|
154
|
+
const client = new pg.default.Client({ connectionString: maintUrl.toString() });
|
|
155
|
+
try {
|
|
156
|
+
await client.connect();
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const result = await client.query(
|
|
162
|
+
"SELECT 1 FROM pg_database WHERE datname = $1",
|
|
163
|
+
[dbName]
|
|
164
|
+
);
|
|
165
|
+
if (result.rows.length > 0) return;
|
|
166
|
+
if (interactive) {
|
|
167
|
+
const { default: inquirer } = await import("inquirer");
|
|
168
|
+
console.log(`
|
|
169
|
+
\x1B[33m\u26A0\x1B[0m Database \x1B[1m${dbName}\x1B[0m does not exist.
|
|
170
|
+
`);
|
|
171
|
+
const { create } = await inquirer.prompt([
|
|
172
|
+
{
|
|
173
|
+
type: "confirm",
|
|
174
|
+
name: "create",
|
|
175
|
+
message: ` Create database "${dbName}" now?`,
|
|
176
|
+
default: true
|
|
177
|
+
}
|
|
178
|
+
]);
|
|
179
|
+
if (!create) {
|
|
180
|
+
console.log(`
|
|
181
|
+
Create it manually: createdb ${dbName}
|
|
182
|
+
`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const spinner = createSpinner(`Creating database "${dbName}"\u2026`);
|
|
187
|
+
try {
|
|
188
|
+
await client.query(`CREATE DATABASE "${dbName}"`);
|
|
189
|
+
spinner.succeed(`Database "${dbName}" created`);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
spinner.fail(`Failed to create database "${dbName}"`);
|
|
192
|
+
console.error(`
|
|
193
|
+
Error: ${err.message}`);
|
|
194
|
+
console.error(` Create it manually: createdb ${dbName}
|
|
195
|
+
`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
} finally {
|
|
199
|
+
await client.end();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
140
202
|
function initCommand() {
|
|
141
|
-
return new Command("init").description("Interactive setup wizard: database, migrations, admin account").action(async () => {
|
|
203
|
+
return new Command("init").description("Interactive setup wizard: database, migrations, admin account").option("-y, --yes", "Accept defaults non-interactively (requires CRMY_ADMIN_EMAIL + CRMY_ADMIN_PASSWORD env vars)").action(async (opts) => {
|
|
204
|
+
const yesMode = !!opts.yes;
|
|
142
205
|
const { default: inquirer } = await import("inquirer");
|
|
143
206
|
console.log("\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
144
207
|
console.log(" \u2502 crmy.ai \u2014 Setup Wizard \u2502");
|
|
@@ -149,39 +212,53 @@ function initCommand() {
|
|
|
149
212
|
console.log(" Step 3 \u2014 Create your admin account\n");
|
|
150
213
|
const localConfigPath = path2.join(process.cwd(), ".crmy.json");
|
|
151
214
|
if (fs2.existsSync(localConfigPath)) {
|
|
152
|
-
|
|
153
|
-
|
|
215
|
+
if (yesMode) {
|
|
216
|
+
console.log(" \x1B[33m\u26A0\x1B[0m Overwriting existing .crmy.json (--yes mode)\n");
|
|
217
|
+
} else {
|
|
218
|
+
console.log(" \x1B[33m\u26A0\x1B[0m A .crmy.json already exists in this directory.\n");
|
|
219
|
+
const { overwrite } = await inquirer.prompt([
|
|
220
|
+
{
|
|
221
|
+
type: "confirm",
|
|
222
|
+
name: "overwrite",
|
|
223
|
+
message: " Overwrite it and run setup again?",
|
|
224
|
+
default: false
|
|
225
|
+
}
|
|
226
|
+
]);
|
|
227
|
+
if (!overwrite) {
|
|
228
|
+
console.log("\n Setup cancelled. Your existing config was not changed.\n");
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
console.log("");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
console.log(" \u2500\u2500 Step 1 of 3: Database Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
235
|
+
let databaseUrl;
|
|
236
|
+
if (yesMode) {
|
|
237
|
+
databaseUrl = process.env.DATABASE_URL ?? "postgresql://localhost:5432/crmy";
|
|
238
|
+
console.log(` Using database URL: ${databaseUrl}
|
|
239
|
+
`);
|
|
240
|
+
} else {
|
|
241
|
+
console.log(" Enter your PostgreSQL connection string.");
|
|
242
|
+
console.log(" Format: postgresql://user:password@host:5432/dbname\n");
|
|
243
|
+
console.log(" Options:");
|
|
244
|
+
console.log(" \u2022 Local install: postgresql://localhost:5432/crmy");
|
|
245
|
+
console.log(" \u2022 Docker: postgresql://postgres:postgres@localhost:5432/crmy");
|
|
246
|
+
console.log(" \u2022 Supabase: Project Settings \u2192 Database \u2192 Connection String");
|
|
247
|
+
console.log(" \u2022 Neon: Dashboard \u2192 Connection Details\n");
|
|
248
|
+
console.log(" \x1B[2mNote: the wizard will retry the connection up to 5 times.\x1B[0m\n");
|
|
249
|
+
const answers = await inquirer.prompt([
|
|
154
250
|
{
|
|
155
|
-
type: "
|
|
156
|
-
name: "
|
|
157
|
-
message: "
|
|
158
|
-
default:
|
|
251
|
+
type: "input",
|
|
252
|
+
name: "databaseUrl",
|
|
253
|
+
message: " PostgreSQL connection string:",
|
|
254
|
+
default: "postgresql://localhost:5432/crmy"
|
|
159
255
|
}
|
|
160
256
|
]);
|
|
161
|
-
|
|
162
|
-
console.log("\n Setup cancelled. Your existing config was not changed.\n");
|
|
163
|
-
process.exit(0);
|
|
164
|
-
}
|
|
165
|
-
console.log("");
|
|
257
|
+
databaseUrl = answers.databaseUrl;
|
|
166
258
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
console.log("
|
|
170
|
-
console.log(" Options:");
|
|
171
|
-
console.log(" \u2022 Local install: postgresql://localhost:5432/crmy");
|
|
172
|
-
console.log(" \u2022 Docker: postgresql://postgres:postgres@localhost:5432/crmy");
|
|
173
|
-
console.log(" \u2022 Supabase: Project Settings \u2192 Database \u2192 Connection String");
|
|
174
|
-
console.log(" \u2022 Neon: Dashboard \u2192 Connection Details\n");
|
|
175
|
-
console.log(" \x1B[2mNote: the wizard will retry the connection up to 5 times.\x1B[0m\n");
|
|
176
|
-
const { databaseUrl } = await inquirer.prompt([
|
|
177
|
-
{
|
|
178
|
-
type: "input",
|
|
179
|
-
name: "databaseUrl",
|
|
180
|
-
message: " PostgreSQL connection string:",
|
|
181
|
-
default: "postgresql://localhost:5432/crmy"
|
|
182
|
-
}
|
|
183
|
-
]);
|
|
184
|
-
console.log("\n \u2500\u2500 Step 2 of 3: Database Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
259
|
+
const isInteractive = process.stdin.isTTY !== false && !yesMode;
|
|
260
|
+
await ensureDatabaseExists(databaseUrl, isInteractive);
|
|
261
|
+
console.log("\n \u2500\u2500 Step 2 of 3: Database Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
185
262
|
process.env.CRMY_IMPORTED = "1";
|
|
186
263
|
const { initPool, closePool, runMigrations } = await import("@crmy/server");
|
|
187
264
|
let spinner = createSpinner("Connecting to database\u2026");
|
|
@@ -208,7 +285,9 @@ function initCommand() {
|
|
|
208
285
|
spinner = createSpinner("Running database migrations\u2026");
|
|
209
286
|
let ran = [];
|
|
210
287
|
try {
|
|
211
|
-
ran = await runMigrations(db)
|
|
288
|
+
ran = await runMigrations(db, (name2, index, total) => {
|
|
289
|
+
spinner.update(`Running migration ${index + 1}/${total}: ${name2}\u2026`);
|
|
290
|
+
});
|
|
212
291
|
if (ran.length > 0) {
|
|
213
292
|
spinner.succeed(`Migrations complete \x1B[2m(${ran.length} applied)\x1B[0m`);
|
|
214
293
|
} else {
|
|
@@ -230,6 +309,38 @@ function initCommand() {
|
|
|
230
309
|
await closePool();
|
|
231
310
|
process.exit(1);
|
|
232
311
|
}
|
|
312
|
+
if (process.env.ENABLE_PGVECTOR !== "true" && !yesMode && isInteractive) {
|
|
313
|
+
console.log("");
|
|
314
|
+
const { enablePgvector } = await inquirer.prompt([
|
|
315
|
+
{
|
|
316
|
+
type: "confirm",
|
|
317
|
+
name: "enablePgvector",
|
|
318
|
+
message: " Enable semantic search? (requires pgvector extension in PostgreSQL)",
|
|
319
|
+
default: false
|
|
320
|
+
}
|
|
321
|
+
]);
|
|
322
|
+
if (enablePgvector) {
|
|
323
|
+
process.env.ENABLE_PGVECTOR = "true";
|
|
324
|
+
spinner = createSpinner("Running pgvector migration\u2026");
|
|
325
|
+
try {
|
|
326
|
+
const pgRan = await runMigrations(db);
|
|
327
|
+
if (pgRan.length > 0) {
|
|
328
|
+
spinner.succeed("Semantic search enabled \x1B[2m(pgvector)\x1B[0m");
|
|
329
|
+
} else {
|
|
330
|
+
spinner.succeed("Semantic search \x1B[2m(already enabled)\x1B[0m");
|
|
331
|
+
}
|
|
332
|
+
} catch (err) {
|
|
333
|
+
spinner.fail("pgvector migration failed");
|
|
334
|
+
console.log(
|
|
335
|
+
`
|
|
336
|
+
\x1B[33m\u26A0\x1B[0m ${err.message}
|
|
337
|
+
This is non-fatal \u2014 CRMy works without semantic search.
|
|
338
|
+
You can enable it later by setting ENABLE_PGVECTOR=true and running: crmy migrate
|
|
339
|
+
`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
233
344
|
spinner = createSpinner("Seeding default tenant\u2026");
|
|
234
345
|
let tenantId;
|
|
235
346
|
try {
|
|
@@ -248,15 +359,15 @@ function initCommand() {
|
|
|
248
359
|
await closePool();
|
|
249
360
|
process.exit(1);
|
|
250
361
|
}
|
|
251
|
-
console.log("\n \u2500\u2500 Step 3 of 3: Admin Account \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
362
|
+
console.log("\n \u2500\u2500 Step 3 of 3: Admin Account \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
252
363
|
const envEmail = process.env.CRMY_ADMIN_EMAIL;
|
|
253
364
|
const envPassword = process.env.CRMY_ADMIN_PASSWORD;
|
|
254
365
|
const envName = process.env.CRMY_ADMIN_NAME;
|
|
255
|
-
const
|
|
366
|
+
const useEnvVars = yesMode || !isInteractive || !!envEmail;
|
|
256
367
|
let name;
|
|
257
368
|
let email;
|
|
258
369
|
let password;
|
|
259
|
-
if (
|
|
370
|
+
if (!useEnvVars) {
|
|
260
371
|
console.log(" Create the first admin account for the CRMy web UI and CLI.\n");
|
|
261
372
|
console.log(
|
|
262
373
|
" \x1B[33mNOTE:\x1B[0m These are your \x1B[1mCRMy login credentials\x1B[0m \u2014 NOT your database credentials.\n"
|
|
@@ -294,7 +405,7 @@ function initCommand() {
|
|
|
294
405
|
} else {
|
|
295
406
|
if (!envEmail || !envPassword) {
|
|
296
407
|
console.error(
|
|
297
|
-
"\n Error: Non-interactive environment detected.\n\n Set CRMY_ADMIN_EMAIL and CRMY_ADMIN_PASSWORD environment variables\n to create the admin account without interactive prompts.\n"
|
|
408
|
+
"\n Error: Non-interactive environment detected.\n\n Set CRMY_ADMIN_EMAIL and CRMY_ADMIN_PASSWORD environment variables\n to create the admin account without interactive prompts.\n (Also required when using --yes flag)\n"
|
|
298
409
|
);
|
|
299
410
|
await closePool();
|
|
300
411
|
process.exit(1);
|
|
@@ -321,6 +432,7 @@ function initCommand() {
|
|
|
321
432
|
console.log(" Creating admin account from environment variables\u2026\n");
|
|
322
433
|
}
|
|
323
434
|
spinner = createSpinner("Creating admin account\u2026");
|
|
435
|
+
let rawKey = "";
|
|
324
436
|
try {
|
|
325
437
|
const salt = crypto.randomBytes(16);
|
|
326
438
|
const hash = crypto.scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1 });
|
|
@@ -333,7 +445,7 @@ function initCommand() {
|
|
|
333
445
|
[tenantId, email.trim(), name, passwordHash]
|
|
334
446
|
);
|
|
335
447
|
const userId = userResult.rows[0].id;
|
|
336
|
-
|
|
448
|
+
rawKey = "crmy_" + crypto.randomBytes(32).toString("hex");
|
|
337
449
|
const keyHash = crypto.createHash("sha256").update(rawKey).digest("hex");
|
|
338
450
|
await db.query(
|
|
339
451
|
`INSERT INTO api_keys (tenant_id, user_id, key_hash, label, scopes)
|
|
@@ -367,8 +479,8 @@ function initCommand() {
|
|
|
367
479
|
await closePool();
|
|
368
480
|
process.exit(1);
|
|
369
481
|
}
|
|
370
|
-
let seedDemo =
|
|
371
|
-
if (isInteractive) {
|
|
482
|
+
let seedDemo = yesMode;
|
|
483
|
+
if (!yesMode && isInteractive) {
|
|
372
484
|
const { loadDemo } = await inquirer.prompt([
|
|
373
485
|
{
|
|
374
486
|
type: "confirm",
|
|
@@ -379,23 +491,61 @@ function initCommand() {
|
|
|
379
491
|
]);
|
|
380
492
|
seedDemo = loadDemo;
|
|
381
493
|
}
|
|
382
|
-
await closePool();
|
|
383
494
|
if (seedDemo) {
|
|
384
495
|
console.log("");
|
|
385
|
-
|
|
496
|
+
spinner = createSpinner("Seeding demo data\u2026");
|
|
386
497
|
try {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
498
|
+
const serverModule = await import("@crmy/server");
|
|
499
|
+
const IDS = {
|
|
500
|
+
ACTOR_CODY: "d0000000-0000-4000-a000-000000000001",
|
|
501
|
+
ACTOR_SARAH_R: "d0000000-0000-4000-a000-000000000002",
|
|
502
|
+
ACTOR_OUTREACH: "d0000000-0000-4000-a000-000000000003",
|
|
503
|
+
ACTOR_RESEARCH: "d0000000-0000-4000-a000-000000000004",
|
|
504
|
+
ACCT_ACME: "d0000000-0000-4000-b000-000000000001",
|
|
505
|
+
ACCT_BRIGHTSIDE: "d0000000-0000-4000-b000-000000000002",
|
|
506
|
+
ACCT_VERTEX: "d0000000-0000-4000-b000-000000000003",
|
|
507
|
+
CT_SARAH_CHEN: "d0000000-0000-4000-c000-000000000001",
|
|
508
|
+
CT_MARCUS_WEBB: "d0000000-0000-4000-c000-000000000002",
|
|
509
|
+
CT_PRIYA_NAIR: "d0000000-0000-4000-c000-000000000003",
|
|
510
|
+
CT_JORDAN_LIU: "d0000000-0000-4000-c000-000000000004",
|
|
511
|
+
CT_TOMAS_RIVERA: "d0000000-0000-4000-c000-000000000005",
|
|
512
|
+
CT_KEIKO_YAMAMOTO: "d0000000-0000-4000-c000-000000000006",
|
|
513
|
+
OPP_ACME: "d0000000-0000-4000-d000-000000000001",
|
|
514
|
+
OPP_BRIGHTSIDE: "d0000000-0000-4000-d000-000000000002",
|
|
515
|
+
OPP_VERTEX: "d0000000-0000-4000-d000-000000000003"
|
|
516
|
+
};
|
|
517
|
+
const check = await db.query("SELECT id FROM actors WHERE id = $1", [IDS.ACTOR_CODY]);
|
|
518
|
+
if (check.rows.length === 0) {
|
|
519
|
+
await db.query(`INSERT INTO actors (id, tenant_id, actor_type, display_name, email) VALUES ($1, $2, 'human', 'Cody Harris', 'cody@crmy.ai') ON CONFLICT (id) DO NOTHING`, [IDS.ACTOR_CODY, tenantId]);
|
|
520
|
+
await db.query(`INSERT INTO actors (id, tenant_id, actor_type, display_name, email) VALUES ($1, $2, 'human', 'Sarah Reeves', 'sarah@crmy.ai') ON CONFLICT (id) DO NOTHING`, [IDS.ACTOR_SARAH_R, tenantId]);
|
|
521
|
+
await db.query(`INSERT INTO actors (id, tenant_id, actor_type, display_name, email, agent_identifier, agent_model) VALUES ($1, $2, 'agent', 'Outreach Agent', NULL, 'outreach-v1', 'claude-sonnet-4-20250514') ON CONFLICT (id) DO NOTHING`, [IDS.ACTOR_OUTREACH, tenantId]);
|
|
522
|
+
await db.query(`INSERT INTO actors (id, tenant_id, actor_type, display_name, email, agent_identifier, agent_model) VALUES ($1, $2, 'agent', 'Research Agent', NULL, 'research-v1', 'claude-sonnet-4-20250514') ON CONFLICT (id) DO NOTHING`, [IDS.ACTOR_RESEARCH, tenantId]);
|
|
523
|
+
await db.query(`INSERT INTO accounts (id, tenant_id, name, industry, health_score, annual_revenue, domain, website) VALUES ($1, $2, 'Acme Corp', 'SaaS', 72, 180000, 'acme.com', 'https://acme.com') ON CONFLICT (id) DO NOTHING`, [IDS.ACCT_ACME, tenantId]);
|
|
524
|
+
await db.query(`INSERT INTO accounts (id, tenant_id, name, industry, health_score, annual_revenue, domain, website) VALUES ($1, $2, 'Brightside Health', 'Healthcare', 45, 96000, 'brightsidehealth.com', 'https://brightsidehealth.com') ON CONFLICT (id) DO NOTHING`, [IDS.ACCT_BRIGHTSIDE, tenantId]);
|
|
525
|
+
await db.query(`INSERT INTO accounts (id, tenant_id, name, industry, health_score, annual_revenue, domain, website) VALUES ($1, $2, 'Vertex Logistics', 'Logistics', 88, 240000, 'vertex.io', 'https://vertex.io') ON CONFLICT (id) DO NOTHING`, [IDS.ACCT_VERTEX, tenantId]);
|
|
526
|
+
await db.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Sarah', 'Chen', 'sarah.chen@acme.com', 'VP Engineering', $3, 'prospect') ON CONFLICT (id) DO NOTHING`, [IDS.CT_SARAH_CHEN, tenantId, IDS.ACCT_ACME]);
|
|
527
|
+
await db.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Marcus', 'Webb', 'marcus.webb@acme.com', 'CFO', $3, 'prospect') ON CONFLICT (id) DO NOTHING`, [IDS.CT_MARCUS_WEBB, tenantId, IDS.ACCT_ACME]);
|
|
528
|
+
await db.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Priya', 'Nair', 'p.nair@brightsidehealth.com', 'CTO', $3, 'active') ON CONFLICT (id) DO NOTHING`, [IDS.CT_PRIYA_NAIR, tenantId, IDS.ACCT_BRIGHTSIDE]);
|
|
529
|
+
await db.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Jordan', 'Liu', 'j.liu@brightsidehealth.com', 'RevOps Lead', $3, 'active') ON CONFLICT (id) DO NOTHING`, [IDS.CT_JORDAN_LIU, tenantId, IDS.ACCT_BRIGHTSIDE]);
|
|
530
|
+
await db.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Tom\xE1s', 'Rivera', 't.rivera@vertex.io', 'Head of Sales Ops', $3, 'champion') ON CONFLICT (id) DO NOTHING`, [IDS.CT_TOMAS_RIVERA, tenantId, IDS.ACCT_VERTEX]);
|
|
531
|
+
await db.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Keiko', 'Yamamoto', 'k.yamamoto@vertex.io', 'CEO', $3, 'champion') ON CONFLICT (id) DO NOTHING`, [IDS.CT_KEIKO_YAMAMOTO, tenantId, IDS.ACCT_VERTEX]);
|
|
532
|
+
await db.query(`INSERT INTO opportunities (id, tenant_id, name, account_id, stage, amount, close_date) VALUES ($1, $2, 'Acme Corp Enterprise Deal', $3, 'Discovery', 180000, '2026-06-30') ON CONFLICT (id) DO NOTHING`, [IDS.OPP_ACME, tenantId, IDS.ACCT_ACME]);
|
|
533
|
+
await db.query(`INSERT INTO opportunities (id, tenant_id, name, account_id, stage, amount, close_date) VALUES ($1, $2, 'Brightside Health Platform Deal', $3, 'PoC', 96000, '2026-05-15') ON CONFLICT (id) DO NOTHING`, [IDS.OPP_BRIGHTSIDE, tenantId, IDS.ACCT_BRIGHTSIDE]);
|
|
534
|
+
await db.query(`INSERT INTO opportunities (id, tenant_id, name, account_id, stage, amount, close_date) VALUES ($1, $2, 'Vertex Logistics Expansion', $3, 'Negotiation', 240000, '2026-04-30') ON CONFLICT (id) DO NOTHING`, [IDS.OPP_VERTEX, tenantId, IDS.ACCT_VERTEX]);
|
|
535
|
+
}
|
|
536
|
+
spinner.succeed("Demo data seeded \x1B[2m(3 accounts, 6 contacts, 3 opportunities)\x1B[0m");
|
|
391
537
|
} catch {
|
|
392
|
-
|
|
538
|
+
spinner.fail("Demo data seeding failed");
|
|
539
|
+
console.log(" \x1B[33m\u26A0\x1B[0m You can run it later with: crmy seed-demo\n");
|
|
393
540
|
}
|
|
394
541
|
}
|
|
542
|
+
await closePool();
|
|
543
|
+
const keyPreview = rawKey.substring(0, 16) + "\u2026";
|
|
395
544
|
console.log("\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
396
545
|
console.log(" \u2502 \x1B[32m\u2713\x1B[0m CRMy is ready! \u2502");
|
|
397
546
|
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n");
|
|
398
547
|
console.log(` Admin account: ${email.trim()}`);
|
|
548
|
+
console.log(` API key: ${keyPreview} \x1B[2m(full key in .crmy.json)\x1B[0m`);
|
|
399
549
|
console.log(" Config saved: .crmy.json \x1B[2m(added to .gitignore)\x1B[0m\n");
|
|
400
550
|
console.log(" Next steps:\n");
|
|
401
551
|
console.log(" Start the server:");
|
|
@@ -494,7 +644,7 @@ function getCLIVersion() {
|
|
|
494
644
|
);
|
|
495
645
|
return pkg.version;
|
|
496
646
|
} catch {
|
|
497
|
-
return "
|
|
647
|
+
return "unknown";
|
|
498
648
|
}
|
|
499
649
|
}
|
|
500
650
|
function printReadyBox(port) {
|
|
@@ -563,6 +713,10 @@ function serverCommand() {
|
|
|
563
713
|
spinner.fail(`${stepLabels[step] ?? step} failed`);
|
|
564
714
|
}
|
|
565
715
|
};
|
|
716
|
+
serverConfig.onMigration = (name, index, total) => {
|
|
717
|
+
spinner.update(`Running migration ${index + 1}/${total}: ${name}\u2026`);
|
|
718
|
+
logToFile(`[MIGRATION] ${index + 1}/${total}: ${name}`);
|
|
719
|
+
};
|
|
566
720
|
let hitlInterval;
|
|
567
721
|
try {
|
|
568
722
|
const result = await createApp(serverConfig);
|
|
@@ -833,8 +987,8 @@ function createHttpClient(serverUrl, token) {
|
|
|
833
987
|
if (!mapping) {
|
|
834
988
|
throw new Error(`Unknown tool: ${toolName} (no REST mapping)`);
|
|
835
989
|
}
|
|
836
|
-
const { method, path:
|
|
837
|
-
const url = `${serverUrl.replace(/\/$/, "")}${
|
|
990
|
+
const { method, path: path7 } = mapping;
|
|
991
|
+
const url = `${serverUrl.replace(/\/$/, "")}${path7(input)}`;
|
|
838
992
|
const headers = {
|
|
839
993
|
"Authorization": `Bearer ${token}`,
|
|
840
994
|
"Content-Type": "application/json"
|
|
@@ -2654,19 +2808,187 @@ Key points: (1) Acme spending 15 hrs/week on manual CRM data entry. (2) Failed S
|
|
|
2654
2808
|
});
|
|
2655
2809
|
}
|
|
2656
2810
|
|
|
2811
|
+
// src/commands/doctor.ts
|
|
2812
|
+
import { Command as Command28 } from "commander";
|
|
2813
|
+
import fs4 from "fs";
|
|
2814
|
+
import path5 from "path";
|
|
2815
|
+
import net from "net";
|
|
2816
|
+
var KNOWN_BAD_SECRETS = ["change-me-in-production", "dev-secret", "secret", ""];
|
|
2817
|
+
function pass(msg) {
|
|
2818
|
+
console.log(` \x1B[32m\u2713\x1B[0m ${msg}`);
|
|
2819
|
+
}
|
|
2820
|
+
function fail(msg, fix) {
|
|
2821
|
+
console.log(` \x1B[31m\u2717\x1B[0m ${msg}`);
|
|
2822
|
+
if (fix) console.log(` \x1B[2m\u2192 ${fix}\x1B[0m`);
|
|
2823
|
+
}
|
|
2824
|
+
function info(msg) {
|
|
2825
|
+
console.log(` \x1B[36m\u2139\x1B[0m ${msg}`);
|
|
2826
|
+
}
|
|
2827
|
+
async function checkPort(port) {
|
|
2828
|
+
return new Promise((resolve) => {
|
|
2829
|
+
const server = net.createServer();
|
|
2830
|
+
server.once("error", () => resolve(false));
|
|
2831
|
+
server.once("listening", () => {
|
|
2832
|
+
server.close(() => resolve(true));
|
|
2833
|
+
});
|
|
2834
|
+
server.listen(port);
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
function doctorCommand() {
|
|
2838
|
+
return new Command28("doctor").description("Check your CRMy setup for common issues").option("--port <port>", "Port to check availability for", "3000").action(async (opts) => {
|
|
2839
|
+
console.log("\n CRMy Doctor\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
|
|
2840
|
+
let passed = 0;
|
|
2841
|
+
let failed = 0;
|
|
2842
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
2843
|
+
if (major >= 20) {
|
|
2844
|
+
pass(`Node.js ${process.version}`);
|
|
2845
|
+
passed++;
|
|
2846
|
+
} else {
|
|
2847
|
+
fail(`Node.js ${process.version} \u2014 CRMy requires >= 20.0.0`, "Install from https://nodejs.org");
|
|
2848
|
+
failed++;
|
|
2849
|
+
}
|
|
2850
|
+
const localPath = path5.join(process.cwd(), ".crmy.json");
|
|
2851
|
+
const hasLocal = fs4.existsSync(localPath);
|
|
2852
|
+
const hasGlobal = fs4.existsSync(GLOBAL_CONFIG);
|
|
2853
|
+
let config = {};
|
|
2854
|
+
if (hasLocal || hasGlobal) {
|
|
2855
|
+
try {
|
|
2856
|
+
config = loadConfigFile();
|
|
2857
|
+
const source = hasLocal ? ".crmy.json (local)" : "~/.crmy/config.json (global)";
|
|
2858
|
+
pass(`Config found: ${source}`);
|
|
2859
|
+
passed++;
|
|
2860
|
+
} catch {
|
|
2861
|
+
fail("Config file exists but is not valid JSON", "Run: crmy init");
|
|
2862
|
+
failed++;
|
|
2863
|
+
}
|
|
2864
|
+
} else {
|
|
2865
|
+
fail("No config file found", "Run: crmy init");
|
|
2866
|
+
failed++;
|
|
2867
|
+
}
|
|
2868
|
+
const dbUrl = config.database?.url ?? process.env.DATABASE_URL;
|
|
2869
|
+
if (dbUrl) {
|
|
2870
|
+
try {
|
|
2871
|
+
process.env.CRMY_IMPORTED = "1";
|
|
2872
|
+
const { initPool, closePool } = await import("@crmy/server");
|
|
2873
|
+
const db = await initPool(dbUrl, 2);
|
|
2874
|
+
await db.query("SELECT 1");
|
|
2875
|
+
pass("PostgreSQL reachable");
|
|
2876
|
+
passed++;
|
|
2877
|
+
try {
|
|
2878
|
+
const { getMigrationStatus } = await import("@crmy/server/db/migrate");
|
|
2879
|
+
const status = await getMigrationStatus(db);
|
|
2880
|
+
if (status.pending.length === 0) {
|
|
2881
|
+
pass(`Migrations up to date (${status.applied.length} applied)`);
|
|
2882
|
+
passed++;
|
|
2883
|
+
} else {
|
|
2884
|
+
const realPending = status.pending.filter(
|
|
2885
|
+
(f) => f !== "022_pgvector.sql" || process.env.ENABLE_PGVECTOR === "true"
|
|
2886
|
+
);
|
|
2887
|
+
if (realPending.length === 0) {
|
|
2888
|
+
pass(`Migrations up to date (${status.applied.length} applied, pgvector optional)`);
|
|
2889
|
+
passed++;
|
|
2890
|
+
} else {
|
|
2891
|
+
fail(`${realPending.length} pending migration(s)`, "Run: crmy migrate");
|
|
2892
|
+
failed++;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
} catch {
|
|
2896
|
+
try {
|
|
2897
|
+
const applied = await db.query("SELECT name FROM _migrations ORDER BY name");
|
|
2898
|
+
pass(`Migrations table exists (${applied.rows.length} applied)`);
|
|
2899
|
+
passed++;
|
|
2900
|
+
} catch {
|
|
2901
|
+
fail("Migrations table not found", "Run: crmy init");
|
|
2902
|
+
failed++;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
try {
|
|
2906
|
+
const users = await db.query("SELECT id FROM users LIMIT 1");
|
|
2907
|
+
if (users.rows.length > 0) {
|
|
2908
|
+
pass("Admin user exists");
|
|
2909
|
+
passed++;
|
|
2910
|
+
} else {
|
|
2911
|
+
fail("No users found", "Run: crmy init (or POST /auth/register)");
|
|
2912
|
+
failed++;
|
|
2913
|
+
}
|
|
2914
|
+
} catch {
|
|
2915
|
+
fail("Could not check users table", "Run: crmy init");
|
|
2916
|
+
failed++;
|
|
2917
|
+
}
|
|
2918
|
+
try {
|
|
2919
|
+
const pgv = await db.query("SELECT 1 FROM pg_extension WHERE extname = 'vector'");
|
|
2920
|
+
if (pgv.rows.length > 0) {
|
|
2921
|
+
pass("pgvector extension installed (semantic search available)");
|
|
2922
|
+
passed++;
|
|
2923
|
+
} else {
|
|
2924
|
+
info("pgvector not installed (semantic search disabled \u2014 this is optional)");
|
|
2925
|
+
}
|
|
2926
|
+
} catch {
|
|
2927
|
+
info("Could not check pgvector status");
|
|
2928
|
+
}
|
|
2929
|
+
await closePool();
|
|
2930
|
+
} catch (err) {
|
|
2931
|
+
fail(`PostgreSQL unreachable: ${err.message}`, "Check that PostgreSQL is running and DATABASE_URL is correct");
|
|
2932
|
+
failed++;
|
|
2933
|
+
}
|
|
2934
|
+
} else {
|
|
2935
|
+
fail("No DATABASE_URL configured", "Run: crmy init");
|
|
2936
|
+
failed++;
|
|
2937
|
+
}
|
|
2938
|
+
const port = parseInt(opts.port, 10);
|
|
2939
|
+
const portAvailable = await checkPort(port);
|
|
2940
|
+
if (portAvailable) {
|
|
2941
|
+
pass(`Port ${port} is available`);
|
|
2942
|
+
passed++;
|
|
2943
|
+
} else {
|
|
2944
|
+
fail(`Port ${port} is in use`, `Try: crmy server --port ${port + 1}`);
|
|
2945
|
+
failed++;
|
|
2946
|
+
}
|
|
2947
|
+
const jwt = config.jwtSecret ?? process.env.JWT_SECRET;
|
|
2948
|
+
if (jwt && !KNOWN_BAD_SECRETS.includes(jwt)) {
|
|
2949
|
+
pass("JWT_SECRET is set and not a known default");
|
|
2950
|
+
passed++;
|
|
2951
|
+
} else if (jwt && KNOWN_BAD_SECRETS.includes(jwt)) {
|
|
2952
|
+
fail("JWT_SECRET is a known default value", "Generate a new one: openssl rand -hex 32");
|
|
2953
|
+
failed++;
|
|
2954
|
+
} else {
|
|
2955
|
+
fail("JWT_SECRET not configured", "Run: crmy init");
|
|
2956
|
+
failed++;
|
|
2957
|
+
}
|
|
2958
|
+
console.log("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2959
|
+
if (failed === 0) {
|
|
2960
|
+
console.log(` \x1B[32m${passed} checks passed\x1B[0m \u2014 everything looks good!
|
|
2961
|
+
`);
|
|
2962
|
+
} else {
|
|
2963
|
+
console.log(` ${passed} passed, \x1B[31m${failed} issue(s) found\x1B[0m
|
|
2964
|
+
`);
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2657
2969
|
// src/index.ts
|
|
2970
|
+
var [_nodeMajor] = process.versions.node.split(".").map(Number);
|
|
2971
|
+
if (_nodeMajor < 20) {
|
|
2972
|
+
console.error(
|
|
2973
|
+
`
|
|
2974
|
+
CRMy requires Node.js >= 20.0.0 (you have ${process.version})
|
|
2975
|
+
Install the latest LTS: https://nodejs.org
|
|
2976
|
+
`
|
|
2977
|
+
);
|
|
2978
|
+
process.exit(1);
|
|
2979
|
+
}
|
|
2658
2980
|
var _require2 = createRequire2(import.meta.url);
|
|
2659
2981
|
function getCLIVersion2() {
|
|
2660
2982
|
try {
|
|
2661
2983
|
const pkg = _require2(
|
|
2662
|
-
|
|
2984
|
+
path6.resolve(path6.dirname(fileURLToPath2(import.meta.url)), "../package.json")
|
|
2663
2985
|
);
|
|
2664
2986
|
return pkg.version;
|
|
2665
2987
|
} catch {
|
|
2666
|
-
return "
|
|
2988
|
+
return "unknown";
|
|
2667
2989
|
}
|
|
2668
2990
|
}
|
|
2669
|
-
var program = new
|
|
2991
|
+
var program = new Command29();
|
|
2670
2992
|
program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version(getCLIVersion2());
|
|
2671
2993
|
program.addCommand(authCommand());
|
|
2672
2994
|
program.addCommand(initCommand());
|
|
@@ -2694,6 +3016,7 @@ program.addCommand(activityTypesCommand());
|
|
|
2694
3016
|
program.addCommand(contextTypesCommand());
|
|
2695
3017
|
program.addCommand(briefingCommand());
|
|
2696
3018
|
program.addCommand(seedDemoCommand());
|
|
3019
|
+
program.addCommand(doctorCommand());
|
|
2697
3020
|
program.addCommand(helpCommand());
|
|
2698
3021
|
program.command("login").description("Sign in to a CRMy server (shortcut for `crmy auth login`)").option("-e, --email <email>", "Email address").option("-p, --password <password>", "Password").action(async (opts) => {
|
|
2699
3022
|
const args = ["auth", "login"];
|