@agenticmail/enterprise 0.5.286 → 0.5.287
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-N5ADPJ5V.js +1423 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/setup-E6TDJMKW.js +20 -0
- package/package.json +1 -1
- package/src/setup/deployment.ts +216 -0
- package/src/setup/index.ts +6 -4
- package/src/setup/provision.ts +61 -15
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-N5ADPJ5V.js";
|
|
10
|
+
import "./chunk-ULRBF2T7.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
package/src/setup/deployment.ts
CHANGED
|
@@ -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://subdomain-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(
|
|
@@ -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,206 @@ 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
|
+
console.log('');
|
|
257
|
+
console.log(chalk.dim(' To start your instance, run these two processes:'));
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(` ${chalk.cyan('cloudflared tunnel --no-autoupdate run --token ' + (tunnelToken || '<your-tunnel-token>'))}`);
|
|
260
|
+
console.log(` ${chalk.cyan('npx @agenticmail/enterprise start')}`);
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log(chalk.dim(' Or let the setup wizard start them with PM2 (next step).\n'));
|
|
263
|
+
|
|
264
|
+
// Save tunnel token to .env
|
|
265
|
+
const envPath = join(homedir(), '.agenticmail', '.env');
|
|
266
|
+
try {
|
|
267
|
+
let envContent = '';
|
|
268
|
+
if (existsSync(envPath)) {
|
|
269
|
+
envContent = readFileSync(envPath, 'utf8');
|
|
270
|
+
}
|
|
271
|
+
if (tunnelToken && !envContent.includes('CLOUDFLARED_TOKEN=')) {
|
|
272
|
+
envContent += `\nCLOUDFLARED_TOKEN=${tunnelToken}\n`;
|
|
273
|
+
}
|
|
274
|
+
if (!envContent.includes('AGENTICMAIL_SUBDOMAIN=')) {
|
|
275
|
+
envContent += `AGENTICMAIL_SUBDOMAIN=${subdomain}\n`;
|
|
276
|
+
}
|
|
277
|
+
if (!envContent.includes('AGENTICMAIL_DOMAIN=')) {
|
|
278
|
+
envContent += `AGENTICMAIL_DOMAIN=${fqdn}\n`;
|
|
279
|
+
}
|
|
280
|
+
const { mkdirSync } = await import('fs');
|
|
281
|
+
mkdirSync(join(homedir(), '.agenticmail'), { recursive: true });
|
|
282
|
+
writeFileSync(envPath, envContent, { mode: 0o600 });
|
|
283
|
+
} catch {}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
subdomain,
|
|
287
|
+
fqdn,
|
|
288
|
+
tunnelId: tunnelId || '',
|
|
289
|
+
tunnelToken: tunnelToken || '',
|
|
290
|
+
port,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
78
294
|
// ─── Cloudflare Tunnel Interactive Setup ────────────
|
|
79
295
|
|
|
80
296
|
async function runTunnelSetup(
|
package/src/setup/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
);
|
package/src/setup/provision.ts
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
264
|
-
|
|
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 ────────────────────────────────────────
|