@agenticmail/enterprise 0.5.286 → 0.5.288

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.
@@ -37,6 +37,28 @@ export async function runRecover(args: string[]): Promise<void> {
37
37
  const { default: chalk } = await import('chalk');
38
38
  const { default: ora } = await import('ora');
39
39
 
40
+ // ─── Detect recovery type ─────────────────────────
41
+ const isCloud = hasFlag(args, '--cloud') || hasFlag(args, '-c');
42
+ if (isCloud || (!getFlag(args, '--domain') && !getFlag(args, '--key'))) {
43
+ // Ask which type
44
+ if (!isCloud && !getFlag(args, '--domain')) {
45
+ const { recoveryType } = await inquirer.prompt([{
46
+ type: 'list',
47
+ name: 'recoveryType',
48
+ message: 'What are you recovering?',
49
+ choices: [
50
+ { name: `AgenticMail Cloud ${chalk.dim('(yourname.agenticmail.io)')}`, value: 'cloud' },
51
+ { name: `Custom Domain ${chalk.dim('(your own domain)')}`, value: 'domain' },
52
+ ],
53
+ }]);
54
+ if (recoveryType === 'cloud') {
55
+ return runCloudRecover(args, inquirer, chalk, ora);
56
+ }
57
+ } else if (isCloud) {
58
+ return runCloudRecover(args, inquirer, chalk, ora);
59
+ }
60
+ }
61
+
40
62
  console.log('');
41
63
  console.log(chalk.bold(' AgenticMail Enterprise — Domain Recovery'));
42
64
  console.log(chalk.dim(' Recover your domain registration on a new machine.'));
@@ -276,6 +298,182 @@ export async function runRecover(args: string[]): Promise<void> {
276
298
  }
277
299
  }
278
300
 
