@crmy/cli 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +427 -54
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,67 +2,225 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command as Command27 } from "commander";
|
|
5
|
+
import { createRequire as createRequire2 } from "module";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
+
import path5 from "path";
|
|
5
8
|
|
|
6
9
|
// src/commands/init.ts
|
|
7
10
|
import { Command } from "commander";
|
|
8
11
|
import crypto from "crypto";
|
|
9
12
|
import fs from "fs";
|
|
10
13
|
import path from "path";
|
|
14
|
+
|
|
15
|
+
// src/spinner.ts
|
|
16
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
17
|
+
function createSpinner(initialMessage) {
|
|
18
|
+
if (!process.stdout.isTTY) {
|
|
19
|
+
if (initialMessage) console.log(` ... ${initialMessage}`);
|
|
20
|
+
return {
|
|
21
|
+
update(message) {
|
|
22
|
+
if (message) console.log(` ... ${message}`);
|
|
23
|
+
},
|
|
24
|
+
succeed(message) {
|
|
25
|
+
console.log(` \u2713 ${message}`);
|
|
26
|
+
},
|
|
27
|
+
fail(message) {
|
|
28
|
+
console.log(` \u2717 ${message}`);
|
|
29
|
+
},
|
|
30
|
+
stop() {
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
let frame = 0;
|
|
35
|
+
let current = initialMessage;
|
|
36
|
+
let stopped = false;
|
|
37
|
+
const cols = () => process.stdout.columns ?? 80;
|
|
38
|
+
const interval = setInterval(() => {
|
|
39
|
+
if (stopped) return;
|
|
40
|
+
const symbol = FRAMES[frame % FRAMES.length];
|
|
41
|
+
const line = ` ${symbol} ${current}`;
|
|
42
|
+
process.stdout.write(`\r${line.padEnd(cols())}`);
|
|
43
|
+
frame++;
|
|
44
|
+
}, 80);
|
|
45
|
+
const clearLine = () => {
|
|
46
|
+
process.stdout.write(`\r${" ".repeat(cols())}\r`);
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
update(message) {
|
|
50
|
+
current = message;
|
|
51
|
+
},
|
|
52
|
+
succeed(message) {
|
|
53
|
+
stopped = true;
|
|
54
|
+
clearInterval(interval);
|
|
55
|
+
clearLine();
|
|
56
|
+
console.log(` \x1B[32m\u2713\x1B[0m ${message}`);
|
|
57
|
+
},
|
|
58
|
+
fail(message) {
|
|
59
|
+
stopped = true;
|
|
60
|
+
clearInterval(interval);
|
|
61
|
+
clearLine();
|
|
62
|
+
console.log(` \x1B[31m\u2717\x1B[0m ${message}`);
|
|
63
|
+
},
|
|
64
|
+
stop() {
|
|
65
|
+
stopped = true;
|
|
66
|
+
clearInterval(interval);
|
|
67
|
+
clearLine();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/commands/init.ts
|
|
73
|
+
function validateEmail(input) {
|
|
74
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.trim()) ? true : "Please enter a valid email address (e.g. you@example.com)";
|
|
75
|
+
}
|
|
76
|
+
function validatePassword(input) {
|
|
77
|
+
return input.length >= 8 ? true : "Password must be at least 8 characters";
|
|
78
|
+
}
|
|
11
79
|
function initCommand() {
|
|
12
|
-
return new Command("init").description("
|
|
80
|
+
return new Command("init").description("Interactive setup wizard: database, migrations, admin account").action(async () => {
|
|
13
81
|
const { default: inquirer } = await import("inquirer");
|
|
14
|
-
console.log("\n
|
|
15
|
-
|
|
82
|
+
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");
|
|
83
|
+
console.log(" \u2502 crmy.ai \u2014 Setup Wizard \u2502");
|
|
84
|
+
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\u2518\n");
|
|
85
|
+
console.log(" This wizard will:\n");
|
|
86
|
+
console.log(" Step 1 \u2014 Connect to your PostgreSQL database");
|
|
87
|
+
console.log(" Step 2 \u2014 Create all CRMy tables (migrations)");
|
|
88
|
+
console.log(" Step 3 \u2014 Create your admin account\n");
|
|
89
|
+
const configPath = path.join(process.cwd(), ".crmy.json");
|
|
90
|
+
if (fs.existsSync(configPath)) {
|
|
91
|
+
console.log(" \x1B[33m\u26A0\x1B[0m A .crmy.json already exists in this directory.\n");
|
|
92
|
+
const { overwrite } = await inquirer.prompt([
|
|
93
|
+
{
|
|
94
|
+
type: "confirm",
|
|
95
|
+
name: "overwrite",
|
|
96
|
+
message: " Overwrite it and run setup again?",
|
|
97
|
+
default: false
|
|
98
|
+
}
|
|
99
|
+
]);
|
|
100
|
+
if (!overwrite) {
|
|
101
|
+
console.log("\n Setup cancelled. Your existing config was not changed.\n");
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
console.log("");
|
|
105
|
+
}
|
|
106
|
+
console.log(" \u2500\u2500 Step 1 of 3: Database Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
107
|
+
console.log(" Enter your PostgreSQL connection string.");
|
|
108
|
+
console.log(" Format: postgresql://user:password@host:5432/dbname\n");
|
|
109
|
+
console.log(" Options:");
|
|
110
|
+
console.log(" \u2022 Local install: postgresql://localhost:5432/crmy");
|
|
111
|
+
console.log(" \u2022 Docker: postgresql://postgres:postgres@localhost:5432/crmy");
|
|
112
|
+
console.log(" \u2022 Supabase: Project Settings \u2192 Database \u2192 Connection String");
|
|
113
|
+
console.log(" \u2022 Neon: Dashboard \u2192 Connection Details\n");
|
|
114
|
+
console.log(" \x1B[2mNote: the wizard will retry the connection up to 5 times.\x1B[0m\n");
|
|
115
|
+
const { databaseUrl } = await inquirer.prompt([
|
|
16
116
|
{
|
|
17
117
|
type: "input",
|
|
18
118
|
name: "databaseUrl",
|
|
19
|
-
message: "PostgreSQL
|
|
119
|
+
message: " PostgreSQL connection string:",
|
|
20
120
|
default: "postgresql://localhost:5432/crmy"
|
|
21
|
-
}
|
|
121
|
+
}
|
|
122
|
+
]);
|
|
123
|
+
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");
|
|
124
|
+
process.env.CRMY_IMPORTED = "1";
|
|
125
|
+
const { initPool, closePool, runMigrations } = await import("@crmy/server");
|
|
126
|
+
let spinner = createSpinner("Connecting to database\u2026");
|
|
127
|
+
let db;
|
|
128
|
+
try {
|
|
129
|
+
db = await initPool(databaseUrl);
|
|
130
|
+
spinner.succeed("Connected to database");
|
|
131
|
+
} catch (err) {
|
|
132
|
+
spinner.fail("Database connection failed");
|
|
133
|
+
const msg = err.message ?? String(err);
|
|
134
|
+
console.error(
|
|
135
|
+
`
|
|
136
|
+
Error: ${msg}
|
|
137
|
+
|
|
138
|
+
Common causes:
|
|
139
|
+
\u2022 PostgreSQL is not running
|
|
140
|
+
\u2022 Wrong host, port, or database name in the URL
|
|
141
|
+
\u2022 Wrong username or password
|
|
142
|
+
\u2022 Database does not exist \u2014 create it with: createdb crmy
|
|
143
|
+
`
|
|
144
|
+
);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
spinner = createSpinner("Running database migrations\u2026");
|
|
148
|
+
let ran = [];
|
|
149
|
+
try {
|
|
150
|
+
ran = await runMigrations(db);
|
|
151
|
+
if (ran.length > 0) {
|
|
152
|
+
spinner.succeed(`Migrations complete \x1B[2m(${ran.length} applied)\x1B[0m`);
|
|
153
|
+
} else {
|
|
154
|
+
spinner.succeed("Migrations complete \x1B[2m(already up to date)\x1B[0m");
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
spinner.fail("Migration failed");
|
|
158
|
+
const msg = err.message ?? String(err);
|
|
159
|
+
console.error(
|
|
160
|
+
`
|
|
161
|
+
SQL error: ${msg}
|
|
162
|
+
|
|
163
|
+
This may mean:
|
|
164
|
+
\u2022 The database user lacks CREATE TABLE permissions
|
|
165
|
+
\u2022 A previous partial migration left the schema in a bad state
|
|
166
|
+
Try: drop and recreate the database, then run init again
|
|
167
|
+
`
|
|
168
|
+
);
|
|
169
|
+
await closePool();
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
spinner = createSpinner("Seeding default tenant\u2026");
|
|
173
|
+
let tenantId;
|
|
174
|
+
try {
|
|
175
|
+
const result = await db.query(
|
|
176
|
+
`INSERT INTO tenants (slug, name) VALUES ('default', 'Default Tenant')
|
|
177
|
+
ON CONFLICT (slug) DO UPDATE SET name = 'Default Tenant'
|
|
178
|
+
RETURNING id`
|
|
179
|
+
);
|
|
180
|
+
tenantId = result.rows[0].id;
|
|
181
|
+
spinner.succeed("Default tenant ready");
|
|
182
|
+
} catch (err) {
|
|
183
|
+
spinner.fail("Failed to seed tenant");
|
|
184
|
+
console.error(`
|
|
185
|
+
Error: ${err.message}
|
|
186
|
+
`);
|
|
187
|
+
await closePool();
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
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");
|
|
191
|
+
console.log(" Create the first admin account for the CRMy web UI and CLI.\n");
|
|
192
|
+
console.log(
|
|
193
|
+
" \x1B[33mNOTE:\x1B[0m These are your \x1B[1mCRMy login credentials\x1B[0m \u2014 NOT your database credentials.\n"
|
|
194
|
+
);
|
|
195
|
+
const { name, email, password } = await inquirer.prompt([
|
|
22
196
|
{
|
|
23
197
|
type: "input",
|
|
24
198
|
name: "name",
|
|
25
|
-
message: "Your name
|
|
199
|
+
message: " Your full name:"
|
|
26
200
|
},
|
|
27
201
|
{
|
|
28
202
|
type: "input",
|
|
29
203
|
name: "email",
|
|
30
|
-
message: "
|
|
204
|
+
message: " Email address (used to log in):",
|
|
205
|
+
validate: validateEmail
|
|
31
206
|
},
|
|
32
207
|
{
|
|
33
208
|
type: "password",
|
|
34
209
|
name: "password",
|
|
35
|
-
message: "Password
|
|
36
|
-
mask: "*"
|
|
210
|
+
message: " Password (min 8 characters):",
|
|
211
|
+
mask: "*",
|
|
212
|
+
validate: validatePassword
|
|
37
213
|
}
|
|
38
214
|
]);
|
|
39
|
-
|
|
215
|
+
spinner = createSpinner("Creating admin account\u2026");
|
|
40
216
|
try {
|
|
41
|
-
const
|
|
42
|
-
const { runMigrations } = await import("@crmy/server");
|
|
43
|
-
console.log("\nConnecting to database...");
|
|
44
|
-
const db = await initPool(answers.databaseUrl);
|
|
45
|
-
console.log("Connected.");
|
|
46
|
-
console.log("Running migrations...");
|
|
47
|
-
const ran = await runMigrations(db);
|
|
48
|
-
if (ran.length > 0) {
|
|
49
|
-
console.log(` Ran ${ran.length} migration(s): ${ran.join(", ")}`);
|
|
50
|
-
} else {
|
|
51
|
-
console.log(" No pending migrations.");
|
|
52
|
-
}
|
|
53
|
-
const tenantResult = await db.query(
|
|
54
|
-
`INSERT INTO tenants (slug, name) VALUES ('default', 'Default Tenant')
|
|
55
|
-
ON CONFLICT (slug) DO UPDATE SET name = 'Default Tenant'
|
|
56
|
-
RETURNING id`
|
|
57
|
-
);
|
|
58
|
-
const tenantId = tenantResult.rows[0].id;
|
|
59
|
-
const passwordHash = crypto.createHash("sha256").update(answers.password).digest("hex");
|
|
217
|
+
const passwordHash = crypto.createHash("sha256").update(password).digest("hex");
|
|
60
218
|
const userResult = await db.query(
|
|
61
219
|
`INSERT INTO users (tenant_id, email, name, role, password_hash)
|
|
62
220
|
VALUES ($1, $2, $3, 'owner', $4)
|
|
63
221
|
ON CONFLICT (tenant_id, email) DO UPDATE SET name = $3, password_hash = $4
|
|
64
222
|
RETURNING id`,
|
|
65
|
-
[tenantId,
|
|
223
|
+
[tenantId, email.trim(), name, passwordHash]
|
|
66
224
|
);
|
|
67
225
|
const userId = userResult.rows[0].id;
|
|
68
226
|
const rawKey = "crmy_" + crypto.randomBytes(32).toString("hex");
|
|
@@ -72,42 +230,52 @@ function initCommand() {
|
|
|
72
230
|
VALUES ($1, $2, $3, 'default', '{read,write,admin}')`,
|
|
73
231
|
[tenantId, userId, keyHash]
|
|
74
232
|
);
|
|
233
|
+
spinner.succeed("Admin account created");
|
|
75
234
|
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
76
|
-
const
|
|
235
|
+
const crmmyConfig = {
|
|
77
236
|
serverUrl: "http://localhost:3000",
|
|
78
237
|
apiKey: rawKey,
|
|
79
238
|
tenantId: "default",
|
|
80
|
-
database: {
|
|
81
|
-
url: answers.databaseUrl
|
|
82
|
-
},
|
|
239
|
+
database: { url: databaseUrl },
|
|
83
240
|
jwtSecret,
|
|
84
241
|
hitl: {
|
|
85
242
|
requireApproval: ["bulk_update", "bulk_delete", "send_email"],
|
|
86
243
|
autoApproveSeconds: 0
|
|
87
244
|
}
|
|
88
245
|
};
|
|
89
|
-
|
|
90
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
246
|
+
fs.writeFileSync(configPath, JSON.stringify(crmmyConfig, null, 2) + "\n");
|
|
91
247
|
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
92
248
|
const gitignoreContent = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : "";
|
|
93
249
|
if (!gitignoreContent.includes(".crmy.json")) {
|
|
94
250
|
fs.appendFileSync(gitignorePath, "\n.crmy.json\n");
|
|
95
251
|
}
|
|
96
|
-
await closePool();
|
|
97
|
-
console.log("\n \u2713 crmy.ai initialized\n");
|
|
98
|
-
console.log(" Add to Claude Code:");
|
|
99
|
-
console.log(" claude mcp add crmy -- npx @crmy/cli mcp\n");
|
|
100
|
-
console.log(" Or start the server:");
|
|
101
|
-
console.log(" npx @crmy/cli server\n");
|
|
102
252
|
} catch (err) {
|
|
103
|
-
|
|
253
|
+
spinner.fail("Failed to create admin account");
|
|
254
|
+
console.error(`
|
|
255
|
+
Error: ${err.message}
|
|
256
|
+
`);
|
|
257
|
+
await closePool();
|
|
104
258
|
process.exit(1);
|
|
105
259
|
}
|
|
260
|
+
await closePool();
|
|
261
|
+
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");
|
|
262
|
+
console.log(" \u2502 \x1B[32m\u2713\x1B[0m CRMy is ready! \u2502");
|
|
263
|
+
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");
|
|
264
|
+
console.log(` Admin account: ${email.trim()}`);
|
|
265
|
+
console.log(" Config saved: .crmy.json \x1B[2m(added to .gitignore)\x1B[0m\n");
|
|
266
|
+
console.log(" Next steps:\n");
|
|
267
|
+
console.log(" Start the server:");
|
|
268
|
+
console.log(" \x1B[1mnpx @crmy/cli server\x1B[0m\n");
|
|
269
|
+
console.log(" Connect to Claude Code:");
|
|
270
|
+
console.log(" \x1B[1mclaude mcp add crmy -- npx @crmy/cli mcp\x1B[0m\n");
|
|
106
271
|
});
|
|
107
272
|
}
|
|
108
273
|
|
|
109
274
|
// src/commands/server.ts
|
|
110
275
|
import { Command as Command2 } from "commander";
|
|
276
|
+
import { createRequire } from "module";
|
|
277
|
+
import { fileURLToPath } from "url";
|
|
278
|
+
import path4 from "path";
|
|
111
279
|
|
|
112
280
|
// src/config.ts
|
|
113
281
|
import fs2 from "fs";
|
|
@@ -150,24 +318,218 @@ function resolveServerUrl() {
|
|
|
150
318
|
return process.env.CRMY_SERVER_URL ?? loadAuthState()?.serverUrl ?? loadConfigFile().serverUrl;
|
|
151
319
|
}
|
|
152
320
|
|
|
321
|
+
// src/banner.ts
|
|
322
|
+
var ASCII_ART = `
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
##*++++*###
|
|
326
|
+
#+-:.......:--=+#
|
|
327
|
+
*+-:::----------:--==*#
|
|
328
|
+
*-:-----------::--::=++=*
|
|
329
|
+
#=:-::-==--------------+++=#
|
|
330
|
+
*=..:--==-=-----==------+++==#
|
|
331
|
+
#-..:--==-=:----====----=+*++=#
|
|
332
|
+
*::.:-===-=-----====-:--=+++++=#
|
|
333
|
+
+-:.:-===-=---=-====-:-==++++++=#
|
|
334
|
+
*:-:--=====--=-======:-==++++++=#*
|
|
335
|
+
#+:::--====-------====:-==++++++=+*
|
|
336
|
+
*=:::--====-:----=====--==+++++++=*
|
|
337
|
+
#=:::-======:-========-:===++++++=*#
|
|
338
|
+
#-::--======:-==---===-:===++++++=+#
|
|
339
|
+
*----:-=====--:.......--===*+++++==#*
|
|
340
|
+
*--:...:==-:...........:===+++++**+**
|
|
341
|
+
*......:................:==-=++***++#
|
|
342
|
+
#=......:..:........:......---++**++++*
|
|
343
|
+
#=......::..................:-==+**+++**
|
|
344
|
+
#=.....:::::................:-==-+*+++*+*
|
|
345
|
+
*-......::::.......:........:-==--++++#==#
|
|
346
|
+
*-........:..........:...:...:-=-:-++=*#*+#
|
|
347
|
+
*:.........................:::-==----=+*
|
|
348
|
+
*:.......:....................::=-------+
|
|
349
|
+
*-............................::-----=#*-+
|
|
350
|
+
*-::......-=:.:++-...:-......::==----*##=+
|
|
351
|
+
***=.=*--**+++#*+.:**++:::=+=::----=***-+
|
|
352
|
+
*=++-**++++**+*+=.-**+==-+*+*-.----+*++.=*
|
|
353
|
+
#=-++-***==**#*===++=*##**+==*=.--=**+++:=#
|
|
354
|
+
#*+#+-# ## #=-----+###++*-=*=.-+***++#*+
|
|
355
|
+
+-:# #-:----=### ###-=#+.-***+**
|
|
356
|
+
+..=# #=:-----*## #-=#=.:=#
|
|
357
|
+
=..-# +:-----+*% #:-#-::=+
|
|
358
|
+
*+=*+ *:-----=*## #:-#*==+*
|
|
359
|
+
#*:-----=**# #::#
|
|
360
|
+
*-------+*# *::#
|
|
361
|
+
#=------+*## +.:*
|
|
362
|
+
#*-----+*## #-..+
|
|
363
|
+
%#***#% +:..-#
|
|
364
|
+
=:..-+
|
|
365
|
+
+:..=*
|
|
366
|
+
#*=+*#
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
`;
|
|
370
|
+
function printBanner(version) {
|
|
371
|
+
console.log(ASCII_ART);
|
|
372
|
+
console.log(` CRMy v${version} \x1B[2m\u2014\x1B[0m Agent-first open source CRM
|
|
373
|
+
`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/logger.ts
|
|
377
|
+
import fs3 from "fs";
|
|
378
|
+
import path3 from "path";
|
|
379
|
+
import os2 from "os";
|
|
380
|
+
var LOG_DIR = path3.join(os2.homedir(), ".crmy");
|
|
381
|
+
var LOG_FILE = path3.join(LOG_DIR, "crmy-server.log");
|
|
382
|
+
function ensureLogDir() {
|
|
383
|
+
fs3.mkdirSync(LOG_DIR, { recursive: true });
|
|
384
|
+
}
|
|
385
|
+
function logToFile(line) {
|
|
386
|
+
try {
|
|
387
|
+
ensureLogDir();
|
|
388
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
389
|
+
fs3.appendFileSync(LOG_FILE, `[${ts}] ${line}
|
|
390
|
+
`);
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
153
395
|
// src/commands/server.ts
|
|
396
|
+
var _require = createRequire(import.meta.url);
|
|
397
|
+
function getCLIVersion() {
|
|
398
|
+
try {
|
|
399
|
+
const pkg = _require(
|
|
400
|
+
path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "../../package.json")
|
|
401
|
+
);
|
|
402
|
+
return pkg.version;
|
|
403
|
+
} catch {
|
|
404
|
+
return "0.5.5";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function printReadyBox(port) {
|
|
408
|
+
const lines = [
|
|
409
|
+
` Web UI \u2192 http://localhost:${port}/app`,
|
|
410
|
+
` API \u2192 http://localhost:${port}/api/v1`,
|
|
411
|
+
` MCP \u2192 http://localhost:${port}/mcp`,
|
|
412
|
+
` Health \u2192 http://localhost:${port}/health`
|
|
413
|
+
];
|
|
414
|
+
const innerWidth = Math.max(...lines.map((l) => l.length)) + 2;
|
|
415
|
+
const bar = "\u2500".repeat(innerWidth);
|
|
416
|
+
console.log(`
|
|
417
|
+
\u250C${bar}\u2510`);
|
|
418
|
+
for (const line of lines) {
|
|
419
|
+
console.log(` \u2502${line.padEnd(innerWidth)}\u2502`);
|
|
420
|
+
}
|
|
421
|
+
console.log(` \u2514${bar}\u2518
|
|
422
|
+
`);
|
|
423
|
+
console.log(` Log file: ${LOG_FILE}`);
|
|
424
|
+
console.log(" Press Ctrl+C to stop\n");
|
|
425
|
+
}
|
|
154
426
|
function serverCommand() {
|
|
155
427
|
return new Command2("server").description("Start the crmy HTTP server").option("--port <port>", "HTTP port", "3000").action(async (opts) => {
|
|
156
|
-
const
|
|
428
|
+
const version = getCLIVersion();
|
|
429
|
+
const port = parseInt(opts.port, 10);
|
|
430
|
+
printBanner(version);
|
|
431
|
+
logToFile(`=== CRMy server starting (v${version}) ===`);
|
|
432
|
+
let config;
|
|
433
|
+
try {
|
|
434
|
+
config = loadConfigFile();
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error(
|
|
437
|
+
"\n No .crmy.json found in the current directory.\n Run `npx @crmy/cli init` to set up CRMy first.\n"
|
|
438
|
+
);
|
|
439
|
+
logToFile("ERROR: .crmy.json not found");
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
157
442
|
process.env.DATABASE_URL = config.database?.url ?? process.env.DATABASE_URL;
|
|
158
443
|
process.env.JWT_SECRET = config.jwtSecret ?? process.env.JWT_SECRET ?? "dev-secret";
|
|
159
|
-
process.env.PORT =
|
|
444
|
+
process.env.PORT = String(port);
|
|
160
445
|
process.env.CRMY_IMPORTED = "1";
|
|
161
446
|
if (!process.env.DATABASE_URL) {
|
|
162
|
-
console.error(
|
|
447
|
+
console.error(
|
|
448
|
+
"\n No database URL configured.\n Run `npx @crmy/cli init` first, or set the DATABASE_URL environment variable.\n"
|
|
449
|
+
);
|
|
450
|
+
logToFile("ERROR: No DATABASE_URL configured");
|
|
163
451
|
process.exit(1);
|
|
164
452
|
}
|
|
165
|
-
const { createApp, loadConfig } = await import("@crmy/server");
|
|
453
|
+
const { createApp, loadConfig, closePool, shutdownPlugins } = await import("@crmy/server");
|
|
166
454
|
const serverConfig = loadConfig();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
455
|
+
let spinner = createSpinner("");
|
|
456
|
+
const stepLabels = {
|
|
457
|
+
db_connect: "Connecting to database",
|
|
458
|
+
migrations: "Running database migrations",
|
|
459
|
+
seed_defaults: "Seeding defaults"
|
|
460
|
+
};
|
|
461
|
+
serverConfig.onProgress = (step, status, detail) => {
|
|
462
|
+
logToFile(`[${status.toUpperCase()}] ${step}${detail ? ": " + detail : ""}`);
|
|
463
|
+
if (status === "start") {
|
|
464
|
+
spinner = createSpinner(stepLabels[step] ?? step);
|
|
465
|
+
} else if (status === "done") {
|
|
466
|
+
const label = stepLabels[step] ?? step;
|
|
467
|
+
const suffix = detail ? ` \x1B[2m(${detail})\x1B[0m` : "";
|
|
468
|
+
spinner.succeed(`${label}${suffix}`);
|
|
469
|
+
} else if (status === "error") {
|
|
470
|
+
spinner.fail(`${stepLabels[step] ?? step} failed`);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
let hitlInterval;
|
|
474
|
+
try {
|
|
475
|
+
const result = await createApp(serverConfig);
|
|
476
|
+
hitlInterval = result.hitlInterval;
|
|
477
|
+
const app = result.app;
|
|
478
|
+
spinner = createSpinner(`Starting HTTP server on port ${port}`);
|
|
479
|
+
logToFile(`[START] bind_port: ${port}`);
|
|
480
|
+
await new Promise((resolve, reject) => {
|
|
481
|
+
const srv = app.listen(port, () => {
|
|
482
|
+
spinner.succeed(`Server listening on port ${port}`);
|
|
483
|
+
logToFile(`[DONE] bind_port: ${port}`);
|
|
484
|
+
resolve();
|
|
485
|
+
});
|
|
486
|
+
srv.on("error", reject);
|
|
487
|
+
});
|
|
488
|
+
printReadyBox(port);
|
|
489
|
+
const shutdown = async () => {
|
|
490
|
+
console.log("\n Shutting down...");
|
|
491
|
+
logToFile("Received shutdown signal");
|
|
492
|
+
if (hitlInterval) clearInterval(hitlInterval);
|
|
493
|
+
await shutdownPlugins?.();
|
|
494
|
+
await closePool();
|
|
495
|
+
process.exit(0);
|
|
496
|
+
};
|
|
497
|
+
process.on("SIGTERM", shutdown);
|
|
498
|
+
process.on("SIGINT", shutdown);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
const error = err;
|
|
501
|
+
spinner.stop();
|
|
502
|
+
if (error.code === "EADDRINUSE") {
|
|
503
|
+
console.error(
|
|
504
|
+
`
|
|
505
|
+
Port ${port} is already in use.
|
|
506
|
+
Try a different port:
|
|
507
|
+
npx @crmy/cli server --port ${port + 1}
|
|
508
|
+
`
|
|
509
|
+
);
|
|
510
|
+
logToFile(`ERROR: EADDRINUSE on port ${port}`);
|
|
511
|
+
} else if (error.message?.includes("ECONNREFUSED") || error.message?.includes("password authentication") || error.message?.includes("SCRAM") || error.message?.includes("connect") || error.message?.includes("database")) {
|
|
512
|
+
console.error(
|
|
513
|
+
`
|
|
514
|
+
Could not connect to PostgreSQL.
|
|
515
|
+
Error: ${error.message}
|
|
516
|
+
|
|
517
|
+
Check that:
|
|
518
|
+
\u2022 PostgreSQL is running
|
|
519
|
+
\u2022 The DATABASE_URL in .crmy.json is correct
|
|
520
|
+
\u2022 The database exists (create it: createdb crmy)
|
|
521
|
+
\u2022 The database user has the right permissions
|
|
522
|
+
`
|
|
523
|
+
);
|
|
524
|
+
logToFile(`ERROR: DB connection: ${error.message}`);
|
|
525
|
+
} else {
|
|
526
|
+
console.error(`
|
|
527
|
+
Server failed to start: ${error.message}
|
|
528
|
+
`);
|
|
529
|
+
logToFile(`ERROR: ${error.message}`);
|
|
530
|
+
}
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
171
533
|
});
|
|
172
534
|
}
|
|
173
535
|
|
|
@@ -354,8 +716,8 @@ function createHttpClient(serverUrl, token) {
|
|
|
354
716
|
if (!mapping) {
|
|
355
717
|
throw new Error(`Unknown tool: ${toolName} (no REST mapping)`);
|
|
356
718
|
}
|
|
357
|
-
const { method, path:
|
|
358
|
-
const url = `${serverUrl.replace(/\/$/, "")}${
|
|
719
|
+
const { method, path: path6 } = mapping;
|
|
720
|
+
const url = `${serverUrl.replace(/\/$/, "")}${path6(input)}`;
|
|
359
721
|
const headers = {
|
|
360
722
|
"Authorization": `Bearer ${token}`,
|
|
361
723
|
"Content-Type": "application/json"
|
|
@@ -2003,8 +2365,19 @@ function helpCommand() {
|
|
|
2003
2365
|
}
|
|
2004
2366
|
|
|
2005
2367
|
// src/index.ts
|
|
2368
|
+
var _require2 = createRequire2(import.meta.url);
|
|
2369
|
+
function getCLIVersion2() {
|
|
2370
|
+
try {
|
|
2371
|
+
const pkg = _require2(
|
|
2372
|
+
path5.resolve(path5.dirname(fileURLToPath2(import.meta.url)), "../package.json")
|
|
2373
|
+
);
|
|
2374
|
+
return pkg.version;
|
|
2375
|
+
} catch {
|
|
2376
|
+
return "0.5.5";
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2006
2379
|
var program = new Command27();
|
|
2007
|
-
program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version(
|
|
2380
|
+
program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version(getCLIVersion2());
|
|
2008
2381
|
program.addCommand(authCommand());
|
|
2009
2382
|
program.addCommand(initCommand());
|
|
2010
2383
|
program.addCommand(serverCommand());
|