posthorn 0.1.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/dist/index.d.ts +1 -0
- package/dist/index.js +646 -0
- package/package.json +43 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk7 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/commands/auth.ts
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
|
|
11
|
+
// src/config.ts
|
|
12
|
+
import Conf from "conf";
|
|
13
|
+
var defaults = {
|
|
14
|
+
apiKey: null,
|
|
15
|
+
apiUrl: "https://api-production-08f2.up.railway.app",
|
|
16
|
+
userId: null,
|
|
17
|
+
setupStep: 0,
|
|
18
|
+
cloudflareConnected: false,
|
|
19
|
+
workspaceConnected: false,
|
|
20
|
+
workspaceAccountId: null
|
|
21
|
+
};
|
|
22
|
+
var conf = new Conf({
|
|
23
|
+
projectName: "outbound-engine",
|
|
24
|
+
defaults
|
|
25
|
+
});
|
|
26
|
+
function getConfig() {
|
|
27
|
+
return {
|
|
28
|
+
apiKey: conf.get("apiKey"),
|
|
29
|
+
apiUrl: conf.get("apiUrl"),
|
|
30
|
+
userId: conf.get("userId"),
|
|
31
|
+
setupStep: conf.get("setupStep"),
|
|
32
|
+
cloudflareConnected: conf.get("cloudflareConnected"),
|
|
33
|
+
workspaceConnected: conf.get("workspaceConnected"),
|
|
34
|
+
workspaceAccountId: conf.get("workspaceAccountId")
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function setConfig(updates) {
|
|
38
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
39
|
+
conf.set(key, value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function resetConfig() {
|
|
43
|
+
conf.clear();
|
|
44
|
+
}
|
|
45
|
+
function getConfigPath() {
|
|
46
|
+
return conf.path;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/commands/auth.ts
|
|
50
|
+
async function register(options) {
|
|
51
|
+
const apiUrl = options.url ?? "https://api-production-08f2.up.railway.app";
|
|
52
|
+
const spinner = ora("Creating account...").start();
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(`${apiUrl}/api/auth/register`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" }
|
|
57
|
+
});
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
spinner.fail(data.error ?? "Registration failed");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setConfig({
|
|
64
|
+
apiKey: data.api_key,
|
|
65
|
+
apiUrl,
|
|
66
|
+
userId: data.user.id,
|
|
67
|
+
setupStep: 1
|
|
68
|
+
});
|
|
69
|
+
spinner.succeed("Account created!");
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(` API Key: ${chalk.cyan(data.api_key)}`);
|
|
72
|
+
console.log(` Tier: ${chalk.yellow(data.user.tier)}`);
|
|
73
|
+
console.log();
|
|
74
|
+
console.log(chalk.dim(" Save this API key \u2014 it won't be shown again."));
|
|
75
|
+
console.log(chalk.dim(` Config stored at: ${getConfigPath()}`));
|
|
76
|
+
} catch (err) {
|
|
77
|
+
spinner.fail(`Could not reach ${apiUrl}`);
|
|
78
|
+
console.log(chalk.dim(` Is the server running? Try: posthorn auth register --url <your-api-url>`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function status() {
|
|
82
|
+
const config = getConfig();
|
|
83
|
+
if (!config.apiKey) {
|
|
84
|
+
console.log(chalk.yellow("Not logged in."));
|
|
85
|
+
console.log(` Run: ${chalk.cyan("posthorn auth register")}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.log(chalk.bold("Account"));
|
|
89
|
+
console.log(` API URL: ${config.apiUrl}`);
|
|
90
|
+
console.log(` API Key: ${config.apiKey.slice(0, 15)}...`);
|
|
91
|
+
console.log(` User ID: ${config.userId ?? "unknown"}`);
|
|
92
|
+
console.log(` Cloudflare: ${config.cloudflareConnected ? chalk.green("connected") : chalk.dim("not connected")}`);
|
|
93
|
+
console.log(` Workspace: ${config.workspaceConnected ? chalk.green("connected") : chalk.dim("not connected")}`);
|
|
94
|
+
console.log(` Setup Step: ${config.setupStep}`);
|
|
95
|
+
console.log();
|
|
96
|
+
console.log(chalk.dim(` Config: ${getConfigPath()}`));
|
|
97
|
+
}
|
|
98
|
+
async function logout() {
|
|
99
|
+
resetConfig();
|
|
100
|
+
console.log(chalk.green("Logged out. Config cleared."));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/commands/accounts.ts
|
|
104
|
+
import chalk2 from "chalk";
|
|
105
|
+
import ora2 from "ora";
|
|
106
|
+
|
|
107
|
+
// src/api.ts
|
|
108
|
+
var ApiError = class extends Error {
|
|
109
|
+
constructor(statusCode, message, body) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.statusCode = statusCode;
|
|
112
|
+
this.body = body;
|
|
113
|
+
}
|
|
114
|
+
statusCode;
|
|
115
|
+
body;
|
|
116
|
+
};
|
|
117
|
+
async function api(path, options = {}) {
|
|
118
|
+
const config = getConfig();
|
|
119
|
+
if (!config.apiKey) {
|
|
120
|
+
throw new Error("Not logged in. Run: posthorn setup");
|
|
121
|
+
}
|
|
122
|
+
const url = `${config.apiUrl}${path}`;
|
|
123
|
+
const res = await fetch(url, {
|
|
124
|
+
method: options.method ?? "GET",
|
|
125
|
+
headers: {
|
|
126
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
127
|
+
"Content-Type": "application/json"
|
|
128
|
+
},
|
|
129
|
+
...options.body && { body: JSON.stringify(options.body) }
|
|
130
|
+
});
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
throw new ApiError(
|
|
134
|
+
res.status,
|
|
135
|
+
data.error ?? `Request failed with ${res.status}`,
|
|
136
|
+
data
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return data;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/commands/accounts.ts
|
|
143
|
+
async function connectCloudflare(token, options) {
|
|
144
|
+
const spinner = ora2("Connecting Cloudflare...").start();
|
|
145
|
+
const data = await api("/api/accounts", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: {
|
|
148
|
+
provider: "cloudflare",
|
|
149
|
+
label: options.label ?? "main",
|
|
150
|
+
credentials: { apiToken: token }
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
const verify = await api(
|
|
154
|
+
`/api/accounts/${data.account.id}/verify`,
|
|
155
|
+
{ method: "POST" }
|
|
156
|
+
);
|
|
157
|
+
if (verify.valid) {
|
|
158
|
+
setConfig({ cloudflareConnected: true });
|
|
159
|
+
spinner.succeed("Cloudflare connected!");
|
|
160
|
+
console.log(` Account ID: ${chalk2.dim(data.account.id)}`);
|
|
161
|
+
if (verify.accountId) console.log(` CF Account: ${chalk2.dim(verify.accountId)}`);
|
|
162
|
+
} else {
|
|
163
|
+
spinner.fail("Cloudflare token is invalid. Check permissions.");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function connectWorkspace(adminEmail) {
|
|
167
|
+
const spinner = ora2("Connecting Google Workspace...").start();
|
|
168
|
+
const data = await api("/api/accounts/workspace", {
|
|
169
|
+
method: "POST",
|
|
170
|
+
body: { adminEmail }
|
|
171
|
+
});
|
|
172
|
+
setConfig({
|
|
173
|
+
workspaceConnected: true,
|
|
174
|
+
workspaceAccountId: data.account.id
|
|
175
|
+
});
|
|
176
|
+
spinner.succeed(data.message);
|
|
177
|
+
console.log(` Customer ID: ${chalk2.dim(data.customerId)}`);
|
|
178
|
+
console.log(` Admin: ${chalk2.dim(data.adminEmail)}`);
|
|
179
|
+
}
|
|
180
|
+
async function listAccounts() {
|
|
181
|
+
const data = await api("/api/accounts");
|
|
182
|
+
if (data.accounts.length === 0) {
|
|
183
|
+
console.log(chalk2.dim(" No connected accounts."));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
console.log(chalk2.bold("Connected Accounts\n"));
|
|
187
|
+
for (const a of data.accounts) {
|
|
188
|
+
const statusColor = a.status === "active" ? chalk2.green : chalk2.red;
|
|
189
|
+
console.log(` ${chalk2.cyan(a.provider)} ${a.label} ${statusColor(a.status)} ${chalk2.dim(a.id)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/commands/domains.ts
|
|
194
|
+
import chalk3 from "chalk";
|
|
195
|
+
import ora3 from "ora";
|
|
196
|
+
async function listDomains() {
|
|
197
|
+
const spinner = ora3("Fetching domains...").start();
|
|
198
|
+
const data = await api("/api/domains");
|
|
199
|
+
spinner.stop();
|
|
200
|
+
if (data.domains.length === 0) {
|
|
201
|
+
console.log(chalk3.dim(" No domains yet. Run: posthorn domains buy <domain>"));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log(chalk3.bold("Domains\n"));
|
|
205
|
+
for (const d of data.domains) {
|
|
206
|
+
const statusColor = d.status === "ready" ? chalk3.green : d.status === "failed" ? chalk3.red : chalk3.yellow;
|
|
207
|
+
console.log(` ${chalk3.cyan(d.name)} ${statusColor(d.status)} ${chalk3.dim(d.id)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function buyDomain(domain, options) {
|
|
211
|
+
if (!options.contact) {
|
|
212
|
+
console.log(chalk3.red("Contact info required for domain purchase."));
|
|
213
|
+
console.log(chalk3.dim(` Use: posthorn domains buy example.com --cloudflare <account-id> --contact '{"email":"...","phone":"...","name":"...","street":"...","city":"...","state":"...","postalCode":"...","countryCode":"US"}'`));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let contact;
|
|
217
|
+
try {
|
|
218
|
+
contact = JSON.parse(options.contact);
|
|
219
|
+
} catch {
|
|
220
|
+
console.log(chalk3.red("Invalid contact JSON"));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const spinner = ora3(`Purchasing ${domain}...`).start();
|
|
224
|
+
const data = await api("/api/domains", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: {
|
|
227
|
+
name: domain,
|
|
228
|
+
cloudflareAccountId: options.cloudflare,
|
|
229
|
+
contact
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
spinner.succeed(`Domain purchase started: ${chalk3.cyan(domain)}`);
|
|
233
|
+
console.log(` Job ID: ${chalk3.dim(data.jobId)}`);
|
|
234
|
+
console.log(` Status: ${chalk3.yellow(data.domain.status)}`);
|
|
235
|
+
console.log();
|
|
236
|
+
console.log(chalk3.dim(" DNS records will be configured automatically."));
|
|
237
|
+
console.log(chalk3.dim(` Check progress: posthorn domains get ${data.domain.id}`));
|
|
238
|
+
}
|
|
239
|
+
async function connectDomain(domain, options) {
|
|
240
|
+
const spinner = ora3(`Connecting ${domain}...`).start();
|
|
241
|
+
const body = {
|
|
242
|
+
name: domain,
|
|
243
|
+
cloudflareAccountId: options.cloudflare
|
|
244
|
+
};
|
|
245
|
+
if (options.workspace) body.workspaceAccountId = options.workspace;
|
|
246
|
+
const data = await api("/api/domains/connect", {
|
|
247
|
+
method: "POST",
|
|
248
|
+
body
|
|
249
|
+
});
|
|
250
|
+
spinner.succeed(data.message);
|
|
251
|
+
console.log(` Domain ID: ${chalk3.dim(data.domain.id)}`);
|
|
252
|
+
}
|
|
253
|
+
async function getDomain(domainId) {
|
|
254
|
+
const data = await api(`/api/domains/${domainId}`);
|
|
255
|
+
const d = data.domain;
|
|
256
|
+
console.log(chalk3.bold("Domain\n"));
|
|
257
|
+
console.log(` Name: ${chalk3.cyan(d.name)}`);
|
|
258
|
+
console.log(` Status: ${d.status === "ready" ? chalk3.green(d.status) : chalk3.yellow(d.status)}`);
|
|
259
|
+
console.log(` ID: ${chalk3.dim(d.id)}`);
|
|
260
|
+
if (d.cloudflare_zone_id) console.log(` Zone: ${chalk3.dim(d.cloudflare_zone_id)}`);
|
|
261
|
+
}
|
|
262
|
+
async function checkDomains(domains2, options) {
|
|
263
|
+
const spinner = ora3("Checking availability...").start();
|
|
264
|
+
const data = await api("/api/domains/check", {
|
|
265
|
+
method: "POST",
|
|
266
|
+
body: { domains: domains2, cloudflareAccountId: options.cloudflare }
|
|
267
|
+
});
|
|
268
|
+
spinner.stop();
|
|
269
|
+
console.log(chalk3.bold("Domain Availability\n"));
|
|
270
|
+
for (const r of data.results) {
|
|
271
|
+
const icon = r.available ? chalk3.green(" \u2713") : chalk3.red(" \u2717");
|
|
272
|
+
const price = r.price ? chalk3.dim(`$${r.price}/yr`) : "";
|
|
273
|
+
console.log(`${icon} ${r.domain} ${price}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/commands/mailboxes.ts
|
|
278
|
+
import chalk4 from "chalk";
|
|
279
|
+
import ora4 from "ora";
|
|
280
|
+
async function listMailboxes(domainId) {
|
|
281
|
+
const data = await api(
|
|
282
|
+
`/api/domains/${domainId}/mailboxes`
|
|
283
|
+
);
|
|
284
|
+
if (data.mailboxes.length === 0) {
|
|
285
|
+
console.log(chalk4.dim(" No mailboxes yet."));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
console.log(chalk4.bold("Mailboxes\n"));
|
|
289
|
+
for (const m of data.mailboxes) {
|
|
290
|
+
const statusColor = m.status === "active" ? chalk4.green : chalk4.yellow;
|
|
291
|
+
console.log(` ${chalk4.cyan(m.email_address)} ${statusColor(m.status)} ${chalk4.dim(m.id)}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function createMailbox(domainId, options) {
|
|
295
|
+
const localPart = options.email.split("@")[0];
|
|
296
|
+
const spinner = ora4(`Creating ${options.email}...`).start();
|
|
297
|
+
const data = await api(`/api/domains/${domainId}/mailboxes`, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
body: {
|
|
300
|
+
localPart,
|
|
301
|
+
firstName: options.first,
|
|
302
|
+
lastName: options.last
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
spinner.succeed(`Mailbox created: ${chalk4.cyan(data.credentials.email)}`);
|
|
306
|
+
console.log();
|
|
307
|
+
console.log(` ${chalk4.bold("Email:")} ${data.credentials.email}`);
|
|
308
|
+
console.log(` ${chalk4.bold("Password:")} ${chalk4.yellow(data.credentials.password)}`);
|
|
309
|
+
console.log();
|
|
310
|
+
console.log(chalk4.red(" Save this password now \u2014 it will not be shown again."));
|
|
311
|
+
console.log(chalk4.dim(" Login at: https://mail.google.com"));
|
|
312
|
+
}
|
|
313
|
+
async function sendEmail(mailboxId, options) {
|
|
314
|
+
const spinner = ora4("Sending...").start();
|
|
315
|
+
const data = await api(
|
|
316
|
+
`/api/mailboxes/${mailboxId}/send`,
|
|
317
|
+
{
|
|
318
|
+
method: "POST",
|
|
319
|
+
body: {
|
|
320
|
+
to: options.to,
|
|
321
|
+
subject: options.subject,
|
|
322
|
+
html: `<p>${options.body}</p>`
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
spinner.succeed("Email sent!");
|
|
327
|
+
console.log(` From: ${chalk4.cyan(data.from)}`);
|
|
328
|
+
console.log(` To: ${data.to}`);
|
|
329
|
+
console.log(` Message-ID: ${chalk4.dim(data.messageId)}`);
|
|
330
|
+
}
|
|
331
|
+
async function provisionCredentials(mailboxId, options) {
|
|
332
|
+
const spinner = ora4("Provisioning credentials...").start();
|
|
333
|
+
const data = await api(`/api/mailboxes/${mailboxId}/credentials`, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
body: { authType: options.type ?? "xoauth2" }
|
|
336
|
+
});
|
|
337
|
+
if (data.connectivity.smtp && data.connectivity.imap) {
|
|
338
|
+
spinner.succeed("Credentials provisioned \u2014 SMTP and IMAP connected!");
|
|
339
|
+
} else {
|
|
340
|
+
spinner.warn("Credentials provisioned but connectivity issues:");
|
|
341
|
+
for (const err of data.connectivity.errors) {
|
|
342
|
+
console.log(` ${chalk4.red("!")} ${err}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/commands/warmup.ts
|
|
348
|
+
import chalk5 from "chalk";
|
|
349
|
+
import ora5 from "ora";
|
|
350
|
+
async function startWarmup(mailboxId) {
|
|
351
|
+
const spinner = ora5("Starting warmup...").start();
|
|
352
|
+
const data = await api("/api/warmup/campaigns", {
|
|
353
|
+
method: "POST",
|
|
354
|
+
body: { mailboxId, autoStart: true }
|
|
355
|
+
});
|
|
356
|
+
spinner.succeed("Warmup started!");
|
|
357
|
+
console.log(` Campaign ID: ${chalk5.dim(data.campaign.id)}`);
|
|
358
|
+
console.log(` Status: ${chalk5.green("active")}`);
|
|
359
|
+
console.log(chalk5.dim(" Emails will ramp up gradually over 30 days."));
|
|
360
|
+
}
|
|
361
|
+
async function pauseWarmup(campaignId) {
|
|
362
|
+
await api(`/api/warmup/campaigns/${campaignId}`, {
|
|
363
|
+
method: "PATCH",
|
|
364
|
+
body: { action: "pause" }
|
|
365
|
+
});
|
|
366
|
+
console.log(chalk5.yellow("Warmup paused."));
|
|
367
|
+
}
|
|
368
|
+
async function warmupStats(campaignId) {
|
|
369
|
+
const data = await api(`/api/warmup/campaigns/${campaignId}`);
|
|
370
|
+
const s = data.stats;
|
|
371
|
+
const c = data.campaign;
|
|
372
|
+
console.log(chalk5.bold("Warmup Stats\n"));
|
|
373
|
+
console.log(` Mailbox: ${chalk5.cyan(c.email_address)}`);
|
|
374
|
+
console.log(` Status: ${c.status === "active" ? chalk5.green("active") : chalk5.yellow(c.status)}`);
|
|
375
|
+
console.log(` Day: ${s.currentDay} / 30`);
|
|
376
|
+
console.log(` Sent: ${s.emailsSent}`);
|
|
377
|
+
console.log(` Received: ${s.emailsReceived}`);
|
|
378
|
+
console.log(` Spam Saves: ${s.spamRescues}`);
|
|
379
|
+
console.log(` Placement: ${s.inboxPlacementRate !== null ? `${s.inboxPlacementRate}%` : chalk5.dim("n/a")}`);
|
|
380
|
+
console.log(` Reputation: ${s.reputationScore}/100`);
|
|
381
|
+
if (s.dailyBreakdown.length > 0) {
|
|
382
|
+
console.log(chalk5.bold("\n Daily Breakdown\n"));
|
|
383
|
+
console.log(chalk5.dim(" Date Sent Inbox Spam Replied Placement"));
|
|
384
|
+
for (const d of s.dailyBreakdown.slice(-14)) {
|
|
385
|
+
const date = d.date.slice(0, 10).padEnd(14);
|
|
386
|
+
const placement = d.placementRate !== null ? `${d.placementRate}%` : "-";
|
|
387
|
+
console.log(` ${date}${String(d.sent).padEnd(6)}${String(d.delivered).padEnd(7)}${String(d.spamDetected + d.spamRescued).padEnd(6)}${String(d.replied).padEnd(9)}${placement}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function listCampaigns() {
|
|
392
|
+
const data = await api("/api/warmup/campaigns");
|
|
393
|
+
if (data.campaigns.length === 0) {
|
|
394
|
+
console.log(chalk5.dim(" No warmup campaigns. Run: posthorn warmup start <mailbox-id>"));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
console.log(chalk5.bold("Warmup Campaigns\n"));
|
|
398
|
+
for (const c of data.campaigns) {
|
|
399
|
+
const statusColor = c.status === "active" ? chalk5.green : c.status === "paused" ? chalk5.yellow : chalk5.dim;
|
|
400
|
+
console.log(
|
|
401
|
+
` ${chalk5.cyan(c.email_address)} ${statusColor(c.status)} day ${c.current_day} rep ${c.reputation_score ?? "-"} ${chalk5.dim(c.id)}`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/commands/setup.ts
|
|
407
|
+
import chalk6 from "chalk";
|
|
408
|
+
import ora6 from "ora";
|
|
409
|
+
import inquirer from "inquirer";
|
|
410
|
+
var STEPS = [
|
|
411
|
+
"Account",
|
|
412
|
+
"Cloudflare",
|
|
413
|
+
"Google Workspace",
|
|
414
|
+
"Domain",
|
|
415
|
+
"DKIM",
|
|
416
|
+
"Mailboxes",
|
|
417
|
+
"Warmup"
|
|
418
|
+
];
|
|
419
|
+
function printProgress(currentStep) {
|
|
420
|
+
console.log();
|
|
421
|
+
for (let i = 0; i < STEPS.length; i++) {
|
|
422
|
+
const icon = i < currentStep ? chalk6.green(" \u2713") : i === currentStep ? chalk6.cyan(" \u2192") : chalk6.dim(" \u25CB");
|
|
423
|
+
const label = i === currentStep ? chalk6.bold(STEPS[i]) : i < currentStep ? STEPS[i] : chalk6.dim(STEPS[i]);
|
|
424
|
+
console.log(`${icon} Step ${i + 1}: ${label}`);
|
|
425
|
+
}
|
|
426
|
+
console.log();
|
|
427
|
+
}
|
|
428
|
+
async function setup(options) {
|
|
429
|
+
console.log(chalk6.bold("\n Posthorn Setup\n"));
|
|
430
|
+
const config = getConfig();
|
|
431
|
+
let step = config.setupStep;
|
|
432
|
+
if (step < 1) {
|
|
433
|
+
printProgress(0);
|
|
434
|
+
if (!config.apiKey) {
|
|
435
|
+
await register({ url: options.url });
|
|
436
|
+
} else {
|
|
437
|
+
console.log(chalk6.green(" Account already exists."));
|
|
438
|
+
}
|
|
439
|
+
setConfig({ setupStep: 1 });
|
|
440
|
+
step = 1;
|
|
441
|
+
}
|
|
442
|
+
if (step < 2) {
|
|
443
|
+
printProgress(1);
|
|
444
|
+
if (!config.cloudflareConnected) {
|
|
445
|
+
const { hasCf } = await inquirer.prompt([
|
|
446
|
+
{ type: "confirm", name: "hasCf", message: "Do you have a Cloudflare account?", default: true }
|
|
447
|
+
]);
|
|
448
|
+
if (!hasCf) {
|
|
449
|
+
console.log();
|
|
450
|
+
console.log(" 1. Go to " + chalk6.cyan("https://dash.cloudflare.com/sign-up"));
|
|
451
|
+
console.log(" 2. Create a free account");
|
|
452
|
+
console.log(" 3. Add a payment method if you want to purchase domains");
|
|
453
|
+
console.log();
|
|
454
|
+
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Ready to continue?" }]);
|
|
455
|
+
}
|
|
456
|
+
console.log();
|
|
457
|
+
console.log(" Create an API token:");
|
|
458
|
+
console.log(" 1. Go to " + chalk6.cyan("https://dash.cloudflare.com/profile/api-tokens"));
|
|
459
|
+
console.log(' 2. Click "Create Token" \u2192 "Create Custom Token"');
|
|
460
|
+
console.log(" 3. Add these permissions:");
|
|
461
|
+
console.log(" - Account \u2192 Registrar: Domains \u2192 Admin");
|
|
462
|
+
console.log(" - Account \u2192 Billing \u2192 Edit");
|
|
463
|
+
console.log(" - Zone \u2192 Zone \u2192 Edit");
|
|
464
|
+
console.log(" - Zone \u2192 DNS \u2192 Edit");
|
|
465
|
+
console.log(" 4. Account/Zone Resources: Include All");
|
|
466
|
+
console.log(' 5. Click "Continue to summary" \u2192 "Create Token"');
|
|
467
|
+
console.log();
|
|
468
|
+
const { token } = await inquirer.prompt([
|
|
469
|
+
{ type: "password", name: "token", message: "Paste your Cloudflare API token:", mask: "*" }
|
|
470
|
+
]);
|
|
471
|
+
await connectCloudflare(token);
|
|
472
|
+
} else {
|
|
473
|
+
console.log(chalk6.green(" Cloudflare already connected."));
|
|
474
|
+
}
|
|
475
|
+
setConfig({ setupStep: 2 });
|
|
476
|
+
step = 2;
|
|
477
|
+
}
|
|
478
|
+
if (step < 3) {
|
|
479
|
+
printProgress(2);
|
|
480
|
+
if (!config.workspaceConnected) {
|
|
481
|
+
const { hasWs } = await inquirer.prompt([
|
|
482
|
+
{ type: "confirm", name: "hasWs", message: "Do you have a Google Workspace account?", default: true }
|
|
483
|
+
]);
|
|
484
|
+
if (!hasWs) {
|
|
485
|
+
console.log();
|
|
486
|
+
console.log(" Set up a new Google Workspace:");
|
|
487
|
+
console.log(" 1. Go to " + chalk6.cyan("https://workspace.google.com") + " \u2192 Get Started");
|
|
488
|
+
console.log(' 2. Enter your business name, select "Just you"');
|
|
489
|
+
console.log(' 3. Click "Set up using your existing domain" and enter your domain');
|
|
490
|
+
console.log(" 4. Create your first email (this will be your outbound email)");
|
|
491
|
+
console.log(" 5. Complete billing setup (14-day free trial available)");
|
|
492
|
+
console.log();
|
|
493
|
+
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Done setting up Workspace?" }]);
|
|
494
|
+
}
|
|
495
|
+
console.log();
|
|
496
|
+
console.log(" Set up domain-wide delegation:");
|
|
497
|
+
console.log(" 1. Go to " + chalk6.cyan("admin.google.com") + " \u2192 Security \u2192 API controls");
|
|
498
|
+
console.log(" 2. Click Domain-wide delegation \u2192 Add new");
|
|
499
|
+
console.log(" 3. Client ID: " + chalk6.yellow("110137377718772968374"));
|
|
500
|
+
console.log(" 4. Scopes: " + chalk6.dim("https://mail.google.com/,https://www.googleapis.com/auth/admin.directory.user,https://www.googleapis.com/auth/admin.directory.domain,https://www.googleapis.com/auth/siteverification"));
|
|
501
|
+
console.log(" 5. Click Authorize");
|
|
502
|
+
console.log();
|
|
503
|
+
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Done with delegation?" }]);
|
|
504
|
+
const { adminEmail } = await inquirer.prompt([
|
|
505
|
+
{ type: "input", name: "adminEmail", message: "Your Workspace admin email:" }
|
|
506
|
+
]);
|
|
507
|
+
await connectWorkspace(adminEmail);
|
|
508
|
+
} else {
|
|
509
|
+
console.log(chalk6.green(" Google Workspace already connected."));
|
|
510
|
+
}
|
|
511
|
+
setConfig({ setupStep: 3 });
|
|
512
|
+
step = 3;
|
|
513
|
+
}
|
|
514
|
+
if (step < 4) {
|
|
515
|
+
printProgress(3);
|
|
516
|
+
const { domainChoice } = await inquirer.prompt([
|
|
517
|
+
{
|
|
518
|
+
type: "list",
|
|
519
|
+
name: "domainChoice",
|
|
520
|
+
message: "Domain setup:",
|
|
521
|
+
choices: [
|
|
522
|
+
{ name: "Buy a new domain", value: "buy" },
|
|
523
|
+
{ name: "Connect an existing domain (must be on Cloudflare)", value: "connect" },
|
|
524
|
+
{ name: "Skip for now", value: "skip" }
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
]);
|
|
528
|
+
if (domainChoice === "connect") {
|
|
529
|
+
const { domain } = await inquirer.prompt([
|
|
530
|
+
{ type: "input", name: "domain", message: "Domain name:" }
|
|
531
|
+
]);
|
|
532
|
+
const accounts2 = await api("/api/accounts");
|
|
533
|
+
const cfAccount = accounts2.accounts.find((a) => a.provider === "cloudflare");
|
|
534
|
+
if (cfAccount) {
|
|
535
|
+
const spinner = ora6(`Connecting ${domain}...`).start();
|
|
536
|
+
try {
|
|
537
|
+
const data = await api("/api/domains/connect", {
|
|
538
|
+
method: "POST",
|
|
539
|
+
body: {
|
|
540
|
+
name: domain,
|
|
541
|
+
cloudflareAccountId: cfAccount.id,
|
|
542
|
+
workspaceAccountId: config.workspaceAccountId
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
spinner.succeed(data.message);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
spinner.fail(err.message);
|
|
548
|
+
console.log(chalk6.dim("\n If the domain isn't on Cloudflare yet:"));
|
|
549
|
+
console.log(chalk6.dim(" 1. Go to dash.cloudflare.com \u2192 Add a site \u2192 enter your domain"));
|
|
550
|
+
console.log(chalk6.dim(" 2. Change nameservers at your registrar to Cloudflare's"));
|
|
551
|
+
console.log(chalk6.dim(" 3. Run this setup again\n"));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} else if (domainChoice === "buy") {
|
|
555
|
+
console.log(chalk6.dim("\n Use the domains command to search and buy:"));
|
|
556
|
+
console.log(chalk6.cyan(" posthorn domains check example.com example.io --cloudflare <id>"));
|
|
557
|
+
console.log(chalk6.cyan(" posthorn domains buy example.com --cloudflare <id> --contact '{...}'"));
|
|
558
|
+
console.log();
|
|
559
|
+
}
|
|
560
|
+
setConfig({ setupStep: 4 });
|
|
561
|
+
step = 4;
|
|
562
|
+
}
|
|
563
|
+
if (step < 5) {
|
|
564
|
+
printProgress(4);
|
|
565
|
+
console.log(" DKIM needs to be set up per domain.");
|
|
566
|
+
console.log(" If you're using Claude Code, run the setup skill \u2014 it automates DKIM via browser.");
|
|
567
|
+
console.log(" Otherwise, go to admin.google.com \u2192 Apps \u2192 Gmail \u2192 Authenticate email");
|
|
568
|
+
console.log();
|
|
569
|
+
await inquirer.prompt([{ type: "confirm", name: "ready", message: "DKIM set up (or skipping for now)?" }]);
|
|
570
|
+
setConfig({ setupStep: 5 });
|
|
571
|
+
step = 5;
|
|
572
|
+
}
|
|
573
|
+
if (step < 6) {
|
|
574
|
+
printProgress(5);
|
|
575
|
+
console.log(" Create mailboxes with:");
|
|
576
|
+
console.log(chalk6.cyan(" posthorn mailboxes create <domain-id> --email john@yourdomain.com --first John --last Smith"));
|
|
577
|
+
console.log();
|
|
578
|
+
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Mailboxes created (or skipping)?" }]);
|
|
579
|
+
setConfig({ setupStep: 6 });
|
|
580
|
+
step = 6;
|
|
581
|
+
}
|
|
582
|
+
if (step < 7) {
|
|
583
|
+
printProgress(6);
|
|
584
|
+
console.log(" Start warming your mailboxes:");
|
|
585
|
+
console.log(chalk6.cyan(" posthorn warmup start <mailbox-id>"));
|
|
586
|
+
console.log(chalk6.cyan(" posthorn warmup stats <campaign-id>"));
|
|
587
|
+
console.log();
|
|
588
|
+
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Ready to finish setup?" }]);
|
|
589
|
+
setConfig({ setupStep: 7 });
|
|
590
|
+
step = 7;
|
|
591
|
+
}
|
|
592
|
+
printProgress(7);
|
|
593
|
+
console.log(chalk6.bold.green(" Setup complete!\n"));
|
|
594
|
+
console.log(" Useful commands:");
|
|
595
|
+
console.log(chalk6.cyan(" posthorn domains list"));
|
|
596
|
+
console.log(chalk6.cyan(" posthorn mailboxes list <domain-id>"));
|
|
597
|
+
console.log(chalk6.cyan(" posthorn warmup stats <campaign-id>"));
|
|
598
|
+
console.log(chalk6.cyan(" posthorn send <mailbox-id> --to user@example.com --subject 'Hi' --body 'Hello!'"));
|
|
599
|
+
console.log(chalk6.cyan(" posthorn auth status"));
|
|
600
|
+
console.log();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/index.ts
|
|
604
|
+
var program = new Command();
|
|
605
|
+
program.name("posthorn").description("Posthorn \u2014 domain setup, mailbox creation, and email warmup").version("0.1.0");
|
|
606
|
+
program.command("setup").description("Guided setup \u2014 walk through the full onboarding flow").option("--url <url>", "API server URL", "https://api-production-08f2.up.railway.app").action(setup);
|
|
607
|
+
var auth = program.command("auth").description("Account management");
|
|
608
|
+
auth.command("register").description("Create a new account and get an API key").option("--url <url>", "API server URL", "https://api-production-08f2.up.railway.app").action(register);
|
|
609
|
+
auth.command("status").description("Show current account info").action(status);
|
|
610
|
+
auth.command("logout").description("Clear stored credentials").action(logout);
|
|
611
|
+
var accounts = program.command("accounts").description("Connected accounts (Cloudflare, Workspace)");
|
|
612
|
+
accounts.command("list").description("List connected accounts").action(listAccounts);
|
|
613
|
+
accounts.command("cloudflare <token>").description("Connect a Cloudflare account").option("--label <label>", "Account label", "main").action(connectCloudflare);
|
|
614
|
+
accounts.command("workspace <admin-email>").description("Connect Google Workspace via domain-wide delegation").action(connectWorkspace);
|
|
615
|
+
var domains = program.command("domains").description("Domain management");
|
|
616
|
+
domains.command("list").description("List all domains").action(listDomains);
|
|
617
|
+
domains.command("get <domain-id>").description("Get domain details").action(getDomain);
|
|
618
|
+
domains.command("check <domains...>").description("Check domain availability").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").action(checkDomains);
|
|
619
|
+
domains.command("buy <domain>").description("Purchase a domain").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--contact <json>", "Registrant contact info as JSON").action(buyDomain);
|
|
620
|
+
domains.command("connect <domain>").description("Connect an existing domain (must be on Cloudflare)").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--workspace <account-id>", "Workspace account ID").action(connectDomain);
|
|
621
|
+
var mailboxes = program.command("mailboxes").description("Mailbox management");
|
|
622
|
+
mailboxes.command("list <domain-id>").description("List mailboxes for a domain").action(listMailboxes);
|
|
623
|
+
mailboxes.command("create <domain-id>").description("Create a new mailbox").requiredOption("--email <email>", "Email address (e.g. john@example.com)").requiredOption("--first <name>", "First name").requiredOption("--last <name>", "Last name").action(createMailbox);
|
|
624
|
+
mailboxes.command("credentials <mailbox-id>").description("Provision IMAP/SMTP credentials for a mailbox").option("--type <type>", "Auth type: xoauth2 or password", "xoauth2").action(provisionCredentials);
|
|
625
|
+
program.command("send <mailbox-id>").description("Send an email").requiredOption("--to <email>", "Recipient email").requiredOption("--subject <subject>", "Email subject").requiredOption("--body <body>", "Email body (plain text)").action(sendEmail);
|
|
626
|
+
var warmup = program.command("warmup").description("Email warmup management");
|
|
627
|
+
warmup.command("list").description("List warmup campaigns").action(listCampaigns);
|
|
628
|
+
warmup.command("start <mailbox-id>").description("Start warming a mailbox").action(startWarmup);
|
|
629
|
+
warmup.command("pause <campaign-id>").description("Pause a warmup campaign").action(pauseWarmup);
|
|
630
|
+
warmup.command("stats <campaign-id>").description("Show warmup stats and daily breakdown").action(warmupStats);
|
|
631
|
+
program.hook("preAction", () => {
|
|
632
|
+
});
|
|
633
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
634
|
+
if (err.statusCode === 401) {
|
|
635
|
+
console.log(chalk7.red("\n Authentication failed. Run: posthorn auth register\n"));
|
|
636
|
+
} else if (err.statusCode === 403) {
|
|
637
|
+
console.log(chalk7.red("\n Account not verified. Complete onboarding: posthorn setup\n"));
|
|
638
|
+
} else if (err.statusCode === 429) {
|
|
639
|
+
console.log(chalk7.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
|
|
640
|
+
} else {
|
|
641
|
+
console.error(chalk7.red(`
|
|
642
|
+
Error: ${err.message}
|
|
643
|
+
`));
|
|
644
|
+
}
|
|
645
|
+
process.exit(1);
|
|
646
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "posthorn",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Posthorn — domain setup, mailbox creation, and email warmup from the command line",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"posthorn": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"email",
|
|
14
|
+
"warmup",
|
|
15
|
+
"cold-email",
|
|
16
|
+
"deliverability",
|
|
17
|
+
"smtp",
|
|
18
|
+
"outbound",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
27
|
+
"dev": "tsx src/index.ts",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^13.0.0",
|
|
32
|
+
"chalk": "^5.4.0",
|
|
33
|
+
"ora": "^8.2.0",
|
|
34
|
+
"inquirer": "^12.0.0",
|
|
35
|
+
"conf": "^13.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"tsup": "^8.0.0",
|
|
39
|
+
"tsx": "^4.0.0",
|
|
40
|
+
"typescript": "^5.0.0",
|
|
41
|
+
"@types/node": "^22.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|