@agenticmail/enterprise 0.5.190 → 0.5.192
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/chunk-WOD3QSGX.js +1206 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/setup-NFCGCG7K.js +20 -0
- package/package.json +4 -2
- package/src/setup/deployment.ts +266 -1
- package/src/setup/index.ts +16 -9
- package/src/setup/provision.ts +48 -7
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
promptCompanyInfo,
|
|
3
|
+
promptDatabase,
|
|
4
|
+
promptDeployment,
|
|
5
|
+
promptDomain,
|
|
6
|
+
promptRegistration,
|
|
7
|
+
provision,
|
|
8
|
+
runSetupWizard
|
|
9
|
+
} from "./chunk-WOD3QSGX.js";
|
|
10
|
+
import "./chunk-VQQ4SYYQ.js";
|
|
11
|
+
import "./chunk-KFQGP6VL.js";
|
|
12
|
+
export {
|
|
13
|
+
promptCompanyInfo,
|
|
14
|
+
promptDatabase,
|
|
15
|
+
promptDeployment,
|
|
16
|
+
promptDomain,
|
|
17
|
+
promptRegistration,
|
|
18
|
+
provision,
|
|
19
|
+
runSetupWizard
|
|
20
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/enterprise",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.192",
|
|
4
4
|
"description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,9 +49,11 @@
|
|
|
49
49
|
"bcryptjs": "^2.4.3",
|
|
50
50
|
"chalk": "^5.0.0",
|
|
51
51
|
"hono": "^4.0.0",
|
|
52
|
+
"imapflow": "^1.2.10",
|
|
52
53
|
"inquirer": "^9.0.0",
|
|
53
54
|
"jose": "^5.0.0",
|
|
54
55
|
"nanoid": "^5.0.0",
|
|
56
|
+
"nodemailer": "^8.0.1",
|
|
55
57
|
"openai": "^4.77.0",
|
|
56
58
|
"ora": "^8.0.0",
|
|
57
59
|
"playwright": "^1.58.2",
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
},
|
|
73
75
|
"peerDependencies": {
|
|
74
76
|
"playwright-core": ">=1.40.0",
|
|
75
|
-
"ws": "
|
|
77
|
+
"ws": "^8.19.0"
|
|
76
78
|
},
|
|
77
79
|
"peerDependenciesMeta": {
|
|
78
80
|
"playwright-core": {
|
package/src/setup/deployment.ts
CHANGED
|
@@ -2,12 +2,29 @@
|
|
|
2
2
|
* Setup Wizard — Step 3: Deployment Target
|
|
3
3
|
*
|
|
4
4
|
* Choose where the enterprise server will run.
|
|
5
|
+
* Includes Cloudflare Tunnel as recommended self-hosted option —
|
|
6
|
+
* handles install, login, tunnel creation, DNS, and PM2 in one flow.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
import { execSync, exec as execCb } from 'child_process';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
import { existsSync, writeFileSync, readFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir, platform, arch } from 'os';
|
|
14
|
+
|
|
15
|
+
const execP = promisify(execCb);
|
|
16
|
+
|
|
17
|
+
export type DeployTarget = 'cloud' | 'cloudflare-tunnel' | 'fly' | 'railway' | 'docker' | 'local';
|
|
8
18
|
|
|
9
19
|
export interface DeploymentSelection {
|
|
10
20
|
target: DeployTarget;
|
|
21
|
+
/** Populated when target is 'cloudflare-tunnel' */
|
|
22
|
+
tunnel?: {
|
|
23
|
+
tunnelId: string;
|
|
24
|
+
domain: string;
|
|
25
|
+
port: number;
|
|
26
|
+
tunnelName: string;
|
|
27
|
+
};
|
|
11
28
|
}
|
|
12
29
|
|
|
13
30
|
export async function promptDeployment(
|
|
@@ -27,6 +44,10 @@ export async function promptDeployment(
|
|
|
27
44
|
name: `AgenticMail Cloud ${chalk.dim('(managed, instant URL)')}`,
|
|
28
45
|
value: 'cloud',
|
|
29
46
|
},
|
|
47
|
+
{
|
|
48
|
+
name: `Cloudflare Tunnel ${chalk.green('← recommended')} ${chalk.dim('(self-hosted, free, no ports)')}`,
|
|
49
|
+
value: 'cloudflare-tunnel',
|
|
50
|
+
},
|
|
30
51
|
{
|
|
31
52
|
name: `Fly.io ${chalk.dim('(your account)')}`,
|
|
32
53
|
value: 'fly',
|
|
@@ -46,5 +67,249 @@ export async function promptDeployment(
|
|
|
46
67
|
],
|
|
47
68
|
}]);
|
|
48
69
|
|
|
70
|
+
if (deployTarget === 'cloudflare-tunnel') {
|
|
71
|
+
const tunnel = await runTunnelSetup(inquirer, chalk);
|
|
72
|
+
return { target: deployTarget, tunnel };
|
|
73
|
+
}
|
|
74
|
+
|
|
49
75
|
return { target: deployTarget };
|
|
50
76
|
}
|
|
77
|
+
|
|
78
|
+
// ─── Cloudflare Tunnel Interactive Setup ────────────
|
|
79
|
+
|
|
80
|
+
async function runTunnelSetup(
|
|
81
|
+
inquirer: any,
|
|
82
|
+
chalk: any,
|
|
83
|
+
): Promise<DeploymentSelection['tunnel']> {
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(chalk.bold(' Cloudflare Tunnel Setup'));
|
|
86
|
+
console.log(chalk.dim(' Exposes your local server to the internet via Cloudflare.'));
|
|
87
|
+
console.log(chalk.dim(' No open ports, free TLS, auto-DNS.\n'));
|
|
88
|
+
|
|
89
|
+
// ── Step 1: Check / Install cloudflared ─────────
|
|
90
|
+
console.log(chalk.bold(' 1. Cloudflared CLI'));
|
|
91
|
+
|
|
92
|
+
let installed = false;
|
|
93
|
+
let version = '';
|
|
94
|
+
try {
|
|
95
|
+
version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
96
|
+
installed = true;
|
|
97
|
+
} catch { /* not installed */ }
|
|
98
|
+
|
|
99
|
+
if (installed) {
|
|
100
|
+
console.log(chalk.green(` ✓ Installed (${version})\n`));
|
|
101
|
+
} else {
|
|
102
|
+
console.log(chalk.yellow(' Not installed.'));
|
|
103
|
+
const { doInstall } = await inquirer.prompt([{
|
|
104
|
+
type: 'confirm',
|
|
105
|
+
name: 'doInstall',
|
|
106
|
+
message: 'Install cloudflared now?',
|
|
107
|
+
default: true,
|
|
108
|
+
}]);
|
|
109
|
+
|
|
110
|
+
if (!doInstall) {
|
|
111
|
+
console.log(chalk.red('\n cloudflared is required for tunnel deployment.'));
|
|
112
|
+
console.log(chalk.dim(' Install it manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/\n'));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(chalk.dim(' Installing...'));
|
|
117
|
+
try {
|
|
118
|
+
await installCloudflared();
|
|
119
|
+
version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
120
|
+
console.log(chalk.green(` ✓ Installed (${version})\n`));
|
|
121
|
+
} catch (err: any) {
|
|
122
|
+
console.log(chalk.red(` ✗ Installation failed: ${err.message}`));
|
|
123
|
+
console.log(chalk.dim(' Install manually and re-run setup.\n'));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Step 2: Login to Cloudflare ─────────────────
|
|
129
|
+
console.log(chalk.bold(' 2. Cloudflare Authentication'));
|
|
130
|
+
|
|
131
|
+
const cfDir = join(homedir(), '.cloudflared');
|
|
132
|
+
const certPath = join(cfDir, 'cert.pem');
|
|
133
|
+
const loggedIn = existsSync(certPath);
|
|
134
|
+
|
|
135
|
+
if (loggedIn) {
|
|
136
|
+
console.log(chalk.green(' ✓ Already authenticated\n'));
|
|
137
|
+
} else {
|
|
138
|
+
console.log(chalk.dim(' This will open your browser to authorize Cloudflare.\n'));
|
|
139
|
+
const { doLogin } = await inquirer.prompt([{
|
|
140
|
+
type: 'confirm',
|
|
141
|
+
name: 'doLogin',
|
|
142
|
+
message: 'Open browser to login to Cloudflare?',
|
|
143
|
+
default: true,
|
|
144
|
+
}]);
|
|
145
|
+
|
|
146
|
+
if (!doLogin) {
|
|
147
|
+
console.log(chalk.red('\n Cloudflare auth is required. Run `cloudflared tunnel login` manually.\n'));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(chalk.dim(' Waiting for browser authorization...'));
|
|
152
|
+
try {
|
|
153
|
+
await execP('cloudflared tunnel login', { timeout: 120000 });
|
|
154
|
+
console.log(chalk.green(' ✓ Authenticated\n'));
|
|
155
|
+
} catch (err: any) {
|
|
156
|
+
console.log(chalk.red(` ✗ Login failed or timed out: ${err.message}`));
|
|
157
|
+
console.log(chalk.dim(' Complete the browser authorization and try again.\n'));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Step 3: Domain + Port ───────────────────────
|
|
163
|
+
console.log(chalk.bold(' 3. Tunnel Configuration'));
|
|
164
|
+
|
|
165
|
+
const { domain, port, tunnelName } = await inquirer.prompt([
|
|
166
|
+
{
|
|
167
|
+
type: 'input',
|
|
168
|
+
name: 'domain',
|
|
169
|
+
message: 'Domain (e.g. dashboard.yourcompany.com):',
|
|
170
|
+
validate: (v: string) => v.includes('.') ? true : 'Enter a valid domain',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'number',
|
|
174
|
+
name: 'port',
|
|
175
|
+
message: 'Local port:',
|
|
176
|
+
default: 3200,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'input',
|
|
180
|
+
name: 'tunnelName',
|
|
181
|
+
message: 'Tunnel name:',
|
|
182
|
+
default: 'agenticmail-enterprise',
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
// ── Step 4: Create tunnel + DNS + Start ─────────
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(chalk.bold(' 4. Deploying'));
|
|
189
|
+
|
|
190
|
+
// Create tunnel
|
|
191
|
+
let tunnelId = '';
|
|
192
|
+
try {
|
|
193
|
+
console.log(chalk.dim(' Creating tunnel...'));
|
|
194
|
+
const out = execSync(`cloudflared tunnel create ${tunnelName} 2>&1`, { encoding: 'utf8', timeout: 30000 });
|
|
195
|
+
const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);
|
|
196
|
+
tunnelId = match?.[1] || '';
|
|
197
|
+
console.log(chalk.green(` ✓ Tunnel created: ${tunnelName} (${tunnelId})`));
|
|
198
|
+
} catch (e: any) {
|
|
199
|
+
if (e.message?.includes('already exists') || e.stderr?.includes('already exists')) {
|
|
200
|
+
try {
|
|
201
|
+
const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });
|
|
202
|
+
const tunnels = JSON.parse(listOut);
|
|
203
|
+
const existing = tunnels.find((t: any) => t.name === tunnelName);
|
|
204
|
+
if (existing) {
|
|
205
|
+
tunnelId = existing.id;
|
|
206
|
+
console.log(chalk.green(` ✓ Using existing tunnel: ${tunnelName} (${tunnelId})`));
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
console.log(chalk.red(` ✗ Tunnel "${tunnelName}" exists but couldn't read its ID`));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
console.log(chalk.red(` ✗ Failed to create tunnel: ${e.message}`));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!tunnelId) {
|
|
219
|
+
console.log(chalk.red(' ✗ Could not determine tunnel ID'));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Write config
|
|
224
|
+
const config = [
|
|
225
|
+
`tunnel: ${tunnelId}`,
|
|
226
|
+
`credentials-file: ${join(cfDir, tunnelId + '.json')}`,
|
|
227
|
+
'',
|
|
228
|
+
'ingress:',
|
|
229
|
+
` - hostname: ${domain}`,
|
|
230
|
+
` service: http://localhost:${port}`,
|
|
231
|
+
' - service: http_status:404',
|
|
232
|
+
].join('\n');
|
|
233
|
+
|
|
234
|
+
writeFileSync(join(cfDir, 'config.yml'), config);
|
|
235
|
+
console.log(chalk.green(` ✓ Config written: ${domain} → localhost:${port}`));
|
|
236
|
+
|
|
237
|
+
// Route DNS
|
|
238
|
+
try {
|
|
239
|
+
execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });
|
|
240
|
+
console.log(chalk.green(` ✓ DNS CNAME created: ${domain}`));
|
|
241
|
+
} catch (e: any) {
|
|
242
|
+
if (e.message?.includes('already exists') || e.stderr?.includes('already exists')) {
|
|
243
|
+
console.log(chalk.green(` ✓ DNS CNAME already exists for ${domain}`));
|
|
244
|
+
} else {
|
|
245
|
+
console.log(chalk.yellow(` ⚠ DNS routing failed — add CNAME manually: ${domain} → ${tunnelId}.cfargotunnel.com`));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Start with PM2
|
|
250
|
+
let started = false;
|
|
251
|
+
try {
|
|
252
|
+
execSync('which pm2', { timeout: 3000 });
|
|
253
|
+
try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
|
|
254
|
+
execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });
|
|
255
|
+
try { execSync('pm2 save 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
|
|
256
|
+
console.log(chalk.green(' ✓ Tunnel running via PM2 (auto-restarts on crash)'));
|
|
257
|
+
started = true;
|
|
258
|
+
} catch { /* PM2 not available */ }
|
|
259
|
+
|
|
260
|
+
if (!started) {
|
|
261
|
+
// Try npm install pm2 globally, then retry
|
|
262
|
+
try {
|
|
263
|
+
console.log(chalk.dim(' Installing PM2 for process management...'));
|
|
264
|
+
execSync('npm install -g pm2', { timeout: 60000, stdio: 'pipe' });
|
|
265
|
+
try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
|
|
266
|
+
execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });
|
|
267
|
+
try { execSync('pm2 save 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
|
|
268
|
+
console.log(chalk.green(' ✓ PM2 installed + tunnel running (auto-restarts on crash)'));
|
|
269
|
+
started = true;
|
|
270
|
+
} catch {
|
|
271
|
+
console.log(chalk.yellow(' ⚠ PM2 not available — tunnel started in background'));
|
|
272
|
+
console.log(chalk.dim(' Install PM2 for auto-restart: npm install -g pm2'));
|
|
273
|
+
try {
|
|
274
|
+
const { spawn } = await import('child_process');
|
|
275
|
+
const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });
|
|
276
|
+
child.unref();
|
|
277
|
+
started = true;
|
|
278
|
+
} catch { /* best effort */ }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log('');
|
|
283
|
+
console.log(chalk.green.bold(` ✓ Tunnel deployed! Your dashboard will be at https://${domain}`));
|
|
284
|
+
console.log('');
|
|
285
|
+
|
|
286
|
+
return { tunnelId, domain, port, tunnelName };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── Install cloudflared binary ─────────────────────
|
|
290
|
+
|
|
291
|
+
async function installCloudflared(): Promise<void> {
|
|
292
|
+
const plat = platform();
|
|
293
|
+
const a = arch();
|
|
294
|
+
|
|
295
|
+
if (plat === 'darwin') {
|
|
296
|
+
try {
|
|
297
|
+
execSync('which brew', { timeout: 3000 });
|
|
298
|
+
execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });
|
|
299
|
+
return;
|
|
300
|
+
} catch { /* no brew, direct download */ }
|
|
301
|
+
const cfArch = a === 'arm64' ? 'arm64' : 'amd64';
|
|
302
|
+
execSync(
|
|
303
|
+
`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${cfArch} && chmod +x /usr/local/bin/cloudflared`,
|
|
304
|
+
{ timeout: 60000 },
|
|
305
|
+
);
|
|
306
|
+
} else if (plat === 'linux') {
|
|
307
|
+
const cfArch = a === 'arm64' ? 'arm64' : 'amd64';
|
|
308
|
+
execSync(
|
|
309
|
+
`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch} && chmod +x /usr/local/bin/cloudflared`,
|
|
310
|
+
{ timeout: 60000 },
|
|
311
|
+
);
|
|
312
|
+
} else {
|
|
313
|
+
throw new Error('Unsupported platform: ' + plat);
|
|
314
|
+
}
|
|
315
|
+
}
|
package/src/setup/index.ts
CHANGED
|
@@ -125,18 +125,25 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
125
125
|
const database = await promptDatabase(inquirer, chalk);
|
|
126
126
|
|
|
127
127
|
// ─── Step 3: Deployment ──────────────────────────
|
|
128
|
-
const
|
|
128
|
+
const deploymentResult = await promptDeployment(inquirer, chalk);
|
|
129
|
+
const deployTarget = deploymentResult.target;
|
|
129
130
|
|
|
130
131
|
// ─── Step 4: Custom Domain ───────────────────────
|
|
131
|
-
|
|
132
|
+
// Skip if Cloudflare Tunnel — domain was already configured during tunnel setup
|
|
133
|
+
const domain = deploymentResult.tunnel
|
|
134
|
+
? { customDomain: deploymentResult.tunnel.domain }
|
|
135
|
+
: await promptDomain(inquirer, chalk, deployTarget);
|
|
132
136
|
|
|
133
137
|
// ─── Step 5: Domain Registration ─────────────────
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
// Skip for tunnel — DNS is already configured by cloudflared
|
|
139
|
+
const registration = deploymentResult.tunnel
|
|
140
|
+
? { registered: true, verificationStatus: 'verified' as const } as any
|
|
141
|
+
: await promptRegistration(
|
|
142
|
+
inquirer, chalk, ora,
|
|
143
|
+
domain.customDomain,
|
|
144
|
+
company.companyName,
|
|
145
|
+
company.adminEmail,
|
|
146
|
+
);
|
|
140
147
|
|
|
141
148
|
// ─── Install DB driver if needed ───────────────
|
|
142
149
|
await ensureDbDriver(database.type, ora, chalk);
|
|
@@ -147,7 +154,7 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
147
154
|
console.log('');
|
|
148
155
|
|
|
149
156
|
const result = await provision(
|
|
150
|
-
{ company, database, deployTarget, domain, registration },
|
|
157
|
+
{ company, database, deployTarget, domain, registration, tunnel: deploymentResult.tunnel },
|
|
151
158
|
ora,
|
|
152
159
|
chalk,
|
|
153
160
|
);
|
package/src/setup/provision.ts
CHANGED
|
@@ -29,6 +29,12 @@ export interface ProvisionConfig {
|
|
|
29
29
|
deployTarget: DeployTarget;
|
|
30
30
|
domain: DomainSelection;
|
|
31
31
|
registration?: RegistrationSelection;
|
|
32
|
+
tunnel?: {
|
|
33
|
+
tunnelId: string;
|
|
34
|
+
domain: string;
|
|
35
|
+
port: number;
|
|
36
|
+
tunnelName: string;
|
|
37
|
+
};
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
export interface ProvisionResult {
|
|
@@ -127,12 +133,24 @@ export async function provision(
|
|
|
127
133
|
|
|
128
134
|
// ─── Admin Account ─────────────────────────────
|
|
129
135
|
spinner.start('Creating admin account...');
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
let admin: any;
|
|
137
|
+
try {
|
|
138
|
+
admin = await db.createUser({
|
|
139
|
+
email: config.company.adminEmail,
|
|
140
|
+
name: 'Admin',
|
|
141
|
+
role: 'owner',
|
|
142
|
+
password: config.company.adminPassword,
|
|
143
|
+
});
|
|
144
|
+
} catch (err: any) {
|
|
145
|
+
// If the user already exists (re-install), look them up instead
|
|
146
|
+
if (err.message?.includes('duplicate key') || err.message?.includes('UNIQUE constraint') || err.code === '23505') {
|
|
147
|
+
admin = await db.getUserByEmail(config.company.adminEmail);
|
|
148
|
+
if (!admin) throw err; // genuinely broken
|
|
149
|
+
spinner.text = 'Admin account already exists, reusing...';
|
|
150
|
+
} else {
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
136
154
|
await db.logEvent({
|
|
137
155
|
actor: admin.id,
|
|
138
156
|
actorType: 'system',
|
|
@@ -182,7 +200,30 @@ async function deploy(
|
|
|
182
200
|
spinner: any,
|
|
183
201
|
chalk: any,
|
|
184
202
|
): Promise<DeployResult> {
|
|
185
|
-
const { deployTarget, company, database, domain } = config;
|
|
203
|
+
const { deployTarget, company, database, domain, tunnel } = config;
|
|
204
|
+
|
|
205
|
+
// ── Cloudflare Tunnel ─────────────────────────────
|
|
206
|
+
if (deployTarget === 'cloudflare-tunnel' && tunnel) {
|
|
207
|
+
spinner.start(`Starting local server on port ${tunnel.port}...`);
|
|
208
|
+
const { createServer } = await import('../server.js');
|
|
209
|
+
const server = createServer({ port: tunnel.port, db, jwtSecret });
|
|
210
|
+
const handle = await server.start();
|
|
211
|
+
spinner.succeed('Server running');
|
|
212
|
+
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log(chalk.green.bold(' AgenticMail Enterprise is live!'));
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(` ${chalk.bold('Public URL:')} ${chalk.cyan('https://' + tunnel.domain)}`);
|
|
217
|
+
console.log(` ${chalk.bold('Local:')} ${chalk.cyan('http://localhost:' + tunnel.port)}`);
|
|
218
|
+
console.log(` ${chalk.bold('Tunnel:')} ${tunnel.tunnelName} (${tunnel.tunnelId})`);
|
|
219
|
+
console.log(` ${chalk.bold('Admin:')} ${company.adminEmail}`);
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(chalk.dim(' Tunnel is managed by PM2 — auto-restarts on crash.'));
|
|
222
|
+
console.log(chalk.dim(' Manage: pm2 status | pm2 logs cloudflared | pm2 restart cloudflared'));
|
|
223
|
+
console.log(chalk.dim(' Press Ctrl+C to stop the server'));
|
|
224
|
+
|
|
225
|
+
return { url: 'https://' + tunnel.domain, close: handle.close };
|
|
226
|
+
}
|
|
186
227
|
|
|
187
228
|
// ── Cloud ─────────────────────────────────────────
|
|
188
229
|
if (deployTarget === 'cloud') {
|