@agenticmail/enterprise 0.2.2 → 0.3.0

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/src/db/sqlite.ts CHANGED
@@ -52,10 +52,13 @@ export class SqliteAdapter extends DatabaseAdapter {
52
52
  const stmts = getAllCreateStatements();
53
53
  const tx = this.db.transaction(() => {
54
54
  for (const stmt of stmts) this.db.exec(stmt);
55
- // Seed retention policy
55
+ // Seed defaults
56
56
  this.db.prepare(
57
57
  `INSERT OR IGNORE INTO retention_policy (id) VALUES ('default')`
58
58
  ).run();
59
+ this.db.prepare(
60
+ `INSERT OR IGNORE INTO company_settings (id, name, subdomain) VALUES ('default', 'My Company', 'my-company')`
61
+ ).run();
59
62
  });
60
63
  tx();
61
64
  }
package/src/server.ts CHANGED
@@ -107,7 +107,7 @@ export function createServer(config: ServerConfig): ServerInstance {
107
107
 
108
108
  app.get('/health', (c) => c.json({
109
109
  status: 'ok',
110
- version: '0.2.2',
110
+ version: '0.3.0',
111
111
  uptime: process.uptime(),
112
112
  }));
113
113
 
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Setup Wizard — Step 1: Company Info
3
+ *
4
+ * Collects company name, admin email, and admin password.
5
+ */
6
+
7
+ export interface CompanyInfo {
8
+ companyName: string;
9
+ adminEmail: string;
10
+ adminPassword: string;
11
+ subdomain: string;
12
+ }
13
+
14
+ export async function promptCompanyInfo(
15
+ inquirer: any,
16
+ chalk: any,
17
+ ): Promise<CompanyInfo> {
18
+ console.log(chalk.bold.cyan(' Step 1 of 4: Company Info'));
19
+ console.log(chalk.dim(' Tell us about your organization.\n'));
20
+
21
+ const { companyName, adminEmail, adminPassword } = await inquirer.prompt([
22
+ {
23
+ type: 'input',
24
+ name: 'companyName',
25
+ message: 'Company name:',
26
+ validate: (v: string) => {
27
+ if (!v.trim()) return 'Company name is required';
28
+ if (v.length > 100) return 'Company name must be under 100 characters';
29
+ return true;
30
+ },
31
+ },
32
+ {
33
+ type: 'input',
34
+ name: 'adminEmail',
35
+ message: 'Admin email:',
36
+ validate: (v: string) => {
37
+ if (!v.includes('@') || !v.includes('.')) return 'Enter a valid email address';
38
+ return true;
39
+ },
40
+ },
41
+ {
42
+ type: 'password',
43
+ name: 'adminPassword',
44
+ message: 'Admin password:',
45
+ mask: '*',
46
+ validate: (v: string) => {
47
+ if (v.length < 8) return 'Password must be at least 8 characters';
48
+ if (!/[A-Z]/.test(v) && !/[0-9]/.test(v)) {
49
+ return 'Password should contain at least one uppercase letter or number';
50
+ }
51
+ return true;
52
+ },
53
+ },
54
+ ]);
55
+
56
+ // Derive subdomain from company name
57
+ const subdomain = companyName
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, '-')
60
+ .replace(/^-|-$/g, '')
61
+ .slice(0, 63);
62
+
63
+ return { companyName, adminEmail, adminPassword, subdomain };
64
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Setup Wizard — Step 2: Database Selection
3
+ *
4
+ * Lets the user pick from 10 database backends and
5
+ * collects the connection details specific to each one.
6
+ */
7
+
8
+ import { getSupportedDatabases } from '../db/factory.js';
9
+
10
+ export interface DatabaseSelection {
11
+ type: string;
12
+ connectionString?: string;
13
+ region?: string;
14
+ accessKeyId?: string;
15
+ secretAccessKey?: string;
16
+ authToken?: string;
17
+ }
18
+
19
+ const CONNECTION_HINTS: Record<string, string> = {
20
+ postgres: 'postgresql://user:pass@host:5432/dbname',
21
+ mysql: 'mysql://user:pass@host:3306/dbname',
22
+ mongodb: 'mongodb+srv://user:pass@cluster.mongodb.net/dbname',
23
+ supabase: 'postgresql://postgres:pass@db.xxxx.supabase.co:5432/postgres',
24
+ neon: 'postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require',
25
+ planetscale: 'mysql://user:pass@aws.connect.psdb.cloud/dbname?ssl={"rejectUnauthorized":true}',
26
+ cockroachdb: 'postgresql://user:pass@cluster.cockroachlabs.cloud:26257/dbname?sslmode=verify-full',
27
+ };
28
+
29
+ export async function promptDatabase(
30
+ inquirer: any,
31
+ chalk: any,
32
+ ): Promise<DatabaseSelection> {
33
+ console.log('');
34
+ console.log(chalk.bold.cyan(' Step 2 of 4: Database'));
35
+ console.log(chalk.dim(' Where should your data live?\n'));
36
+
37
+ const databases = getSupportedDatabases();
38
+ const { dbType } = await inquirer.prompt([
39
+ {
40
+ type: 'list',
41
+ name: 'dbType',
42
+ message: 'Database backend:',
43
+ choices: databases.map((d: any) => ({
44
+ name: `${d.label} ${chalk.dim(`(${d.group})`)}`,
45
+ value: d.type,
46
+ })),
47
+ },
48
+ ]);
49
+
50
+ // ─── SQLite ────────────────────────────────────────
51
+ if (dbType === 'sqlite') {
52
+ const { dbPath } = await inquirer.prompt([{
53
+ type: 'input',
54
+ name: 'dbPath',
55
+ message: 'Database file path:',
56
+ default: './agenticmail-enterprise.db',
57
+ }]);
58
+ return { type: dbType, connectionString: dbPath };
59
+ }
60
+
61
+ // ─── DynamoDB ──────────────────────────────────────
62
+ if (dbType === 'dynamodb') {
63
+ const answers = await inquirer.prompt([
64
+ {
65
+ type: 'input',
66
+ name: 'region',
67
+ message: 'AWS Region:',
68
+ default: 'us-east-1',
69
+ },
70
+ {
71
+ type: 'input',
72
+ name: 'accessKeyId',
73
+ message: 'AWS Access Key ID:',
74
+ validate: (v: string) => v.length > 0 || 'Required',
75
+ },
76
+ {
77
+ type: 'password',
78
+ name: 'secretAccessKey',
79
+ message: 'AWS Secret Access Key:',
80
+ mask: '*',
81
+ validate: (v: string) => v.length > 0 || 'Required',
82
+ },
83
+ ]);
84
+ return { type: dbType, ...answers };
85
+ }
86
+
87
+ // ─── Turso / LibSQL ────────────────────────────────
88
+ if (dbType === 'turso') {
89
+ const answers = await inquirer.prompt([
90
+ {
91
+ type: 'input',
92
+ name: 'connectionString',
93
+ message: 'Turso database URL:',
94
+ suffix: chalk.dim(' (e.g. libsql://db-org.turso.io)'),
95
+ validate: (v: string) => v.length > 0 || 'Required',
96
+ },
97
+ {
98
+ type: 'password',
99
+ name: 'authToken',
100
+ message: 'Turso auth token:',
101
+ mask: '*',
102
+ validate: (v: string) => v.length > 0 || 'Required',
103
+ },
104
+ ]);
105
+ return { type: dbType, connectionString: answers.connectionString, authToken: answers.authToken };
106
+ }
107
+
108
+ // ─── All others (connection string) ────────────────
109
+ const hint = CONNECTION_HINTS[dbType] || '';
110
+ const { connectionString } = await inquirer.prompt([{
111
+ type: 'input',
112
+ name: 'connectionString',
113
+ message: 'Connection string:',
114
+ suffix: hint ? chalk.dim(` (e.g. ${hint})`) : '',
115
+ validate: (v: string) => v.length > 0 || 'Connection string is required',
116
+ }]);
117
+
118
+ return { type: dbType, connectionString };
119
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Setup Wizard — Step 3: Deployment Target
3
+ *
4
+ * Choose where the enterprise server will run.
5
+ */
6
+
7
+ export type DeployTarget = 'cloud' | 'fly' | 'railway' | 'docker' | 'local';
8
+
9
+ export interface DeploymentSelection {
10
+ target: DeployTarget;
11
+ }
12
+
13
+ export async function promptDeployment(
14
+ inquirer: any,
15
+ chalk: any,
16
+ ): Promise<DeploymentSelection> {
17
+ console.log('');
18
+ console.log(chalk.bold.cyan(' Step 3 of 4: Deployment'));
19
+ console.log(chalk.dim(' Where should your dashboard run?\n'));
20
+
21
+ const { deployTarget } = await inquirer.prompt([{
22
+ type: 'list',
23
+ name: 'deployTarget',
24
+ message: 'Deploy to:',
25
+ choices: [
26
+ {
27
+ name: `AgenticMail Cloud ${chalk.dim('(managed, instant URL)')}`,
28
+ value: 'cloud',
29
+ },
30
+ {
31
+ name: `Fly.io ${chalk.dim('(your account)')}`,
32
+ value: 'fly',
33
+ },
34
+ {
35
+ name: `Railway ${chalk.dim('(your account)')}`,
36
+ value: 'railway',
37
+ },
38
+ {
39
+ name: `Docker ${chalk.dim('(self-hosted)')}`,
40
+ value: 'docker',
41
+ },
42
+ {
43
+ name: `Local ${chalk.dim('(dev/testing, runs here)')}`,
44
+ value: 'local',
45
+ },
46
+ ],
47
+ }]);
48
+
49
+ return { target: deployTarget };
50
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Setup Wizard — Step 4: Custom Domain (Optional)
3
+ *
4
+ * Optionally configure a custom domain for the dashboard.
5
+ */
6
+
7
+ export interface DomainSelection {
8
+ customDomain?: string;
9
+ }
10
+
11
+ export async function promptDomain(
12
+ inquirer: any,
13
+ chalk: any,
14
+ deployTarget: string,
15
+ ): Promise<DomainSelection> {
16
+ // Skip for local deployments
17
+ if (deployTarget === 'local') {
18
+ return {};
19
+ }
20
+
21
+ console.log('');
22
+ console.log(chalk.bold.cyan(' Step 4 of 4: Custom Domain'));
23
+ console.log(chalk.dim(' Optional — you can add this later.\n'));
24
+
25
+ const { wantsDomain } = await inquirer.prompt([{
26
+ type: 'confirm',
27
+ name: 'wantsDomain',
28
+ message: 'Add a custom domain?',
29
+ default: false,
30
+ }]);
31
+
32
+ if (!wantsDomain) return {};
33
+
34
+ const { domain } = await inquirer.prompt([{
35
+ type: 'input',
36
+ name: 'domain',
37
+ message: 'Custom domain:',
38
+ suffix: chalk.dim(' (e.g. agents.acme.com)'),
39
+ validate: (v: string) => {
40
+ if (!v.includes('.')) return 'Enter a valid domain (e.g. agents.acme.com)';
41
+ return true;
42
+ },
43
+ }]);
44
+
45
+ return { customDomain: domain };
46
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Setup Wizard — Orchestrator
3
+ *
4
+ * Runs the 4-step interactive setup wizard by composing
5
+ * the individual step modules. Each step is in its own file
6
+ * so the wizard logic stays manageable.
7
+ *
8
+ * Steps:
9
+ * 1. Company Info (setup/company.ts)
10
+ * 2. Database (setup/database.ts)
11
+ * 3. Deployment (setup/deployment.ts)
12
+ * 4. Custom Domain (setup/domain.ts)
13
+ * → Provision (setup/provision.ts)
14
+ */
15
+
16
+ import { promptCompanyInfo } from './company.js';
17
+ import { promptDatabase } from './database.js';
18
+ import { promptDeployment } from './deployment.js';
19
+ import { promptDomain } from './domain.js';
20
+ import { provision } from './provision.js';
21
+
22
+ export { promptCompanyInfo } from './company.js';
23
+ export { promptDatabase } from './database.js';
24
+ export { promptDeployment } from './deployment.js';
25
+ export { promptDomain } from './domain.js';
26
+ export { provision } from './provision.js';
27
+ export type { CompanyInfo } from './company.js';
28
+ export type { DatabaseSelection } from './database.js';
29
+ export type { DeployTarget, DeploymentSelection } from './deployment.js';
30
+ export type { DomainSelection } from './domain.js';
31
+ export type { ProvisionConfig, ProvisionResult } from './provision.js';
32
+
33
+ /**
34
+ * Run the full interactive setup wizard.
35
+ * Returns when provisioning is complete (or the local server is running).
36
+ */
37
+ export async function runSetupWizard(): Promise<void> {
38
+ // Dynamic imports — these are optional CLI deps
39
+ const { default: inquirer } = await import('inquirer');
40
+ const { default: ora } = await import('ora');
41
+ const { default: chalk } = await import('chalk');
42
+
43
+ // ─── Banner ──────────────────────────────────────
44
+ console.log('');
45
+ console.log(chalk.bold(' AgenticMail Enterprise'));
46
+ console.log(chalk.dim(' AI Agent Identity & Email for Organizations'));
47
+ console.log('');
48
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
49
+ console.log('');
50
+
51
+ // ─── Step 1: Company ─────────────────────────────
52
+ const company = await promptCompanyInfo(inquirer, chalk);
53
+
54
+ // ─── Step 2: Database ────────────────────────────
55
+ const database = await promptDatabase(inquirer, chalk);
56
+
57
+ // ─── Step 3: Deployment ──────────────────────────
58
+ const { target: deployTarget } = await promptDeployment(inquirer, chalk);
59
+
60
+ // ─── Step 4: Custom Domain ───────────────────────
61
+ const domain = await promptDomain(inquirer, chalk, deployTarget);
62
+
63
+ // ─── Provision ───────────────────────────────────
64
+ console.log('');
65
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
66
+ console.log('');
67
+
68
+ const result = await provision(
69
+ { company, database, deployTarget, domain },
70
+ ora,
71
+ chalk,
72
+ );
73
+
74
+ if (!result.success) {
75
+ console.error('');
76
+ console.error(chalk.red(` Setup failed: ${result.error}`));
77
+ console.error(chalk.dim(' Check your database connection and try again.'));
78
+ process.exit(1);
79
+ }
80
+
81
+ console.log('');
82
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Setup Wizard — Provisioning
3
+ *
4
+ * Connects to the database, runs migrations, creates the admin account,
5
+ * and deploys to the selected target. This is the "do the work" step
6
+ * after all prompts are collected.
7
+ */
8
+
9
+ import { randomUUID } from 'crypto';
10
+ import type { DatabaseAdapter } from '../db/adapter.js';
11
+ import type { CompanyInfo } from './company.js';
12
+ import type { DatabaseSelection } from './database.js';
13
+ import type { DeployTarget } from './deployment.js';
14
+ import type { DomainSelection } from './domain.js';
15
+
16
+ export interface ProvisionConfig {
17
+ company: CompanyInfo;
18
+ database: DatabaseSelection;
19
+ deployTarget: DeployTarget;
20
+ domain: DomainSelection;
21
+ }
22
+
23
+ export interface ProvisionResult {
24
+ success: boolean;
25
+ url?: string;
26
+ error?: string;
27
+ jwtSecret: string;
28
+ db: DatabaseAdapter;
29
+ serverClose?: () => void;
30
+ }
31
+
32
+ export async function provision(
33
+ config: ProvisionConfig,
34
+ ora: any,
35
+ chalk: any,
36
+ ): Promise<ProvisionResult> {
37
+ const spinner = ora('Connecting to database...').start();
38
+ const jwtSecret = randomUUID() + randomUUID();
39
+
40
+ try {
41
+ // ─── Database ──────────────────────────────────
42
+ const { createAdapter } = await import('../db/factory.js');
43
+ const db = await createAdapter(config.database as any);
44
+ spinner.text = 'Running migrations...';
45
+ await db.migrate();
46
+ spinner.succeed('Database ready');
47
+
48
+ // ─── Company Settings ──────────────────────────
49
+ spinner.start('Creating company...');
50
+ await db.updateSettings({
51
+ name: config.company.companyName,
52
+ subdomain: config.company.subdomain,
53
+ domain: config.domain.customDomain,
54
+ });
55
+ spinner.succeed('Company created');
56
+
57
+ // ─── Admin Account ─────────────────────────────
58
+ spinner.start('Creating admin account...');
59
+ const admin = await db.createUser({
60
+ email: config.company.adminEmail,
61
+ name: 'Admin',
62
+ role: 'owner',
63
+ password: config.company.adminPassword,
64
+ });
65
+ await db.logEvent({
66
+ actor: admin.id,
67
+ actorType: 'system',
68
+ action: 'setup.complete',
69
+ resource: `company:${config.company.subdomain}`,
70
+ details: {
71
+ dbType: config.database.type,
72
+ deployTarget: config.deployTarget,
73
+ companyName: config.company.companyName,
74
+ },
75
+ });
76
+ spinner.succeed('Admin account created');
77
+
78
+ // ─── Deploy ────────────────────────────────────
79
+ const result = await deploy(config, db, jwtSecret, spinner, chalk);
80
+
81
+ return {
82
+ success: true,
83
+ url: result.url,
84
+ jwtSecret,
85
+ db,
86
+ serverClose: result.close,
87
+ };
88
+ } catch (err: any) {
89
+ spinner.fail(`Setup failed: ${err.message}`);
90
+ return {
91
+ success: false,
92
+ error: err.message,
93
+ jwtSecret,
94
+ db: null as any,
95
+ };
96
+ }
97
+ }
98
+
99
+ // ─── Deploy to selected target ──────────────────────
100
+
101
+ interface DeployResult {
102
+ url?: string;
103
+ close?: () => void;
104
+ }
105
+
106
+ async function deploy(
107
+ config: ProvisionConfig,
108
+ db: DatabaseAdapter,
109
+ jwtSecret: string,
110
+ spinner: any,
111
+ chalk: any,
112
+ ): Promise<DeployResult> {
113
+ const { deployTarget, company, database, domain } = config;
114
+
115
+ // ── Cloud ─────────────────────────────────────────
116
+ if (deployTarget === 'cloud') {
117
+ spinner.start('Deploying to AgenticMail Cloud...');
118
+ const { deployToCloud } = await import('../deploy/managed.js');
119
+ const result = await deployToCloud({
120
+ subdomain: company.subdomain,
121
+ plan: 'free',
122
+ dbType: database.type,
123
+ dbConnectionString: database.connectionString || '',
124
+ jwtSecret,
125
+ });
126
+ spinner.succeed(`Deployed to ${result.url}`);
127
+
128
+ printCloudSuccess(chalk, result.url, company.adminEmail, domain.customDomain, company.subdomain);
129
+ return { url: result.url };
130
+ }
131
+
132
+ // ── Docker ────────────────────────────────────────
133
+ if (deployTarget === 'docker') {
134
+ const { generateDockerCompose } = await import('../deploy/managed.js');
135
+ const compose = generateDockerCompose({
136
+ dbType: database.type,
137
+ dbConnectionString: database.connectionString || '',
138
+ port: 3000,
139
+ jwtSecret,
140
+ });
141
+ const { writeFileSync } = await import('fs');
142
+ writeFileSync('docker-compose.yml', compose);
143
+ spinner.succeed('docker-compose.yml generated');
144
+
145
+ console.log('');
146
+ console.log(chalk.green.bold(' Docker deployment ready!'));
147
+ console.log('');
148
+ console.log(` Run: ${chalk.cyan('docker compose up -d')}`);
149
+ console.log(` Dashboard: ${chalk.cyan('http://localhost:3000')}`);
150
+ return { url: 'http://localhost:3000' };
151
+ }
152
+
153
+ // ── Fly.io ────────────────────────────────────────
154
+ if (deployTarget === 'fly') {
155
+ const { generateFlyToml } = await import('../deploy/managed.js');
156
+ const flyToml = generateFlyToml(`am-${company.subdomain}`, 'iad');
157
+ const { writeFileSync } = await import('fs');
158
+ writeFileSync('fly.toml', flyToml);
159
+ spinner.succeed('fly.toml generated');
160
+
161
+ console.log('');
162
+ console.log(chalk.green.bold(' Fly.io deployment ready!'));
163
+ console.log('');
164
+ console.log(` 1. ${chalk.cyan('fly launch --copy-config')}`);
165
+ console.log(` 2. ${chalk.cyan(`fly secrets set DATABASE_URL="${database.connectionString}" JWT_SECRET="${jwtSecret}"`)}`);
166
+ console.log(` 3. ${chalk.cyan('fly deploy')}`);
167
+ return {};
168
+ }
169
+
170
+ // ── Railway ───────────────────────────────────────
171
+ if (deployTarget === 'railway') {
172
+ const { generateRailwayConfig } = await import('../deploy/managed.js');
173
+ const railwayConfig = generateRailwayConfig();
174
+ const { writeFileSync } = await import('fs');
175
+ writeFileSync('railway.toml', railwayConfig);
176
+ spinner.succeed('railway.toml generated');
177
+
178
+ console.log('');
179
+ console.log(chalk.green.bold(' Railway deployment ready!'));
180
+ console.log('');
181
+ console.log(` 1. ${chalk.cyan('railway init')}`);
182
+ console.log(` 2. ${chalk.cyan('railway link')}`);
183
+ console.log(` 3. ${chalk.cyan('railway up')}`);
184
+ return {};
185
+ }
186
+
187
+ // ── Local ─────────────────────────────────────────
188
+ spinner.start('Starting local server...');
189
+ const { createServer } = await import('../server.js');
190
+ const server = createServer({ port: 3000, db, jwtSecret });
191
+ const handle = await server.start();
192
+ spinner.succeed('Server running');
193
+
194
+ console.log('');
195
+ console.log(chalk.green.bold(' AgenticMail Enterprise is running!'));
196
+ console.log('');
197
+ console.log(` ${chalk.bold('Dashboard:')} ${chalk.cyan('http://localhost:3000')}`);
198
+ console.log(` ${chalk.bold('API:')} ${chalk.cyan('http://localhost:3000/api')}`);
199
+ console.log(` ${chalk.bold('Admin:')} ${company.adminEmail}`);
200
+ console.log('');
201
+ console.log(chalk.dim(' Press Ctrl+C to stop'));
202
+
203
+ return { url: 'http://localhost:3000', close: handle.close };
204
+ }
205
+
206
+ // ─── Success Messages ───────────────────────────────
207
+
208
+ function printCloudSuccess(
209
+ chalk: any,
210
+ url: string,
211
+ adminEmail: string,
212
+ customDomain?: string,
213
+ subdomain?: string,
214
+ ) {
215
+ console.log('');
216
+ console.log(chalk.green.bold(' Your dashboard is live!'));
217
+ console.log('');
218
+ console.log(` ${chalk.bold('URL:')} ${chalk.cyan(url)}`);
219
+ console.log(` ${chalk.bold('Admin:')} ${adminEmail}`);
220
+ console.log(` ${chalk.bold('Password:')} (the one you just set)`);
221
+ if (customDomain) {
222
+ console.log('');
223
+ console.log(chalk.dim(` To use ${customDomain}:`));
224
+ console.log(chalk.dim(` Add CNAME: ${customDomain} → ${subdomain}.agenticmail.cloud`));
225
+ }
226
+ }