@dench.com/cli 2.1.3 → 2.1.5
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/dench.ts +58 -6
- package/email.ts +424 -0
- package/package.json +2 -1
package/dench.ts
CHANGED
|
@@ -2351,6 +2351,7 @@ async function getRuntime() {
|
|
|
2351
2351
|
return {
|
|
2352
2352
|
mode: "session" as const,
|
|
2353
2353
|
host: resolveApiHost(host),
|
|
2354
|
+
convexUrl: stored.session.convexUrl,
|
|
2354
2355
|
client: createCliConvexClient(stored.session.convexUrl),
|
|
2355
2356
|
sessionToken: stored.session.sessionToken,
|
|
2356
2357
|
organization: stored.session.organization,
|
|
@@ -2397,6 +2398,7 @@ async function getRuntime() {
|
|
|
2397
2398
|
return {
|
|
2398
2399
|
mode: "apiKey" as const,
|
|
2399
2400
|
host: apiHost,
|
|
2401
|
+
convexUrl: sandboxConvexUrl,
|
|
2400
2402
|
client: createCliConvexClient(sandboxConvexUrl),
|
|
2401
2403
|
sessionToken: apiKey,
|
|
2402
2404
|
organization: sandboxOrganization,
|
|
@@ -2412,6 +2414,7 @@ async function getRuntime() {
|
|
|
2412
2414
|
return {
|
|
2413
2415
|
mode: "session" as const,
|
|
2414
2416
|
host: apiHost,
|
|
2417
|
+
convexUrl: sandboxConvexUrl,
|
|
2415
2418
|
client: createCliConvexClient(sandboxConvexUrl),
|
|
2416
2419
|
sessionToken: sandboxAgentToken,
|
|
2417
2420
|
organization: sandboxOrganization,
|
|
@@ -2981,13 +2984,44 @@ function suggestedWorkFromArtifacts(artifacts: JsonRecord[]) {
|
|
|
2981
2984
|
);
|
|
2982
2985
|
}
|
|
2983
2986
|
|
|
2987
|
+
/**
|
|
2988
|
+
* The fs daemon authenticates purely from `CONVEX_URL` + `DENCH_API_KEY`
|
|
2989
|
+
* env vars (sandboxes bake them in). Local shells running off an OAuth
|
|
2990
|
+
* agent session have neither — resolve the active session and inject
|
|
2991
|
+
* its convexUrl + `dch_agent_*` token. The server-side files functions
|
|
2992
|
+
* accept the agent-session token in the same `apiKey` slot (prefix
|
|
2993
|
+
* dispatch, like `requireCrmAccess`), so the daemon works unchanged.
|
|
2994
|
+
*/
|
|
2995
|
+
async function resolveFsDaemonEnv(): Promise<NodeJS.ProcessEnv> {
|
|
2996
|
+
if (process.env.CONVEX_URL?.trim() && process.env.DENCH_API_KEY?.trim()) {
|
|
2997
|
+
return process.env;
|
|
2998
|
+
}
|
|
2999
|
+
try {
|
|
3000
|
+
const runtime = await getRuntime();
|
|
3001
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") {
|
|
3002
|
+
return {
|
|
3003
|
+
...process.env,
|
|
3004
|
+
CONVEX_URL: process.env.CONVEX_URL?.trim() || runtime.convexUrl,
|
|
3005
|
+
DENCH_API_KEY:
|
|
3006
|
+
process.env.DENCH_API_KEY?.trim() || runtime.sessionToken,
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
} catch {
|
|
3010
|
+
// No saved session / dev env — fall through with the unmodified env;
|
|
3011
|
+
// the daemon prints its own missing-credential error for subcommands
|
|
3012
|
+
// that need Convex access.
|
|
3013
|
+
}
|
|
3014
|
+
return process.env;
|
|
3015
|
+
}
|
|
3016
|
+
|
|
2984
3017
|
async function runFsCommand() {
|
|
2985
3018
|
const subArgs = args.slice(args.indexOf("fs") + 1);
|
|
2986
3019
|
const configuredBinary = process.env.DENCH_FS_DAEMON_BIN?.trim();
|
|
2987
3020
|
const binary = configuredBinary || "dench-fs-daemon";
|
|
3021
|
+
const daemonEnv = await resolveFsDaemonEnv();
|
|
2988
3022
|
let exitCode: number;
|
|
2989
3023
|
try {
|
|
2990
|
-
exitCode = await spawnInherited(binary, subArgs);
|
|
3024
|
+
exitCode = await spawnInherited(binary, subArgs, daemonEnv);
|
|
2991
3025
|
} catch (error) {
|
|
2992
3026
|
if (configuredBinary || !isCommandNotFound(error)) {
|
|
2993
3027
|
throw error;
|
|
@@ -2995,7 +3029,11 @@ async function runFsCommand() {
|
|
|
2995
3029
|
const localDaemon = fileURLToPath(
|
|
2996
3030
|
new URL("./fs-daemon.ts", import.meta.url),
|
|
2997
3031
|
);
|
|
2998
|
-
exitCode = await spawnInherited(
|
|
3032
|
+
exitCode = await spawnInherited(
|
|
3033
|
+
"bun",
|
|
3034
|
+
[localDaemon, ...subArgs],
|
|
3035
|
+
daemonEnv,
|
|
3036
|
+
);
|
|
2999
3037
|
}
|
|
3000
3038
|
if (exitCode !== 0) {
|
|
3001
3039
|
process.exitCode = exitCode;
|
|
@@ -3025,8 +3063,18 @@ async function requireFilesApiKey(
|
|
|
3025
3063
|
runtime: Runtime,
|
|
3026
3064
|
command: string,
|
|
3027
3065
|
): Promise<string> {
|
|
3028
|
-
|
|
3029
|
-
|
|
3066
|
+
// The Convex files functions accept either a unified Dench API key OR a
|
|
3067
|
+
// `dch_agent_*` OAuth agent-session token in their `apiKey` arg (the
|
|
3068
|
+
// server dispatches on prefix, same as `requireCrmAccess`). So the
|
|
3069
|
+
// runtime's own bearer works directly — no gateway-key exchange, which
|
|
3070
|
+
// used to gate OAuth sessions behind a paid workspace and an extra
|
|
3071
|
+
// host round-trip.
|
|
3072
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") {
|
|
3073
|
+
return runtime.sessionToken;
|
|
3074
|
+
}
|
|
3075
|
+
const envApiKey = process.env.DENCH_API_KEY?.trim();
|
|
3076
|
+
if (envApiKey) return envApiKey;
|
|
3077
|
+
throw loginRequiredError(command);
|
|
3030
3078
|
}
|
|
3031
3079
|
|
|
3032
3080
|
type FileTreeRow = {
|
|
@@ -3603,11 +3651,15 @@ function sha256HexSync(bytes: Uint8Array): string {
|
|
|
3603
3651
|
return createHash("sha256").update(bytes).digest("hex");
|
|
3604
3652
|
}
|
|
3605
3653
|
|
|
3606
|
-
async function spawnInherited(
|
|
3654
|
+
async function spawnInherited(
|
|
3655
|
+
binary: string,
|
|
3656
|
+
commandArgs: string[],
|
|
3657
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
3658
|
+
) {
|
|
3607
3659
|
return await new Promise<number>((resolve, reject) => {
|
|
3608
3660
|
const child = spawn(binary, commandArgs, {
|
|
3609
3661
|
stdio: "inherit",
|
|
3610
|
-
env
|
|
3662
|
+
env,
|
|
3611
3663
|
});
|
|
3612
3664
|
child.on("error", reject);
|
|
3613
3665
|
child.on("close", (code) => resolve(code ?? 1));
|
package/email.ts
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import type { ConvexHttpClient } from "convex/browser";
|
|
3
|
+
import { makeFunctionReference } from "convex/server";
|
|
4
|
+
import { getFlag, parseJson } from "./lib/cli-args";
|
|
5
|
+
|
|
6
|
+
type EmailCliContext = {
|
|
7
|
+
convex: ConvexHttpClient;
|
|
8
|
+
args: string[];
|
|
9
|
+
jsonOutput: boolean;
|
|
10
|
+
sessionToken?: string;
|
|
11
|
+
bearerToken?: string;
|
|
12
|
+
gatewayBaseUrl?: string;
|
|
13
|
+
gatewayHeaders?: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class EmailCliError extends Error {}
|
|
17
|
+
|
|
18
|
+
type JsonRecord = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
function normalizeGatewayBaseUrl(value: string): string {
|
|
21
|
+
return value.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveGatewayBaseUrl(ctx: EmailCliContext): string {
|
|
25
|
+
const explicit =
|
|
26
|
+
process.env.DENCH_GATEWAY_URL?.trim() || process.env.GATEWAY_URL?.trim();
|
|
27
|
+
if (explicit) return normalizeGatewayBaseUrl(explicit);
|
|
28
|
+
if (ctx.gatewayBaseUrl?.trim()) {
|
|
29
|
+
return normalizeGatewayBaseUrl(ctx.gatewayBaseUrl.trim());
|
|
30
|
+
}
|
|
31
|
+
const host = process.env.DENCH_HOST?.trim();
|
|
32
|
+
if (host && /localhost|127\.0\.0\.1/.test(host)) {
|
|
33
|
+
return "http://localhost:8787";
|
|
34
|
+
}
|
|
35
|
+
return "https://gateway.merseoriginals.com";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireBearerToken(ctx: EmailCliContext): string {
|
|
39
|
+
const token = ctx.bearerToken?.trim() || process.env.DENCH_API_KEY?.trim();
|
|
40
|
+
if (!token) {
|
|
41
|
+
throw new EmailCliError(
|
|
42
|
+
"No Dench credential found. Set DENCH_API_KEY or run `dench signin`.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return token;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function callGateway(
|
|
49
|
+
ctx: EmailCliContext,
|
|
50
|
+
method: "GET" | "POST",
|
|
51
|
+
path: string,
|
|
52
|
+
body?: JsonRecord,
|
|
53
|
+
) {
|
|
54
|
+
const baseUrl = resolveGatewayBaseUrl(ctx);
|
|
55
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
56
|
+
method,
|
|
57
|
+
headers: {
|
|
58
|
+
accept: "application/json",
|
|
59
|
+
authorization: `Bearer ${requireBearerToken(ctx)}`,
|
|
60
|
+
...(ctx.gatewayHeaders ?? {}),
|
|
61
|
+
...(method === "POST" ? { "content-type": "application/json" } : {}),
|
|
62
|
+
},
|
|
63
|
+
...(method === "POST" ? { body: JSON.stringify(body ?? {}) } : {}),
|
|
64
|
+
});
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new EmailCliError(
|
|
68
|
+
`Email service returned ${response.status}: ${text.slice(0, 500)}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const payload = text ? JSON.parse(text) : {};
|
|
72
|
+
return payload;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function callMutation(
|
|
76
|
+
ctx: EmailCliContext,
|
|
77
|
+
name: string,
|
|
78
|
+
args: JsonRecord,
|
|
79
|
+
) {
|
|
80
|
+
const ref = makeFunctionReference<"mutation">(name);
|
|
81
|
+
return await ctx.convex.mutation(ref, {
|
|
82
|
+
...args,
|
|
83
|
+
sessionToken: ctx.sessionToken,
|
|
84
|
+
} as never);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function callQuery(ctx: EmailCliContext, name: string, args: JsonRecord) {
|
|
88
|
+
const ref = makeFunctionReference<"query">(name);
|
|
89
|
+
return await ctx.convex.query(ref, {
|
|
90
|
+
...args,
|
|
91
|
+
sessionToken: ctx.sessionToken,
|
|
92
|
+
} as never);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function callAction(
|
|
96
|
+
ctx: EmailCliContext,
|
|
97
|
+
name: string,
|
|
98
|
+
args: JsonRecord,
|
|
99
|
+
) {
|
|
100
|
+
const ref = makeFunctionReference<"action">(name);
|
|
101
|
+
return await ctx.convex.action(ref, {
|
|
102
|
+
...args,
|
|
103
|
+
sessionToken: ctx.sessionToken,
|
|
104
|
+
} as never);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function out(ctx: EmailCliContext, payload: unknown, text: string) {
|
|
108
|
+
if (ctx.jsonOutput) {
|
|
109
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function requiredFlag(args: string[], name: string): string {
|
|
116
|
+
const value = getFlag(args, name)?.trim();
|
|
117
|
+
if (!value) throw new EmailCliError(`Missing required ${name}`);
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function optionalJsonFlag(args: string[], name: string): unknown | undefined {
|
|
122
|
+
const raw = getFlag(args, name);
|
|
123
|
+
return raw === undefined ? undefined : parseJson(raw);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function readTextFlag(
|
|
127
|
+
args: string[],
|
|
128
|
+
inlineFlag: string,
|
|
129
|
+
fileFlag: string,
|
|
130
|
+
) {
|
|
131
|
+
const inline = getFlag(args, inlineFlag);
|
|
132
|
+
if (inline !== undefined) return inline;
|
|
133
|
+
const path = getFlag(args, fileFlag);
|
|
134
|
+
if (!path) return undefined;
|
|
135
|
+
return await readFile(path, "utf8");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function readRecipients(path: string): Promise<JsonRecord[]> {
|
|
139
|
+
const content = await readFile(path, "utf8");
|
|
140
|
+
const trimmed = content.trim();
|
|
141
|
+
if (!trimmed) return [];
|
|
142
|
+
if (trimmed.startsWith("[")) {
|
|
143
|
+
const parsed = JSON.parse(trimmed);
|
|
144
|
+
if (!Array.isArray(parsed))
|
|
145
|
+
throw new EmailCliError("Recipients file must be an array or JSONL");
|
|
146
|
+
return parsed.map((row) =>
|
|
147
|
+
row && typeof row === "object"
|
|
148
|
+
? (row as JsonRecord)
|
|
149
|
+
: { email: String(row) },
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return trimmed
|
|
153
|
+
.split(/\r?\n/)
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.map((line) => {
|
|
156
|
+
const parsed = JSON.parse(line);
|
|
157
|
+
return parsed && typeof parsed === "object"
|
|
158
|
+
? (parsed as JsonRecord)
|
|
159
|
+
: { email: String(parsed) };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toRecipients(rows: JsonRecord[]) {
|
|
164
|
+
return rows.map((row) => {
|
|
165
|
+
const email =
|
|
166
|
+
row.email ?? row.Email ?? row["Email Address"] ?? row.emailAddress;
|
|
167
|
+
if (typeof email !== "string") {
|
|
168
|
+
throw new EmailCliError("Each recipient row must include an email field");
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
email,
|
|
172
|
+
displayName:
|
|
173
|
+
typeof row.name === "string"
|
|
174
|
+
? row.name
|
|
175
|
+
: typeof row.displayName === "string"
|
|
176
|
+
? row.displayName
|
|
177
|
+
: undefined,
|
|
178
|
+
personalization: row.personalization ?? row,
|
|
179
|
+
personEntryId:
|
|
180
|
+
typeof row.personEntryId === "string" ? row.personEntryId : undefined,
|
|
181
|
+
companyEntryId:
|
|
182
|
+
typeof row.companyEntryId === "string" ? row.companyEntryId : undefined,
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function emailHelp() {
|
|
188
|
+
console.log(`dench email — Dench Emailing Service campaign commands
|
|
189
|
+
|
|
190
|
+
Usage:
|
|
191
|
+
dench email identity verify --from founder@example.com [--json]
|
|
192
|
+
dench email identity verify --domain example.com --from founder@example.com [--json]
|
|
193
|
+
dench email identity status --identity founder@example.com [--json]
|
|
194
|
+
dench email identity status --identity-id <identityId> [--json]
|
|
195
|
+
dench email template create --name "Intro" --subject "Hi {{firstName}}" --html-file body.html [--text-file body.txt] [--json]
|
|
196
|
+
dench email template preview --template <id> --data '{"firstName":"Ada"}' [--json]
|
|
197
|
+
dench email campaign create --name "Q2 outreach" --identity <id> --template <id> [--json]
|
|
198
|
+
dench email campaign recipients add --campaign <id> --file recipients.jsonl [--json]
|
|
199
|
+
dench email campaign preview --campaign <id> [--sample 5] [--json]
|
|
200
|
+
dench email campaign submit --campaign <id> [--json]
|
|
201
|
+
dench email campaign status --campaign <id> [--json]
|
|
202
|
+
dench email campaign pause|resume|cancel --campaign <id> [--json]`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function runEmailCommand(ctx: EmailCliContext): Promise<void> {
|
|
206
|
+
const group = ctx.args.shift();
|
|
207
|
+
if (!group || group === "help" || group === "--help" || group === "-h") {
|
|
208
|
+
emailHelp();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (group === "identity") {
|
|
212
|
+
await runIdentity(ctx);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (group === "template") {
|
|
216
|
+
await runTemplate(ctx);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (group === "campaign") {
|
|
220
|
+
await runCampaign(ctx);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
throw new EmailCliError(`Unknown dench email subcommand: ${group}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function runIdentity(ctx: EmailCliContext) {
|
|
227
|
+
const sub = ctx.args.shift();
|
|
228
|
+
if (sub === "verify") {
|
|
229
|
+
const domain = getFlag(ctx.args, "--domain");
|
|
230
|
+
const from = requiredFlag(ctx.args, "--from");
|
|
231
|
+
const type = domain ? "domain" : "email";
|
|
232
|
+
const identity = domain ?? from;
|
|
233
|
+
const configurationSetName = getFlag(ctx.args, "--configuration-set");
|
|
234
|
+
const created = await callMutation(
|
|
235
|
+
ctx,
|
|
236
|
+
"functions/emailCampaigns:createIdentity",
|
|
237
|
+
{
|
|
238
|
+
type,
|
|
239
|
+
domain,
|
|
240
|
+
fromEmail: from,
|
|
241
|
+
fromName: getFlag(ctx.args, "--from-name"),
|
|
242
|
+
replyToEmail: getFlag(ctx.args, "--reply-to"),
|
|
243
|
+
configurationSetName,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
const verification = await callAction(
|
|
247
|
+
ctx,
|
|
248
|
+
"functions/emailCampaignsNode:beginSenderVerification",
|
|
249
|
+
{ identityId: (created as JsonRecord).identityId },
|
|
250
|
+
);
|
|
251
|
+
out(
|
|
252
|
+
ctx,
|
|
253
|
+
{ ok: true, created, verification },
|
|
254
|
+
`Verification started for ${identity}`,
|
|
255
|
+
);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (sub === "status") {
|
|
259
|
+
const identityId = getFlag(ctx.args, "--identity-id");
|
|
260
|
+
if (identityId) {
|
|
261
|
+
const payload = await callAction(
|
|
262
|
+
ctx,
|
|
263
|
+
"functions/emailCampaignsNode:refreshIdentityStatus",
|
|
264
|
+
{ identityId },
|
|
265
|
+
);
|
|
266
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const identity = requiredFlag(ctx.args, "--identity");
|
|
270
|
+
const payload = await callGateway(
|
|
271
|
+
ctx,
|
|
272
|
+
"GET",
|
|
273
|
+
`/v1/email/identities/${encodeURIComponent(identity)}/status`,
|
|
274
|
+
);
|
|
275
|
+
const status = payload as JsonRecord;
|
|
276
|
+
const shaped = {
|
|
277
|
+
ok: status.ok === true,
|
|
278
|
+
identity: status.identity,
|
|
279
|
+
deliveryVerified: status.verifiedForSendingStatus === true,
|
|
280
|
+
deliveryVerificationStatus: status.verificationStatus,
|
|
281
|
+
};
|
|
282
|
+
out(ctx, shaped, JSON.stringify(shaped, null, 2));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
throw new EmailCliError("Usage: dench email identity verify|status");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function runTemplate(ctx: EmailCliContext) {
|
|
289
|
+
const sub = ctx.args.shift();
|
|
290
|
+
if (sub === "create") {
|
|
291
|
+
const htmlBody = await readTextFlag(ctx.args, "--html", "--html-file");
|
|
292
|
+
if (!htmlBody) throw new EmailCliError("Missing --html or --html-file");
|
|
293
|
+
const textBody = await readTextFlag(ctx.args, "--text", "--text-file");
|
|
294
|
+
const payload = await callMutation(
|
|
295
|
+
ctx,
|
|
296
|
+
"functions/emailCampaigns:createTemplate",
|
|
297
|
+
{
|
|
298
|
+
name: requiredFlag(ctx.args, "--name"),
|
|
299
|
+
subject: requiredFlag(ctx.args, "--subject"),
|
|
300
|
+
htmlBody,
|
|
301
|
+
textBody,
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
out(
|
|
305
|
+
ctx,
|
|
306
|
+
payload,
|
|
307
|
+
`Template created: ${(payload as JsonRecord).templateId}`,
|
|
308
|
+
);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (sub === "preview") {
|
|
312
|
+
const templateId = requiredFlag(ctx.args, "--template");
|
|
313
|
+
const data = optionalJsonFlag(ctx.args, "--data");
|
|
314
|
+
const payload = await callQuery(
|
|
315
|
+
ctx,
|
|
316
|
+
"functions/emailCampaigns:previewTemplate",
|
|
317
|
+
{ templateId, data },
|
|
318
|
+
);
|
|
319
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
throw new EmailCliError("Usage: dench email template create|preview");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function runCampaign(ctx: EmailCliContext) {
|
|
326
|
+
const sub = ctx.args.shift();
|
|
327
|
+
if (sub === "create") {
|
|
328
|
+
const payload = await callMutation(
|
|
329
|
+
ctx,
|
|
330
|
+
"functions/emailCampaigns:createCampaign",
|
|
331
|
+
{
|
|
332
|
+
name: requiredFlag(ctx.args, "--name"),
|
|
333
|
+
identityId: requiredFlag(ctx.args, "--identity"),
|
|
334
|
+
templateId: requiredFlag(ctx.args, "--template"),
|
|
335
|
+
scheduledAt: getFlag(ctx.args, "--scheduled-at")
|
|
336
|
+
? Date.parse(requiredFlag(ctx.args, "--scheduled-at"))
|
|
337
|
+
: undefined,
|
|
338
|
+
maxRecipients: getFlag(ctx.args, "--max-recipients")
|
|
339
|
+
? Number(requiredFlag(ctx.args, "--max-recipients"))
|
|
340
|
+
: undefined,
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
out(
|
|
344
|
+
ctx,
|
|
345
|
+
payload,
|
|
346
|
+
`Campaign created: ${(payload as JsonRecord).campaignId}`,
|
|
347
|
+
);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (sub === "recipients") {
|
|
351
|
+
const nested = ctx.args.shift();
|
|
352
|
+
if (nested !== "add") {
|
|
353
|
+
throw new EmailCliError(
|
|
354
|
+
"Usage: dench email campaign recipients add --campaign <id> --file recipients.jsonl",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const campaignId = requiredFlag(ctx.args, "--campaign");
|
|
358
|
+
const file = getFlag(ctx.args, "--file");
|
|
359
|
+
const rows = file
|
|
360
|
+
? await readRecipients(file)
|
|
361
|
+
: ((optionalJsonFlag(ctx.args, "--data") as JsonRecord[] | undefined) ??
|
|
362
|
+
[]);
|
|
363
|
+
if (rows.length === 0) throw new EmailCliError("No recipients provided");
|
|
364
|
+
const payload = await callMutation(
|
|
365
|
+
ctx,
|
|
366
|
+
"functions/emailCampaigns:addRecipients",
|
|
367
|
+
{ campaignId, recipients: toRecipients(rows) },
|
|
368
|
+
);
|
|
369
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (sub === "preview") {
|
|
373
|
+
const sampleRaw = getFlag(ctx.args, "--sample");
|
|
374
|
+
const payload = await callQuery(
|
|
375
|
+
ctx,
|
|
376
|
+
"functions/emailCampaigns:previewCampaign",
|
|
377
|
+
{
|
|
378
|
+
campaignId: requiredFlag(ctx.args, "--campaign"),
|
|
379
|
+
sample: sampleRaw ? Number(sampleRaw) : undefined,
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (sub === "submit") {
|
|
386
|
+
const payload = await callMutation(
|
|
387
|
+
ctx,
|
|
388
|
+
"functions/emailCampaigns:submitCampaign",
|
|
389
|
+
{
|
|
390
|
+
campaignId: requiredFlag(ctx.args, "--campaign"),
|
|
391
|
+
approvalId: getFlag(ctx.args, "--approval"),
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (sub === "status") {
|
|
398
|
+
const payload = await callQuery(
|
|
399
|
+
ctx,
|
|
400
|
+
"functions/emailCampaigns:getCampaign",
|
|
401
|
+
{
|
|
402
|
+
campaignId: requiredFlag(ctx.args, "--campaign"),
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (sub === "pause" || sub === "resume" || sub === "cancel") {
|
|
409
|
+
const fn =
|
|
410
|
+
sub === "pause"
|
|
411
|
+
? "pauseCampaign"
|
|
412
|
+
: sub === "resume"
|
|
413
|
+
? "resumeCampaign"
|
|
414
|
+
: "cancelCampaign";
|
|
415
|
+
const payload = await callMutation(ctx, `functions/emailCampaigns:${fn}`, {
|
|
416
|
+
campaignId: requiredFlag(ctx.args, "--campaign"),
|
|
417
|
+
});
|
|
418
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
throw new EmailCliError(
|
|
422
|
+
"Usage: dench email campaign create|recipients|preview|submit|status|pause|resume|cancel",
|
|
423
|
+
);
|
|
424
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "Dench agent workspace CLI. v2 unifies auth behind `dench signin`; the legacy `dench login` / `dench onboard` / `dench setup` / `dench what-can-i-do` / `dench register` commands now error with a redirect.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"chat.ts",
|
|
25
25
|
"chat-spawn.ts",
|
|
26
26
|
"crm.ts",
|
|
27
|
+
"email.ts",
|
|
27
28
|
"members.ts",
|
|
28
29
|
"cron.ts",
|
|
29
30
|
"browser.ts",
|