301
+ // ─── AgenticMail Cloud Recovery ─────────────────────
302
+
303
+ const REGISTRY_URL = process.env.AGENTICMAIL_SUBDOMAIN_REGISTRY_URL
304
+ || 'https://registry.agenticmail.io';
305
+
306
+ async function runCloudRecover(args: string[], inquirer: any, chalk: any, ora: any): Promise<void> {
307
+ console.log('');
308
+ console.log(chalk.bold(' AgenticMail Cloud — Recovery'));
309
+ console.log(chalk.dim(' Recover your agenticmail.io subdomain on a new machine.'));
310
+ console.log('');
311
+ console.log(chalk.dim(' You will need your AGENTICMAIL_VAULT_KEY — the key from your'));
312
+ console.log(chalk.dim(' original installation\'s ~/.agenticmail/.env file.'));
313
+ console.log('');
314
+
315
+ // Step 1: Get vault key
316
+ let vaultKey = getFlag(args, '--vault-key') || process.env.AGENTICMAIL_VAULT_KEY;
317
+ if (!vaultKey) {
318
+ const answer = await inquirer.prompt([{
319
+ type: 'password',
320
+ name: 'vaultKey',
321
+ message: 'Your AGENTICMAIL_VAULT_KEY:',
322
+ mask: '*',
323
+ validate: (v: string) => v.trim().length >= 16 ? true : 'Key seems too short — check your backup',
324
+ }]);
325
+ vaultKey = answer.vaultKey.trim();
326
+ }
327
+
328
+ // Step 2: Optionally get subdomain name (speeds up recovery)
329
+ let subdomain = getFlag(args, '--name') || getFlag(args, '--subdomain');
330
+ if (!subdomain) {
331
+ const answer = await inquirer.prompt([{
332
+ type: 'input',
333
+ name: 'subdomain',
334
+ message: 'Your subdomain (optional — press Enter to auto-detect):',
335
+ suffix: chalk.dim('.agenticmail.io'),
336
+ }]);
337
+ subdomain = answer.subdomain?.trim() || undefined;
338
+ }
339
+
340
+ // Step 3: Recover from registry
341
+ const { createHash } = await import('crypto');
342
+ const vaultKeyHash = createHash('sha256').update(vaultKey!).digest('hex');
343
+
344
+ const spinner = ora('Recovering subdomain credentials...').start();
345
+ try {
346
+ const body: any = { vaultKeyHash };
347
+ if (subdomain) body.name = subdomain;
348
+
349
+ const resp = await fetch(`${REGISTRY_URL}/recover`, {
350
+ method: 'POST',
351
+ headers: { 'Content-Type': 'application/json' },
352
+ body: JSON.stringify(body),
353
+ });
354
+ const data = await resp.json() as any;
355
+
356
+ if (!data.success) {
357
+ spinner.fail(data.error || 'Recovery failed');
358
+ console.log('');
359
+ console.log(chalk.dim(' Make sure you are using the exact AGENTICMAIL_VAULT_KEY from'));
360
+ console.log(chalk.dim(' your original installation (~/.agenticmail/.env).'));
361
+ return;
362
+ }
363
+
364
+ spinner.succeed(`Recovered: ${chalk.bold(data.fqdn)}`);
365
+
366
+ // Step 4: Save to .env
367
+ const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('fs');
368
+ const { join } = await import('path');
369
+ const { homedir } = await import('os');
370
+
371
+ const amDir = join(homedir(), '.agenticmail');
372
+ mkdirSync(amDir, { recursive: true });
373
+ const envPath = join(amDir, '.env');
374
+
375
+ let envContent = '';
376
+ if (existsSync(envPath)) {
377
+ envContent = readFileSync(envPath, 'utf8');
378
+ }
379
+
380
+ // Update or add each key
381
+ const updates: Record<string, string> = {
382
+ AGENTICMAIL_VAULT_KEY: vaultKey!,
383
+ AGENTICMAIL_SUBDOMAIN: data.subdomain,
384
+ AGENTICMAIL_DOMAIN: data.fqdn,
385
+ CLOUDFLARED_TOKEN: data.tunnelToken,
386
+ };
387
+
388
+ for (const [key, val] of Object.entries(updates)) {
389
+ if (!val) continue;
390
+ const regex = new RegExp(`^${key}=.*$`, 'm');
391
+ if (regex.test(envContent)) {
392
+ envContent = envContent.replace(regex, `${key}=${val}`);
393
+ } else {
394
+ envContent += `${envContent.endsWith('\n') ? '' : '\n'}${key}=${val}\n`;
395
+ }
396
+ }
397
+
398
+ writeFileSync(envPath, envContent, { mode: 0o600 });
399
+
400
+ console.log('');
401
+ console.log(chalk.green.bold(' Recovery complete!'));
402
+ console.log('');
403
+ console.log(` Subdomain: ${chalk.bold(data.fqdn)}`);
404
+ console.log(` Tunnel Token: ${chalk.dim('saved to ~/.agenticmail/.env')}`);
405
+ console.log(` Vault Key: ${chalk.dim('saved to ~/.agenticmail/.env')}`);
406
+ console.log('');
407
+ console.log(chalk.bold(' Next steps:'));
408
+ console.log('');
409
+ console.log(` 1. Set your DATABASE_URL in ${chalk.dim('~/.agenticmail/.env')}`);
410
+ console.log(` ${chalk.dim('(same database from your original installation)')}`);
411
+ console.log('');
412
+ console.log(` 2. Start your instance:`);
413
+ console.log(` ${chalk.cyan('npx @agenticmail/enterprise start')}`);
414
+ console.log('');
415
+ console.log(chalk.dim(' The server will auto-start cloudflared with your tunnel token.'));
416
+ console.log(chalk.dim(' Your dashboard will be live again at https://' + data.fqdn));
417
+ console.log('');
418
+
419
+ // Step 5: Offer to install cloudflared now
420
+ const { doInstall } = await inquirer.prompt([{
421
+ type: 'confirm',
422
+ name: 'doInstall',
423
+ message: 'Install cloudflared and start the tunnel now?',
424
+ default: true,
425
+ }]);
426
+
427
+ if (doInstall) {
428
+ const { execSync } = await import('child_process');
429
+ const { platform, arch } = await import('os');
430
+
431
+ // Install cloudflared if needed
432
+ try {
433
+ execSync('which cloudflared', { timeout: 3000 });
434
+ console.log(chalk.green(' cloudflared already installed'));
435
+ } catch {
436
+ const spinner2 = ora('Installing cloudflared...').start();
437
+ try {
438
+ const os = platform();
439
+ if (os === 'darwin') {
440
+ try {
441
+ execSync('brew install cloudflared', { stdio: 'pipe', timeout: 120000 });
442
+ } catch {
443
+ const cfArch = arch() === 'arm64' ? 'arm64' : 'amd64';
444
+ execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${cfArch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });
445
+ }
446
+ } else {
447
+ const cfArch = arch() === 'arm64' ? 'arm64' : 'amd64';
448
+ execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });
449
+ }
450
+ spinner2.succeed('cloudflared installed');
451
+ } catch (e: any) {
452
+ spinner2.fail('Could not install cloudflared: ' + e.message);
453
+ console.log(chalk.dim(' Install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
454
+ }
455
+ }
456
+
457
+ // Start via PM2
458
+ try {
459
+ const { execSync: ex } = await import('child_process');
460
+ ex('which pm2', { timeout: 3000 });
461
+ try { ex('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch {}
462
+ ex(`pm2 start cloudflared --name cloudflared -- tunnel --no-autoupdate run --token ${data.tunnelToken}`, { timeout: 15000 });
463
+ try { ex('pm2 save 2>/dev/null', { timeout: 5000 }); } catch {}
464
+ console.log(chalk.green(' Tunnel running via PM2'));
465
+ } catch {
466
+ console.log(chalk.dim(' Start the tunnel manually:'));
467
+ console.log(chalk.cyan(` cloudflared tunnel --no-autoupdate run --token ${data.tunnelToken}`));
468
+ }
469
+ }
470
+
471
+ } catch (err: any) {
472
+ spinner.fail('Recovery failed: ' + err.message);
473
+ console.log(chalk.dim(' Check your internet connection and try again.'));
474
+ }
475
+ }
476
+
279
477
  /** Detect DB type from connection string */
280
478
  function detectDbType(url: string): string {
281
479
  const u = url.toLowerCase().trim();
@@ -16,6 +16,9 @@ const execP = promisify(execCb);
16
16
 
17
17
  export type DeployTarget = 'cloud' | 'cloudflare-tunnel' | 'fly' | 'railway' | 'docker' | 'local';
18
18
 
19
+ const SUBDOMAIN_REGISTRY_URL = process.env.AGENTICMAIL_SUBDOMAIN_REGISTRY_URL
20
+ || 'https://registry.agenticmail.io';
21
+
19
22
  export interface DeploymentSelection {
20
23
  target: DeployTarget;
21
24
  /** Populated when target is 'cloudflare-tunnel' */
@@ -25,6 +28,14 @@ export interface DeploymentSelection {
25
28
  port: number;
26
29
  tunnelName: string;
27
30
  };
31
+ /** Populated when target is 'cloud' (agenticmail.io subdomain) */
32
+ cloud?: {
33
+ subdomain: string;
34
+ fqdn: string;
35
+ tunnelId: string;
36
+ tunnelToken: string;
37
+ port: number;
38
+ };
28
39
  }
29
40
 
30
41
  export async function promptDeployment(
@@ -41,11 +52,11 @@ export async function promptDeployment(
41
52
  message: 'Deploy to:',
42
53
  choices: [
43
54
  {
44
- name: `AgenticMail Cloud ${chalk.dim('(managed, instant URL)')}`,
55
+ name: `AgenticMail Cloud ${chalk.green('← recommended')} ${chalk.dim('(instant URL, zero config)')}`,
45
56
  value: 'cloud',
46
57
  },
47
58
  {
48
- name: `Cloudflare Tunnel ${chalk.green('← recommended')} ${chalk.dim('(self-hosted, free, no ports)')}`,
59
+ name: `Cloudflare Tunnel ${chalk.dim('(self-hosted, free, no ports)')}`,
49
60
  value: 'cloudflare-tunnel',
50
61
  },
51
62
  {
@@ -67,6 +78,11 @@ export async function promptDeployment(
67
78
  ],
68
79
  }]);
69
80
 
81
+ if (deployTarget === 'cloud') {
82
+ const cloud = await runCloudSetup(inquirer, chalk);
83
+ return { target: deployTarget, cloud };
84
+ }
85
+
70
86
  if (deployTarget === 'cloudflare-tunnel') {
71
87
  const tunnel = await runTunnelSetup(inquirer, chalk);
72
88
  return { target: deployTarget, tunnel };
@@ -75,6 +91,219 @@ export async function promptDeployment(
75
91
  return { target: deployTarget };
76
92
  }
77
93
 
94
+ // ─── AgenticMail Cloud (Subdomain) Setup ────────────
95
+
96
+ async function runCloudSetup(
97
+ inquirer: any,
98
+ chalk: any,
99
+ ): Promise<DeploymentSelection['cloud']> {
100
+ console.log('');
101
+ console.log(chalk.bold(' AgenticMail Cloud Setup'));
102
+ console.log(chalk.dim(' Get a free subdomain on agenticmail.io — no Cloudflare account needed.'));
103
+ console.log(chalk.dim(' Your instance will be live at https://yourname.agenticmail.io\n'));
104
+
105
+ // ── Step 1: Choose subdomain ─────────
106
+
107
+ let subdomain = '';
108
+ let claimResult: any = null;
109
+
110
+ while (!subdomain) {
111
+ const { name } = await inquirer.prompt([{
112
+ type: 'input',
113
+ name: 'name',
114
+ message: 'Choose your subdomain:',
115
+ suffix: chalk.dim('.agenticmail.io'),
116
+ validate: (input: string) => {
117
+ const cleaned = input.toLowerCase().trim();
118
+ if (cleaned.length < 3) return 'Must be at least 3 characters';
119
+ if (cleaned.length > 32) return 'Must be 32 characters or fewer';
120
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(cleaned)) return 'Only lowercase letters, numbers, and hyphens allowed';
121
+ return true;
122
+ },
123
+ }]);
124
+
125
+ const cleaned = name.toLowerCase().trim();
126
+
127
+ // Check availability
128
+ process.stdout.write(chalk.dim(` Checking ${cleaned}.agenticmail.io... `));
129
+ try {
130
+ const checkResp = await fetch(`${SUBDOMAIN_REGISTRY_URL}/check?name=${encodeURIComponent(cleaned)}`);
131
+ const checkData = await checkResp.json() as any;
132
+
133
+ if (!checkData.available) {
134
+ console.log(chalk.red('✗ ' + (checkData.reason || 'Not available')));
135
+ continue;
136
+ }
137
+ console.log(chalk.green('✓ Available!'));
138
+ } catch (err: any) {
139
+ console.log(chalk.yellow('⚠ Could not check availability: ' + err.message));
140
+ console.log(chalk.dim(' Proceeding anyway — the claim step will verify.\n'));
141
+ }
142
+
143
+ // Confirm
144
+ const { confirmed } = await inquirer.prompt([{
145
+ type: 'confirm',
146
+ name: 'confirmed',
147
+ message: `Claim ${chalk.bold(cleaned + '.agenticmail.io')}?`,
148
+ default: true,
149
+ }]);
150
+
151
+ if (!confirmed) continue;
152
+
153
+ // ── Step 2: Claim subdomain ─────────
154
+
155
+ // Get or generate vault key hash for recovery
156
+ const { createHash, randomUUID } = await import('crypto');
157
+ let vaultKey = process.env.AGENTICMAIL_VAULT_KEY;
158
+ if (!vaultKey) {
159
+ vaultKey = randomUUID() + randomUUID();
160
+ process.env.AGENTICMAIL_VAULT_KEY = vaultKey;
161
+ }
162
+ const vaultKeyHash = createHash('sha256').update(vaultKey).digest('hex');
163
+
164
+ process.stdout.write(chalk.dim(' Provisioning subdomain... '));
165
+ try {
166
+ const claimResp = await fetch(`${SUBDOMAIN_REGISTRY_URL}/claim`, {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({ name: cleaned, vaultKeyHash }),
170
+ });
171
+ claimResult = await claimResp.json() as any;
172
+
173
+ if (claimResult.error) {
174
+ console.log(chalk.red('✗ ' + claimResult.error));
175
+
176
+ // If they already have a subdomain, offer recovery
177
+ if (claimResult.error.includes('already has subdomain')) {
178
+ const { wantsRecover } = await inquirer.prompt([{
179
+ type: 'confirm',
180
+ name: 'wantsRecover',
181
+ message: 'Recover your existing subdomain instead?',
182
+ default: true,
183
+ }]);
184
+ if (wantsRecover) {
185
+ const recoverResp = await fetch(`${SUBDOMAIN_REGISTRY_URL}/recover`, {
186
+ method: 'POST',
187
+ headers: { 'Content-Type': 'application/json' },
188
+ body: JSON.stringify({ vaultKeyHash }),
189
+ });
190
+ claimResult = await recoverResp.json() as any;
191
+ if (claimResult.success) {
192
+ subdomain = claimResult.subdomain;
193
+ console.log(chalk.green(`✓ Recovered: ${claimResult.fqdn}`));
194
+ } else {
195
+ console.log(chalk.red('✗ Recovery failed: ' + (claimResult.error || 'Unknown error')));
196
+ }
197
+ }
198
+ }
199
+ continue;
200
+ }
201
+
202
+ if (claimResult.success) {
203
+ subdomain = claimResult.subdomain || cleaned;
204
+ if (claimResult.recovered) {
205
+ console.log(chalk.green('✓ Recovered existing subdomain'));
206
+ } else {
207
+ console.log(chalk.green('✓ Subdomain claimed!'));
208
+ }
209
+ }
210
+ } catch (err: any) {
211
+ console.log(chalk.red('✗ Failed: ' + err.message));
212
+ console.log(chalk.dim(' Check your internet connection and try again.\n'));
213
+ }
214
+ }
215
+
216
+ // ── Step 3: Install cloudflared ─────────
217
+
218
+ console.log('');
219
+ console.log(chalk.bold(' Installing cloudflared connector...'));
220
+
221
+ let cloudflaredPath = '';
222
+ try {
223
+ cloudflaredPath = execSync('which cloudflared', { encoding: 'utf8' }).trim();
224
+ console.log(chalk.green(` ✓ cloudflared found at ${cloudflaredPath}`));
225
+ } catch {
226
+ console.log(chalk.dim(' cloudflared not found — installing...'));
227
+ try {
228
+ const os = platform();
229
+ if (os === 'darwin') {
230
+ execSync('brew install cloudflared', { stdio: 'pipe' });
231
+ } else if (os === 'linux') {
232
+ const archStr = arch() === 'arm64' ? 'arm64' : 'amd64';
233
+ execSync(`curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${archStr} -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared`, { stdio: 'pipe' });
234
+ } else {
235
+ console.log(chalk.yellow(' Please install cloudflared manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/'));
236
+ }
237
+ cloudflaredPath = execSync('which cloudflared', { encoding: 'utf8' }).trim();
238
+ console.log(chalk.green(` ✓ cloudflared installed at ${cloudflaredPath}`));
239
+ } catch (e: any) {
240
+ console.log(chalk.yellow(' ⚠ Could not auto-install cloudflared.'));
241
+ console.log(chalk.dim(' Install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/'));
242
+ }
243
+ }
244
+
245
+ // ── Step 4: Configure PM2 ─────────
246
+
247
+ const port = 3100;
248
+ const fqdn = claimResult?.fqdn || `${subdomain}.agenticmail.io`;
249
+ const tunnelToken = claimResult?.tunnelToken;
250
+ const tunnelId = claimResult?.tunnelId;
251
+
252
+ console.log('');
253
+ console.log(chalk.bold.green(' ✓ Setup Complete!'));
254
+ console.log('');
255
+ console.log(` Your dashboard: ${chalk.bold.cyan('https://' + fqdn)}`);
256
+
257
+ // ── CRITICAL: Vault key backup warning ─────────
258
+ console.log('');
259
+ console.log(chalk.bgYellow.black.bold(' ⚠ SAVE YOUR RECOVERY KEY ⚠ '));
260
+ console.log('');
261
+ console.log(chalk.yellow(' If your computer crashes, you need this key to recover your subdomain.'));
262
+ console.log(chalk.yellow(' It is stored in ~/.agenticmail/.env — back up this file somewhere safe!'));
263
+ console.log('');
264
+ console.log(` ${chalk.bold('AGENTICMAIL_VAULT_KEY=')}${chalk.dim(process.env.AGENTICMAIL_VAULT_KEY || '(check ~/.agenticmail/.env)')}`);
265
+ console.log('');
266
+ console.log(chalk.dim(' To recover on a new machine, run:'));
267
+ console.log(chalk.dim(' npx @agenticmail/enterprise recover --cloud'));
268
+ console.log('');
269
+ console.log('');
270
+ console.log(chalk.dim(' To start your instance, run these two processes:'));
271
+ console.log('');
272
+ console.log(` ${chalk.cyan('cloudflared tunnel --no-autoupdate run --token ' + (tunnelToken || '<your-tunnel-token>'))}`);
273
+ console.log(` ${chalk.cyan('npx @agenticmail/enterprise start')}`);
274
+ console.log('');
275
+ console.log(chalk.dim(' Or let the setup wizard start them with PM2 (next step).\n'));
276
+
277
+ // Save tunnel token to .env
278
+ const envPath = join(homedir(), '.agenticmail', '.env');
279
+ try {
280
+ let envContent = '';
281
+ if (existsSync(envPath)) {
282
+ envContent = readFileSync(envPath, 'utf8');
283
+ }
284
+ if (tunnelToken && !envContent.includes('CLOUDFLARED_TOKEN=')) {
285
+ envContent += `\nCLOUDFLARED_TOKEN=${tunnelToken}\n`;
286
+ }
287
+ if (!envContent.includes('AGENTICMAIL_SUBDOMAIN=')) {
288
+ envContent += `AGENTICMAIL_SUBDOMAIN=${subdomain}\n`;
289
+ }
290
+ if (!envContent.includes('AGENTICMAIL_DOMAIN=')) {
291
+ envContent += `AGENTICMAIL_DOMAIN=${fqdn}\n`;
292
+ }
293
+ const { mkdirSync } = await import('fs');
294
+ mkdirSync(join(homedir(), '.agenticmail'), { recursive: true });
295
+ writeFileSync(envPath, envContent, { mode: 0o600 });
296
+ } catch {}
297
+
298
+ return {
299
+ subdomain,
300
+ fqdn,
301
+ tunnelId: tunnelId || '',
302
+ tunnelToken: tunnelToken || '',
303
+ port,
304
+ };
305
+ }
306
+
78
307
  // ─── Cloudflare Tunnel Interactive Setup ────────────
79
308
 
80
309
  async function runTunnelSetup(
@@ -129,14 +129,16 @@ export async function runSetupWizard(): Promise<void> {
129
129
  const deployTarget = deploymentResult.target;
130
130
 
131
131
  // ─── Step 4: Custom Domain ───────────────────────
132
- // Skip if Cloudflare Tunnel — domain was already configured during tunnel setup
132
+ // Skip if Cloudflare Tunnel or Cloud — domain was already configured
133
133
  const domain = deploymentResult.tunnel
134
134
  ? { customDomain: deploymentResult.tunnel.domain }
135
+ : deploymentResult.cloud
136
+ ? { customDomain: deploymentResult.cloud.fqdn }
135
137
  : await promptDomain(inquirer, chalk, deployTarget);
136
138
 
137
139
  // ─── Step 5: Domain Registration ─────────────────
138
- // Skip for tunnel — DNS is already configured by cloudflared
139
- const registration = deploymentResult.tunnel
140
+ // Skip for tunnel or cloud — DNS is already configured
141
+ const registration = (deploymentResult.tunnel || deploymentResult.cloud)
140
142
  ? { registered: true, verificationStatus: 'verified' as const } as any
141
143
  : await promptRegistration(
142
144
  inquirer, chalk, ora,
@@ -154,7 +156,7 @@ export async function runSetupWizard(): Promise<void> {
154
156
  console.log('');
155
157
 
156
158
  const result = await provision(
157
- { company, database, deployTarget, domain, registration, tunnel: deploymentResult.tunnel },
159
+ { company, database, deployTarget, domain, registration, tunnel: deploymentResult.tunnel, cloud: deploymentResult.cloud },
158
160
  ora,
159
161
  chalk,
160
162
  );
@@ -35,6 +35,13 @@ export interface ProvisionConfig {
35
35
  port: number;
36
36
  tunnelName: string;
37
37
  };
38
+ cloud?: {
39
+ subdomain: string;
40
+ fqdn: string;
41
+ tunnelId: string;
42
+ tunnelToken: string;
43
+ port: number;
44
+ };
38
45
  }
39
46
 
40
47
  export interface ProvisionResult {
@@ -222,7 +229,7 @@ async function deploy(
222
229
  spinner: any,
223
230
  chalk: any,
224
231
  ): Promise<DeployResult> {
225
- const { deployTarget, company, database, domain, tunnel } = config;
232
+ const { deployTarget, company, database, domain, tunnel, cloud } = config;
226
233
 
227
234
  // ── Cloudflare Tunnel ─────────────────────────────
228
235
  if (deployTarget === 'cloudflare-tunnel' && tunnel) {
@@ -247,21 +254,60 @@ async function deploy(
247
254
  return { url: 'https://' + tunnel.domain, close: handle.close };
248
255
  }
249
256
 
250
- // ── Cloud ─────────────────────────────────────────
251
- if (deployTarget === 'cloud') {
252
- spinner.start('Deploying to AgenticMail Cloud...');
253
- const { deployToCloud } = await import('../deploy/managed.js');
254
- const result = await deployToCloud({
255
- subdomain: company.subdomain,
256
- plan: 'free',
257
- dbType: database.type,
258
- dbConnectionString: database.connectionString || '',
259
- jwtSecret,
260
- });
261
- spinner.succeed(`Deployed to ${result.url}`);
257
+ // ── Cloud (agenticmail.io subdomain) ───────────────
258
+ if (deployTarget === 'cloud' && cloud) {
259
+ spinner.start('Configuring agenticmail.io deployment...');
262
260
 
263
- printCloudSuccess(chalk, result.url, company.adminEmail, domain.customDomain, company.subdomain);
264
- return { url: result.url };
261
+ // Start cloudflared with the tunnel token via PM2
262
+ try {
263
+ const pm2 = await import('pm2' as string);
264
+ await new Promise<void>((resolve, reject) => {
265
+ pm2.connect((err: any) => {
266
+ if (err) { reject(err); return; }
267
+
268
+ // Start cloudflared tunnel
269
+ pm2.start({
270
+ name: 'cloudflared',
271
+ script: 'cloudflared',
272
+ args: `tunnel --no-autoupdate run --token ${cloud.tunnelToken}`,
273
+ interpreter: 'none',
274
+ autorestart: true,
275
+ }, (e: any) => {
276
+ if (e) console.warn(`PM2 cloudflared start: ${e.message}`);
277
+
278
+ // Start enterprise server
279
+ pm2.start({
280
+ name: 'enterprise',
281
+ script: 'npx',
282
+ args: '@agenticmail/enterprise start',
283
+ env: {
284
+ PORT: String(cloud.port || 3100),
285
+ DATABASE_URL: database.connectionString || '',
286
+ JWT_SECRET: jwtSecret,
287
+ AGENTICMAIL_VAULT_KEY: vaultKey,
288
+ AGENTICMAIL_DOMAIN: cloud.fqdn,
289
+ },
290
+ autorestart: true,
291
+ }, (e2: any) => {
292
+ pm2.disconnect();
293
+ if (e2) reject(e2); else resolve();
294
+ });
295
+ });
296
+ });
297
+ });
298
+ spinner.succeed(`Live at https://${cloud.fqdn}`);
299
+ } catch (e: any) {
300
+ spinner.warn(`PM2 setup failed: ${e.message}`);
301
+ console.log(` Start manually:`);
302
+ console.log(` cloudflared tunnel --no-autoupdate run --token ${cloud.tunnelToken}`);
303
+ console.log(` npx @agenticmail/enterprise start`);
304
+ }
305
+
306
+ console.log('');
307
+ console.log(` Dashboard: https://${cloud.fqdn}`);
308
+ console.log(` To recover on a new machine, keep your AGENTICMAIL_VAULT_KEY safe.`);
309
+ console.log('');
310
+ return { url: `https://${cloud.fqdn}` };
265
311
  }
266
312
 
267
313
  // ── Docker ────────────────────────────────────────