@hogsend/cli 0.2.3 → 0.6.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/bin.js +904 -168
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
- package/skills/hogsend-authoring-journeys/SKILL.md +1 -1
- package/skills/hogsend-authoring-journeys/references/journey-meta.md +8 -3
- package/skills/hogsend-authoring-lists/SKILL.md +178 -0
- package/skills/hogsend-cli/SKILL.md +44 -18
- package/skills/hogsend-client-sdk/SKILL.md +185 -0
- package/skills/hogsend-client-sdk/references/api-surface.md +181 -0
- package/src/bin.ts +4 -1
- package/src/commands/campaigns.ts +309 -0
- package/src/commands/contacts.ts +176 -6
- package/src/commands/emails.ts +231 -0
- package/src/commands/events.ts +253 -15
- package/src/commands/index.ts +4 -0
- package/src/commands/types.ts +8 -2
- package/src/lib/config.ts +23 -1
- package/src/lib/http.ts +122 -49
- package/studio/assets/{index-r9qr4mus.js → index-D7Ax_oFF.js} +1 -1
- package/studio/index.html +1 -1
package/dist/bin.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/bin.ts
|
|
4
4
|
import { createRequire as createRequire3 } from "module";
|
|
5
5
|
|
|
6
|
-
// src/commands/
|
|
6
|
+
// src/commands/campaigns.ts
|
|
7
7
|
import { parseArgs } from "util";
|
|
8
8
|
|
|
9
9
|
// src/lib/http.ts
|
|
@@ -33,53 +33,81 @@ function bodyMessage(status, body) {
|
|
|
33
33
|
}
|
|
34
34
|
return `request failed with status ${status}`;
|
|
35
35
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
36
|
+
async function request(baseUrl, key, missingKeyMessage, method, path, opts) {
|
|
37
|
+
if (opts.auth && !key) {
|
|
38
|
+
throw makeHttpError(missingKeyMessage, 0, void 0);
|
|
39
|
+
}
|
|
40
|
+
const headers = { Accept: "application/json" };
|
|
41
|
+
if (opts.auth && key) {
|
|
42
|
+
headers.Authorization = `Bearer ${key}`;
|
|
43
|
+
}
|
|
44
|
+
if (opts.body !== void 0) {
|
|
45
|
+
headers["Content-Type"] = "application/json";
|
|
46
|
+
}
|
|
47
|
+
const url = buildUrl(baseUrl, path, opts.query);
|
|
48
|
+
let res;
|
|
49
|
+
try {
|
|
50
|
+
res = await fetch(url, {
|
|
51
|
+
method,
|
|
52
|
+
headers,
|
|
53
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
54
|
+
});
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
57
|
+
throw makeHttpError(`cannot reach ${baseUrl} (${msg})`, 0, void 0);
|
|
58
|
+
}
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
let parsed;
|
|
61
|
+
if (text.length > 0) {
|
|
54
62
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
59
|
-
});
|
|
60
|
-
} catch (cause) {
|
|
61
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
62
|
-
throw makeHttpError(`cannot reach ${cfg.baseUrl} (${msg})`, 0, void 0);
|
|
63
|
-
}
|
|
64
|
-
const text = await res.text();
|
|
65
|
-
let parsed;
|
|
66
|
-
if (text.length > 0) {
|
|
67
|
-
try {
|
|
68
|
-
parsed = JSON.parse(text);
|
|
69
|
-
} catch {
|
|
70
|
-
parsed = text;
|
|
71
|
-
}
|
|
63
|
+
parsed = JSON.parse(text);
|
|
64
|
+
} catch {
|
|
65
|
+
parsed = text;
|
|
72
66
|
}
|
|
73
|
-
if (!res.ok) {
|
|
74
|
-
throw makeHttpError(bodyMessage(res.status, parsed), res.status, parsed);
|
|
75
|
-
}
|
|
76
|
-
return parsed;
|
|
77
67
|
}
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
throw makeHttpError(bodyMessage(res.status, parsed), res.status, parsed);
|
|
70
|
+
}
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
function createAdminClient(cfg) {
|
|
74
|
+
const missing = "no admin key configured \u2014 pass --admin-key, or set HOGSEND_ADMIN_KEY / ADMIN_API_KEY";
|
|
75
|
+
return {
|
|
76
|
+
cfg,
|
|
77
|
+
get: (path, query, extras) => request(cfg.baseUrl, cfg.adminKey, missing, "GET", path, {
|
|
78
|
+
query,
|
|
79
|
+
auth: extras?.auth ?? true
|
|
80
|
+
}),
|
|
81
|
+
patch: (path, body) => request(cfg.baseUrl, cfg.adminKey, missing, "PATCH", path, {
|
|
82
|
+
body,
|
|
83
|
+
auth: true
|
|
84
|
+
}),
|
|
85
|
+
post: (path, body) => request(cfg.baseUrl, cfg.adminKey, missing, "POST", path, {
|
|
86
|
+
body,
|
|
87
|
+
auth: true
|
|
88
|
+
})
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function createDataPlaneClient(cfg) {
|
|
92
|
+
const missing = "no data key configured \u2014 pass --data-key, or set HOGSEND_DATA_KEY / HOGSEND_API_KEY";
|
|
78
93
|
return {
|
|
79
94
|
cfg,
|
|
80
|
-
get: (path, query
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
get: (path, query) => request(cfg.baseUrl, cfg.dataKey, missing, "GET", path, {
|
|
96
|
+
query,
|
|
97
|
+
auth: true
|
|
98
|
+
}),
|
|
99
|
+
post: (path, body) => request(cfg.baseUrl, cfg.dataKey, missing, "POST", path, {
|
|
100
|
+
body,
|
|
101
|
+
auth: true
|
|
102
|
+
}),
|
|
103
|
+
put: (path, body) => request(cfg.baseUrl, cfg.dataKey, missing, "PUT", path, {
|
|
104
|
+
body,
|
|
105
|
+
auth: true
|
|
106
|
+
}),
|
|
107
|
+
del: (path, body) => request(cfg.baseUrl, cfg.dataKey, missing, "DELETE", path, {
|
|
108
|
+
body,
|
|
109
|
+
auth: true
|
|
110
|
+
})
|
|
83
111
|
};
|
|
84
112
|
}
|
|
85
113
|
|
|
@@ -198,15 +226,249 @@ ${title}`);
|
|
|
198
226
|
};
|
|
199
227
|
}
|
|
200
228
|
|
|
229
|
+
// src/commands/campaigns.ts
|
|
230
|
+
var usage = `hogsend campaigns <subcommand> [options]
|
|
231
|
+
|
|
232
|
+
Queue and inspect broadcasts: durably send one email template to every
|
|
233
|
+
subscribed member of a list (or every active member of a bucket). Wraps the
|
|
234
|
+
data-plane campaigns routes (POST /v1/campaigns, GET /v1/campaigns/{id}).
|
|
235
|
+
|
|
236
|
+
Subcommands:
|
|
237
|
+
send Queue a campaign. Sends run async in the worker.
|
|
238
|
+
status <id> Show a campaign's status + send counts.
|
|
239
|
+
|
|
240
|
+
send options (exactly one of --list / --bucket, plus --template, required):
|
|
241
|
+
--list <id> Target every subscribed member of this list.
|
|
242
|
+
--bucket <id> Target every active member of this bucket.
|
|
243
|
+
--template <key> Email template to send.
|
|
244
|
+
--prop <key=value> Template prop; repeatable. Value parsed as JSON, falling
|
|
245
|
+
back to a string.
|
|
246
|
+
--props <json> Template props as one JSON object (merged with --prop).
|
|
247
|
+
--name <text> Human label for the campaign.
|
|
248
|
+
--from <addr> Override the default From address.
|
|
249
|
+
--subject <text> Override the rendered subject.
|
|
250
|
+
|
|
251
|
+
Global options (handled by the router): --url, --admin-key, --data-key, --json,
|
|
252
|
+
-h/--help.
|
|
253
|
+
|
|
254
|
+
Examples:
|
|
255
|
+
hogsend campaigns send --list newsletter --template june-update --name "June"
|
|
256
|
+
hogsend campaigns send --bucket power-users --template feature-launch --json
|
|
257
|
+
hogsend campaigns status cmp_123 --json`;
|
|
258
|
+
var badge = `${color.bgMagenta(color.black(" hogsend "))} campaigns`;
|
|
259
|
+
function parseProps(ctx, propsJson, propPairs) {
|
|
260
|
+
const out = {};
|
|
261
|
+
let any = false;
|
|
262
|
+
if (propsJson !== void 0) {
|
|
263
|
+
let parsed;
|
|
264
|
+
try {
|
|
265
|
+
parsed = JSON.parse(propsJson);
|
|
266
|
+
} catch {
|
|
267
|
+
ctx.out.fail(`--props must be valid JSON, got: ${propsJson}`);
|
|
268
|
+
}
|
|
269
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
270
|
+
ctx.out.fail("--props must be a JSON object");
|
|
271
|
+
}
|
|
272
|
+
Object.assign(out, parsed);
|
|
273
|
+
any = true;
|
|
274
|
+
}
|
|
275
|
+
for (const pair of propPairs ?? []) {
|
|
276
|
+
const eq = pair.indexOf("=");
|
|
277
|
+
if (eq === -1) {
|
|
278
|
+
ctx.out.fail(`--prop must be key=value, got: ${pair}`);
|
|
279
|
+
}
|
|
280
|
+
const key = pair.slice(0, eq).trim();
|
|
281
|
+
if (key === "") {
|
|
282
|
+
ctx.out.fail(`--prop key cannot be empty, got: ${pair}`);
|
|
283
|
+
}
|
|
284
|
+
out[key] = coerceValue(pair.slice(eq + 1));
|
|
285
|
+
any = true;
|
|
286
|
+
}
|
|
287
|
+
return any ? out : void 0;
|
|
288
|
+
}
|
|
289
|
+
function coerceValue(raw) {
|
|
290
|
+
try {
|
|
291
|
+
return JSON.parse(raw);
|
|
292
|
+
} catch {
|
|
293
|
+
return raw;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function statusColor(status) {
|
|
297
|
+
switch (status) {
|
|
298
|
+
case "sent":
|
|
299
|
+
return color.green(status);
|
|
300
|
+
case "queued":
|
|
301
|
+
case "sending":
|
|
302
|
+
return color.cyan(status);
|
|
303
|
+
default:
|
|
304
|
+
return color.red(status);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function runSend(ctx, argv) {
|
|
308
|
+
const { values } = parseArgs({
|
|
309
|
+
args: argv,
|
|
310
|
+
allowPositionals: true,
|
|
311
|
+
options: {
|
|
312
|
+
list: { type: "string" },
|
|
313
|
+
bucket: { type: "string" },
|
|
314
|
+
template: { type: "string" },
|
|
315
|
+
prop: { type: "string", multiple: true },
|
|
316
|
+
props: { type: "string" },
|
|
317
|
+
name: { type: "string" },
|
|
318
|
+
from: { type: "string" },
|
|
319
|
+
subject: { type: "string" },
|
|
320
|
+
help: { type: "boolean", short: "h", default: false }
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
if (values.help) {
|
|
324
|
+
ctx.out.log(usage);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const list = values.list;
|
|
328
|
+
const bucket = values.bucket;
|
|
329
|
+
if (list && bucket || !list && !bucket) {
|
|
330
|
+
ctx.out.fail("campaigns send requires exactly one of --list or --bucket");
|
|
331
|
+
}
|
|
332
|
+
const template = values.template;
|
|
333
|
+
if (!template) {
|
|
334
|
+
ctx.out.fail(
|
|
335
|
+
"campaigns send requires --template, e.g. hogsend campaigns send --list newsletter --template welcome"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const props = parseProps(ctx, values.props, values.prop);
|
|
339
|
+
const body = { template };
|
|
340
|
+
if (list) body.list = list;
|
|
341
|
+
if (bucket) body.bucket = bucket;
|
|
342
|
+
if (props) body.props = props;
|
|
343
|
+
if (values.name) body.name = values.name;
|
|
344
|
+
if (values.from) body.from = values.from;
|
|
345
|
+
if (values.subject) body.subject = values.subject;
|
|
346
|
+
let res;
|
|
347
|
+
try {
|
|
348
|
+
res = await ctx.out.step(
|
|
349
|
+
`Queuing campaign ${template}`,
|
|
350
|
+
() => ctx.dataHttp.post("/v1/campaigns", body)
|
|
351
|
+
);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (isHttpError(error)) {
|
|
354
|
+
ctx.out.fail(error.message);
|
|
355
|
+
}
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
if (ctx.json) {
|
|
359
|
+
ctx.out.json(res);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
ctx.out.intro(`${badge} send`);
|
|
363
|
+
ctx.out.kv(
|
|
364
|
+
{
|
|
365
|
+
campaignId: res.campaignId,
|
|
366
|
+
template,
|
|
367
|
+
audience: list ? `list:${list}` : `bucket:${bucket}`,
|
|
368
|
+
status: statusColor(res.status)
|
|
369
|
+
},
|
|
370
|
+
"Campaign queued"
|
|
371
|
+
);
|
|
372
|
+
ctx.out.outro(
|
|
373
|
+
`${color.green("Queued")} \u2014 poll ${color.cyan(`hogsend campaigns status ${res.campaignId}`)} for progress.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
async function runStatus(ctx, argv) {
|
|
377
|
+
const { values, positionals } = parseArgs({
|
|
378
|
+
args: argv,
|
|
379
|
+
allowPositionals: true,
|
|
380
|
+
options: {
|
|
381
|
+
help: { type: "boolean", short: "h", default: false }
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
if (values.help) {
|
|
385
|
+
ctx.out.log(usage);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const id = positionals[0];
|
|
389
|
+
if (!id) {
|
|
390
|
+
ctx.out.fail(
|
|
391
|
+
"campaigns status requires a campaign id, e.g. hogsend campaigns status cmp_123"
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
let res;
|
|
395
|
+
try {
|
|
396
|
+
res = await ctx.out.step(
|
|
397
|
+
`Fetching campaign ${id}`,
|
|
398
|
+
() => ctx.dataHttp.get(
|
|
399
|
+
`/v1/campaigns/${encodeURIComponent(id)}`
|
|
400
|
+
)
|
|
401
|
+
);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (isHttpError(error)) {
|
|
404
|
+
ctx.out.fail(error.message);
|
|
405
|
+
}
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
if (ctx.json) {
|
|
409
|
+
ctx.out.json(res);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
ctx.out.intro(`${badge} status`);
|
|
413
|
+
ctx.out.kv(
|
|
414
|
+
{
|
|
415
|
+
id: res.id,
|
|
416
|
+
name: res.name,
|
|
417
|
+
status: statusColor(res.status),
|
|
418
|
+
audience: `${res.audienceKind}:${res.audienceId}`,
|
|
419
|
+
template: res.templateKey,
|
|
420
|
+
recipients: res.totalRecipients,
|
|
421
|
+
sent: color.green(String(res.sentCount)),
|
|
422
|
+
skipped: color.yellow(String(res.skippedCount)),
|
|
423
|
+
failed: res.failedCount > 0 ? color.red(String(res.failedCount)) : String(res.failedCount),
|
|
424
|
+
startedAt: res.startedAt ?? "",
|
|
425
|
+
completedAt: res.completedAt ?? ""
|
|
426
|
+
},
|
|
427
|
+
"Campaign"
|
|
428
|
+
);
|
|
429
|
+
ctx.out.outro(
|
|
430
|
+
`${res.name} \u2192 ${statusColor(res.status)} ` + color.dim(
|
|
431
|
+
`(${res.sentCount}/${res.totalRecipients} sent, ${res.skippedCount} skipped, ${res.failedCount} failed)`
|
|
432
|
+
)
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
async function run(ctx) {
|
|
436
|
+
const sub = ctx.argv[0];
|
|
437
|
+
switch (sub) {
|
|
438
|
+
case "send":
|
|
439
|
+
return runSend(ctx, ctx.argv.slice(1));
|
|
440
|
+
case "status":
|
|
441
|
+
return runStatus(ctx, ctx.argv.slice(1));
|
|
442
|
+
case void 0:
|
|
443
|
+
ctx.out.fail(
|
|
444
|
+
"campaigns requires a subcommand: send | status (see hogsend campaigns --help)"
|
|
445
|
+
);
|
|
446
|
+
break;
|
|
447
|
+
default:
|
|
448
|
+
ctx.out.fail(
|
|
449
|
+
`unknown campaigns subcommand "${sub}" \u2014 expected send or status`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
var campaignsCommand = {
|
|
454
|
+
name: "campaigns",
|
|
455
|
+
summary: "Queue a broadcast to a list/bucket, or check its status",
|
|
456
|
+
usage,
|
|
457
|
+
run
|
|
458
|
+
};
|
|
459
|
+
|
|
201
460
|
// src/commands/contacts.ts
|
|
202
|
-
|
|
461
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
462
|
+
var usage2 = `hogsend contacts <subcommand> [options]
|
|
203
463
|
|
|
204
|
-
Inspect contacts via the
|
|
464
|
+
Inspect contacts via the admin API (/v1/admin/contacts) and upsert them via the
|
|
465
|
+
data plane (PUT /v1/contacts).
|
|
205
466
|
|
|
206
467
|
Subcommands:
|
|
207
468
|
list List contacts (newest activity first).
|
|
208
469
|
get <id> Get one contact (by id or externalId) + preferences.
|
|
209
470
|
timeline <id> Merged event/email/journey activity for a contact.
|
|
471
|
+
upsert Create or update a contact (PUT /v1/contacts).
|
|
210
472
|
|
|
211
473
|
list options:
|
|
212
474
|
--search <q> Filter by email/externalId substring.
|
|
@@ -218,13 +480,76 @@ timeline options:
|
|
|
218
480
|
--limit <n> Page size (1-100, default 50).
|
|
219
481
|
--offset <n> Page offset (default 0).
|
|
220
482
|
|
|
221
|
-
|
|
483
|
+
upsert options (at least one of --email / --user-id required):
|
|
484
|
+
--email <addr> Contact email (a resolvable identity key).
|
|
485
|
+
--user-id <id> External (distinct) id.
|
|
486
|
+
--prop <key=value> Contact property; repeatable. Value parsed as JSON,
|
|
487
|
+
falling back to a string. Uses the data plane (ingest key).
|
|
488
|
+
--props <json> Contact properties as one JSON object (merged with --prop).
|
|
489
|
+
--list <id> Subscribe to a list; repeatable.
|
|
490
|
+
--unlist <id> Unsubscribe from a list; repeatable.
|
|
491
|
+
|
|
492
|
+
Global options (handled by the router): --url, --admin-key, --data-key, --json,
|
|
493
|
+
-h/--help.
|
|
222
494
|
|
|
223
495
|
Examples:
|
|
224
496
|
hogsend contacts list --search acme@ --json
|
|
225
497
|
hogsend contacts get user_123
|
|
226
|
-
hogsend contacts timeline user_123 --type email --json
|
|
227
|
-
|
|
498
|
+
hogsend contacts timeline user_123 --type email --json
|
|
499
|
+
hogsend contacts upsert --email a@b.com --user-id user_123 --prop plan=pro
|
|
500
|
+
hogsend contacts upsert --user-id user_123 --props '{"plan":"pro","seats":5}'`;
|
|
501
|
+
var badge2 = `${color.bgMagenta(color.black(" hogsend "))} contacts`;
|
|
502
|
+
function parseProps2(ctx, propsJson, propPairs) {
|
|
503
|
+
const out = {};
|
|
504
|
+
let any = false;
|
|
505
|
+
if (propsJson !== void 0) {
|
|
506
|
+
let parsed;
|
|
507
|
+
try {
|
|
508
|
+
parsed = JSON.parse(propsJson);
|
|
509
|
+
} catch {
|
|
510
|
+
ctx.out.fail(`--props must be valid JSON, got: ${propsJson}`);
|
|
511
|
+
}
|
|
512
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
513
|
+
ctx.out.fail("--props must be a JSON object");
|
|
514
|
+
}
|
|
515
|
+
Object.assign(out, parsed);
|
|
516
|
+
any = true;
|
|
517
|
+
}
|
|
518
|
+
for (const pair of propPairs ?? []) {
|
|
519
|
+
const eq = pair.indexOf("=");
|
|
520
|
+
if (eq === -1) {
|
|
521
|
+
ctx.out.fail(`--prop must be key=value, got: ${pair}`);
|
|
522
|
+
}
|
|
523
|
+
const key = pair.slice(0, eq).trim();
|
|
524
|
+
if (key === "") {
|
|
525
|
+
ctx.out.fail(`--prop key cannot be empty, got: ${pair}`);
|
|
526
|
+
}
|
|
527
|
+
const raw = pair.slice(eq + 1);
|
|
528
|
+
out[key] = coerceValue2(raw);
|
|
529
|
+
any = true;
|
|
530
|
+
}
|
|
531
|
+
return any ? out : void 0;
|
|
532
|
+
}
|
|
533
|
+
function coerceValue2(raw) {
|
|
534
|
+
try {
|
|
535
|
+
return JSON.parse(raw);
|
|
536
|
+
} catch {
|
|
537
|
+
return raw;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function parseLists(subscribe, unsubscribe) {
|
|
541
|
+
const out = {};
|
|
542
|
+
let any = false;
|
|
543
|
+
for (const id of subscribe ?? []) {
|
|
544
|
+
out[id] = true;
|
|
545
|
+
any = true;
|
|
546
|
+
}
|
|
547
|
+
for (const id of unsubscribe ?? []) {
|
|
548
|
+
out[id] = false;
|
|
549
|
+
any = true;
|
|
550
|
+
}
|
|
551
|
+
return any ? out : void 0;
|
|
552
|
+
}
|
|
228
553
|
async function fetchOrFail(ctx, label, fn) {
|
|
229
554
|
try {
|
|
230
555
|
return await ctx.out.step(label, fn);
|
|
@@ -239,7 +564,7 @@ async function fetchOrFail(ctx, label, fn) {
|
|
|
239
564
|
}
|
|
240
565
|
}
|
|
241
566
|
async function runList(ctx, argv) {
|
|
242
|
-
const { values } =
|
|
567
|
+
const { values } = parseArgs2({
|
|
243
568
|
args: argv,
|
|
244
569
|
allowPositionals: true,
|
|
245
570
|
options: {
|
|
@@ -250,7 +575,7 @@ async function runList(ctx, argv) {
|
|
|
250
575
|
}
|
|
251
576
|
});
|
|
252
577
|
if (values.help) {
|
|
253
|
-
ctx.out.log(
|
|
578
|
+
ctx.out.log(usage2);
|
|
254
579
|
return;
|
|
255
580
|
}
|
|
256
581
|
const query = {
|
|
@@ -258,7 +583,7 @@ async function runList(ctx, argv) {
|
|
|
258
583
|
limit: values.limit,
|
|
259
584
|
offset: values.offset
|
|
260
585
|
};
|
|
261
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
586
|
+
if (!ctx.json) ctx.out.intro(`${badge2} list`);
|
|
262
587
|
const res = await fetchOrFail(
|
|
263
588
|
ctx,
|
|
264
589
|
"Fetching contacts",
|
|
@@ -282,7 +607,7 @@ async function runList(ctx, argv) {
|
|
|
282
607
|
);
|
|
283
608
|
}
|
|
284
609
|
async function runGet(ctx, argv) {
|
|
285
|
-
const { values, positionals } =
|
|
610
|
+
const { values, positionals } = parseArgs2({
|
|
286
611
|
args: argv,
|
|
287
612
|
allowPositionals: true,
|
|
288
613
|
options: {
|
|
@@ -290,7 +615,7 @@ async function runGet(ctx, argv) {
|
|
|
290
615
|
}
|
|
291
616
|
});
|
|
292
617
|
if (values.help) {
|
|
293
|
-
ctx.out.log(
|
|
618
|
+
ctx.out.log(usage2);
|
|
294
619
|
return;
|
|
295
620
|
}
|
|
296
621
|
const id = positionals[1];
|
|
@@ -299,7 +624,7 @@ async function runGet(ctx, argv) {
|
|
|
299
624
|
"contacts get requires an id, e.g. hogsend contacts get user_123"
|
|
300
625
|
);
|
|
301
626
|
}
|
|
302
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
627
|
+
if (!ctx.json) ctx.out.intro(`${badge2} get`);
|
|
303
628
|
const res = await fetchOrFail(
|
|
304
629
|
ctx,
|
|
305
630
|
"Fetching contact",
|
|
@@ -337,7 +662,7 @@ async function runGet(ctx, argv) {
|
|
|
337
662
|
ctx.out.outro(`Contact ${color.cyan(contact.externalId)}`);
|
|
338
663
|
}
|
|
339
664
|
async function runTimeline(ctx, argv) {
|
|
340
|
-
const { values, positionals } =
|
|
665
|
+
const { values, positionals } = parseArgs2({
|
|
341
666
|
args: argv,
|
|
342
667
|
allowPositionals: true,
|
|
343
668
|
options: {
|
|
@@ -348,7 +673,7 @@ async function runTimeline(ctx, argv) {
|
|
|
348
673
|
}
|
|
349
674
|
});
|
|
350
675
|
if (values.help) {
|
|
351
|
-
ctx.out.log(
|
|
676
|
+
ctx.out.log(usage2);
|
|
352
677
|
return;
|
|
353
678
|
}
|
|
354
679
|
const id = positionals[1];
|
|
@@ -365,7 +690,7 @@ async function runTimeline(ctx, argv) {
|
|
|
365
690
|
limit: values.limit,
|
|
366
691
|
offset: values.offset
|
|
367
692
|
};
|
|
368
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
693
|
+
if (!ctx.json) ctx.out.intro(`${badge2} timeline`);
|
|
369
694
|
const res = await fetchOrFail(
|
|
370
695
|
ctx,
|
|
371
696
|
"Fetching timeline",
|
|
@@ -401,7 +726,62 @@ function summarizeTimelineEntry(entry) {
|
|
|
401
726
|
const subject = d.subject ? String(d.subject) : String(d.templateKey ?? "");
|
|
402
727
|
return `${subject} [${String(d.status ?? "")}]`;
|
|
403
728
|
}
|
|
404
|
-
async function
|
|
729
|
+
async function runUpsert(ctx, argv) {
|
|
730
|
+
const { values } = parseArgs2({
|
|
731
|
+
args: argv,
|
|
732
|
+
allowPositionals: true,
|
|
733
|
+
options: {
|
|
734
|
+
email: { type: "string" },
|
|
735
|
+
"user-id": { type: "string" },
|
|
736
|
+
prop: { type: "string", multiple: true },
|
|
737
|
+
props: { type: "string" },
|
|
738
|
+
list: { type: "string", multiple: true },
|
|
739
|
+
unlist: { type: "string", multiple: true },
|
|
740
|
+
help: { type: "boolean", short: "h", default: false }
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
if (values.help) {
|
|
744
|
+
ctx.out.log(usage2);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const email = values.email;
|
|
748
|
+
const userId = values["user-id"];
|
|
749
|
+
if (!email && !userId) {
|
|
750
|
+
ctx.out.fail(
|
|
751
|
+
"contacts upsert requires at least one of --email or --user-id"
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
const properties = parseProps2(ctx, values.props, values.prop);
|
|
755
|
+
const lists = parseLists(values.list, values.unlist);
|
|
756
|
+
const body = {};
|
|
757
|
+
if (email) body.email = email;
|
|
758
|
+
if (userId) body.userId = userId;
|
|
759
|
+
if (properties) body.properties = properties;
|
|
760
|
+
if (lists) body.lists = lists;
|
|
761
|
+
if (!ctx.json) ctx.out.intro(`${badge2} upsert`);
|
|
762
|
+
const res = await fetchOrFail(
|
|
763
|
+
ctx,
|
|
764
|
+
"Upserting contact",
|
|
765
|
+
() => ctx.dataHttp.put("/v1/contacts", body)
|
|
766
|
+
);
|
|
767
|
+
if (ctx.json) {
|
|
768
|
+
ctx.out.json(res);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
ctx.out.kv(
|
|
772
|
+
{
|
|
773
|
+
id: res.id,
|
|
774
|
+
created: res.created,
|
|
775
|
+
linked: res.linked,
|
|
776
|
+
email: email ?? color.dim("(none)"),
|
|
777
|
+
userId: userId ?? color.dim("(none)")
|
|
778
|
+
},
|
|
779
|
+
"Contact"
|
|
780
|
+
);
|
|
781
|
+
const verb = res.created ? "created" : "updated";
|
|
782
|
+
ctx.out.outro(`Contact ${color.cyan(res.id)} ${verb}.`);
|
|
783
|
+
}
|
|
784
|
+
async function run2(ctx) {
|
|
405
785
|
const sub = ctx.argv[0];
|
|
406
786
|
switch (sub) {
|
|
407
787
|
case "list":
|
|
@@ -410,26 +790,28 @@ async function run(ctx) {
|
|
|
410
790
|
return runGet(ctx, ctx.argv);
|
|
411
791
|
case "timeline":
|
|
412
792
|
return runTimeline(ctx, ctx.argv);
|
|
793
|
+
case "upsert":
|
|
794
|
+
return runUpsert(ctx, ctx.argv.slice(1));
|
|
413
795
|
case void 0:
|
|
414
796
|
ctx.out.fail(
|
|
415
|
-
"contacts requires a subcommand: list, get, or
|
|
797
|
+
"contacts requires a subcommand: list, get, timeline, or upsert (see hogsend contacts --help)"
|
|
416
798
|
);
|
|
417
799
|
break;
|
|
418
800
|
default:
|
|
419
801
|
ctx.out.fail(
|
|
420
|
-
`unknown contacts subcommand "${sub}" \u2014 expected list, get, or
|
|
802
|
+
`unknown contacts subcommand "${sub}" \u2014 expected list, get, timeline, or upsert`
|
|
421
803
|
);
|
|
422
804
|
}
|
|
423
805
|
}
|
|
424
806
|
var contactsCommand = {
|
|
425
807
|
name: "contacts",
|
|
426
|
-
summary: "List, inspect, and
|
|
427
|
-
usage,
|
|
428
|
-
run
|
|
808
|
+
summary: "List, inspect, trace, and upsert contacts",
|
|
809
|
+
usage: usage2,
|
|
810
|
+
run: run2
|
|
429
811
|
};
|
|
430
812
|
|
|
431
813
|
// src/commands/doctor.ts
|
|
432
|
-
import { parseArgs as
|
|
814
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
433
815
|
|
|
434
816
|
// src/lib/skills.ts
|
|
435
817
|
import {
|
|
@@ -561,7 +943,7 @@ function skillsNudge(ctx) {
|
|
|
561
943
|
"Skills out of date"
|
|
562
944
|
);
|
|
563
945
|
}
|
|
564
|
-
var
|
|
946
|
+
var usage3 = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
|
|
565
947
|
|
|
566
948
|
Probe a running Hogsend instance via GET /v1/health and report its health:
|
|
567
949
|
component status (database, redis), two-track schema state (engine + client),
|
|
@@ -603,8 +985,8 @@ function trackLine(name, track) {
|
|
|
603
985
|
const required = track.required ?? color.dim("none");
|
|
604
986
|
return `${color.bold(name.padEnd(7))} applied ${applied} -> required ${required} ${sync}`;
|
|
605
987
|
}
|
|
606
|
-
async function
|
|
607
|
-
const { values } =
|
|
988
|
+
async function run3(ctx) {
|
|
989
|
+
const { values } = parseArgs3({
|
|
608
990
|
args: ctx.argv,
|
|
609
991
|
allowPositionals: true,
|
|
610
992
|
options: {
|
|
@@ -614,7 +996,7 @@ async function run2(ctx) {
|
|
|
614
996
|
strict: false
|
|
615
997
|
});
|
|
616
998
|
if (values.help) {
|
|
617
|
-
ctx.out.log(
|
|
999
|
+
ctx.out.log(usage3);
|
|
618
1000
|
return;
|
|
619
1001
|
}
|
|
620
1002
|
const { baseUrl } = ctx.http.cfg;
|
|
@@ -675,8 +1057,8 @@ async function run2(ctx) {
|
|
|
675
1057
|
if (!ok) process.exit(1);
|
|
676
1058
|
return;
|
|
677
1059
|
}
|
|
678
|
-
const
|
|
679
|
-
ctx.out.intro(
|
|
1060
|
+
const badge5 = `${color.bgMagenta(color.black(" hogsend "))} doctor`;
|
|
1061
|
+
ctx.out.intro(badge5);
|
|
680
1062
|
const verdictColor = verdict === "ok" ? color.green : verdict === "degraded" ? color.red : color.yellow;
|
|
681
1063
|
const lines = [
|
|
682
1064
|
`${verdictColor("\u25CF")} ${color.bold(verdict)}`,
|
|
@@ -704,15 +1086,15 @@ async function run2(ctx) {
|
|
|
704
1086
|
var doctorCommand = {
|
|
705
1087
|
name: "doctor",
|
|
706
1088
|
summary: "Probe a running instance's health (GET /v1/health)",
|
|
707
|
-
usage:
|
|
708
|
-
run:
|
|
1089
|
+
usage: usage3,
|
|
1090
|
+
run: run3
|
|
709
1091
|
};
|
|
710
1092
|
|
|
711
1093
|
// src/commands/eject.ts
|
|
712
1094
|
import { existsSync as existsSync3, realpathSync } from "fs";
|
|
713
1095
|
import { createRequire as createRequire2 } from "module";
|
|
714
1096
|
import { dirname, join as join3, sep as sep2 } from "path";
|
|
715
|
-
import { parseArgs as
|
|
1097
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
716
1098
|
|
|
717
1099
|
// src/eject.ts
|
|
718
1100
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -826,7 +1208,7 @@ async function countFiles(dir) {
|
|
|
826
1208
|
}
|
|
827
1209
|
|
|
828
1210
|
// src/commands/eject.ts
|
|
829
|
-
var
|
|
1211
|
+
var usage4 = `hogsend eject <package> [--force] [--cwd <dir>]
|
|
830
1212
|
|
|
831
1213
|
Copy a @hogsend/* package's source into vendor/<name> and rewrite the consumer
|
|
832
1214
|
dependency to file:./vendor/<name>. Every other dependency keeps upgrading.
|
|
@@ -854,8 +1236,8 @@ function resolveSourceDir(pkg, consumerRoot) {
|
|
|
854
1236
|
}
|
|
855
1237
|
return null;
|
|
856
1238
|
}
|
|
857
|
-
async function
|
|
858
|
-
const { values, positionals } =
|
|
1239
|
+
async function run4(ctx) {
|
|
1240
|
+
const { values, positionals } = parseArgs4({
|
|
859
1241
|
args: ctx.argv,
|
|
860
1242
|
allowPositionals: true,
|
|
861
1243
|
options: {
|
|
@@ -865,7 +1247,7 @@ async function run3(ctx) {
|
|
|
865
1247
|
}
|
|
866
1248
|
});
|
|
867
1249
|
if (values.help) {
|
|
868
|
-
ctx.out.log(
|
|
1250
|
+
ctx.out.log(usage4);
|
|
869
1251
|
return;
|
|
870
1252
|
}
|
|
871
1253
|
const pkg = positionals[0];
|
|
@@ -909,36 +1291,242 @@ async function run3(ctx) {
|
|
|
909
1291
|
var ejectCommand = {
|
|
910
1292
|
name: "eject",
|
|
911
1293
|
summary: "Vendor a @hogsend/* package into vendor/<name>",
|
|
912
|
-
usage:
|
|
913
|
-
run:
|
|
1294
|
+
usage: usage4,
|
|
1295
|
+
run: run4
|
|
914
1296
|
};
|
|
915
1297
|
|
|
916
|
-
// src/commands/
|
|
917
|
-
import { parseArgs as
|
|
918
|
-
var
|
|
1298
|
+
// src/commands/emails.ts
|
|
1299
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
1300
|
+
var usage5 = `hogsend emails <subcommand> [options]
|
|
919
1301
|
|
|
920
|
-
|
|
921
|
-
|
|
1302
|
+
Send a transactional email through the data plane (POST /v1/emails). The send
|
|
1303
|
+
runs through the full preferences + tracking pipeline (link-click + open).
|
|
922
1304
|
|
|
923
|
-
|
|
924
|
-
<
|
|
1305
|
+
Subcommands:
|
|
1306
|
+
send <template> Send the named template to a recipient.
|
|
1307
|
+
|
|
1308
|
+
send options (at least one of --to / --user-id required):
|
|
1309
|
+
--to <addr> Recipient email address.
|
|
1310
|
+
--user-id <id> External (distinct) id; the recipient email is resolved
|
|
1311
|
+
from the contact (404 if it has no resolvable email).
|
|
1312
|
+
--prop <key=value> Template prop; repeatable. Value parsed as JSON, falling
|
|
1313
|
+
back to a string.
|
|
1314
|
+
--props <json> Template props as one JSON object (merged with --prop).
|
|
1315
|
+
--from <addr> Override the default From address.
|
|
1316
|
+
--subject <text> Override the rendered subject.
|
|
1317
|
+
--reply-to <addr> Set the Reply-To address.
|
|
1318
|
+
--category <key> Preference category / list id to gate the send on.
|
|
1319
|
+
--skip-preference-check Bypass unsubscribe/suppression (requires full-admin).
|
|
1320
|
+
--idempotency-key <k> Dedup key.
|
|
1321
|
+
|
|
1322
|
+
Global options (handled by the router): --url, --admin-key, --data-key, --json,
|
|
1323
|
+
-h/--help.
|
|
925
1324
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1325
|
+
Examples:
|
|
1326
|
+
hogsend emails send welcome --to a@b.com --prop name=Ada
|
|
1327
|
+
hogsend emails send welcome --user-id user_123 --props '{"name":"Ada"}' --json`;
|
|
1328
|
+
var badge3 = `${color.bgMagenta(color.black(" hogsend "))} emails`;
|
|
1329
|
+
function parseProps3(ctx, propsJson, propPairs) {
|
|
1330
|
+
const out = {};
|
|
1331
|
+
let any = false;
|
|
1332
|
+
if (propsJson !== void 0) {
|
|
1333
|
+
let parsed;
|
|
1334
|
+
try {
|
|
1335
|
+
parsed = JSON.parse(propsJson);
|
|
1336
|
+
} catch {
|
|
1337
|
+
ctx.out.fail(`--props must be valid JSON, got: ${propsJson}`);
|
|
1338
|
+
}
|
|
1339
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1340
|
+
ctx.out.fail("--props must be a JSON object");
|
|
1341
|
+
}
|
|
1342
|
+
Object.assign(out, parsed);
|
|
1343
|
+
any = true;
|
|
1344
|
+
}
|
|
1345
|
+
for (const pair of propPairs ?? []) {
|
|
1346
|
+
const eq = pair.indexOf("=");
|
|
1347
|
+
if (eq === -1) {
|
|
1348
|
+
ctx.out.fail(`--prop must be key=value, got: ${pair}`);
|
|
1349
|
+
}
|
|
1350
|
+
const key = pair.slice(0, eq).trim();
|
|
1351
|
+
if (key === "") {
|
|
1352
|
+
ctx.out.fail(`--prop key cannot be empty, got: ${pair}`);
|
|
1353
|
+
}
|
|
1354
|
+
out[key] = coerceValue3(pair.slice(eq + 1));
|
|
1355
|
+
any = true;
|
|
1356
|
+
}
|
|
1357
|
+
return any ? out : void 0;
|
|
1358
|
+
}
|
|
1359
|
+
function coerceValue3(raw) {
|
|
1360
|
+
try {
|
|
1361
|
+
return JSON.parse(raw);
|
|
1362
|
+
} catch {
|
|
1363
|
+
return raw;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function statusColor2(status) {
|
|
1367
|
+
switch (status) {
|
|
1368
|
+
case "queued":
|
|
1369
|
+
case "sent":
|
|
1370
|
+
return color.green(status);
|
|
1371
|
+
case "skipped":
|
|
1372
|
+
return color.dim(status);
|
|
1373
|
+
default:
|
|
1374
|
+
return color.yellow(status);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
async function runSend2(ctx, argv) {
|
|
1378
|
+
const { values, positionals } = parseArgs5({
|
|
1379
|
+
args: argv,
|
|
1380
|
+
allowPositionals: true,
|
|
1381
|
+
options: {
|
|
1382
|
+
to: { type: "string" },
|
|
1383
|
+
"user-id": { type: "string" },
|
|
1384
|
+
prop: { type: "string", multiple: true },
|
|
1385
|
+
props: { type: "string" },
|
|
1386
|
+
from: { type: "string" },
|
|
1387
|
+
subject: { type: "string" },
|
|
1388
|
+
"reply-to": { type: "string" },
|
|
1389
|
+
category: { type: "string" },
|
|
1390
|
+
"skip-preference-check": { type: "boolean", default: false },
|
|
1391
|
+
"idempotency-key": { type: "string" },
|
|
1392
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
if (values.help) {
|
|
1396
|
+
ctx.out.log(usage5);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const template = positionals[0];
|
|
1400
|
+
if (!template) {
|
|
1401
|
+
ctx.out.fail(
|
|
1402
|
+
"emails send requires a template, e.g. hogsend emails send welcome --to a@b.com"
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
const to = values.to;
|
|
1406
|
+
const userId = values["user-id"];
|
|
1407
|
+
if (!to && !userId) {
|
|
1408
|
+
ctx.out.fail("emails send requires at least one of --to or --user-id");
|
|
1409
|
+
}
|
|
1410
|
+
const props = parseProps3(ctx, values.props, values.prop);
|
|
1411
|
+
const body = { template };
|
|
1412
|
+
if (to) body.to = to;
|
|
1413
|
+
if (userId) body.userId = userId;
|
|
1414
|
+
if (props) body.props = props;
|
|
1415
|
+
if (values.from) body.from = values.from;
|
|
1416
|
+
if (values.subject) body.subject = values.subject;
|
|
1417
|
+
if (values["reply-to"]) body.replyTo = values["reply-to"];
|
|
1418
|
+
if (values.category) body.category = values.category;
|
|
1419
|
+
if (values["skip-preference-check"]) body.skipPreferenceCheck = true;
|
|
1420
|
+
if (values["idempotency-key"]) {
|
|
1421
|
+
body.idempotencyKey = values["idempotency-key"];
|
|
1422
|
+
}
|
|
1423
|
+
let res;
|
|
1424
|
+
try {
|
|
1425
|
+
res = await ctx.out.step(
|
|
1426
|
+
`Sending ${template}`,
|
|
1427
|
+
() => ctx.dataHttp.post("/v1/emails", body)
|
|
1428
|
+
);
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
if (isHttpError(error)) {
|
|
1431
|
+
ctx.out.fail(error.message);
|
|
1432
|
+
}
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
1435
|
+
if (ctx.json) {
|
|
1436
|
+
ctx.out.json(res);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
ctx.out.intro(`${badge3} send`);
|
|
1440
|
+
ctx.out.kv(
|
|
1441
|
+
{
|
|
1442
|
+
emailSendId: res.emailSendId,
|
|
1443
|
+
template,
|
|
1444
|
+
recipient: to ?? userId ?? "",
|
|
1445
|
+
status: statusColor2(res.status),
|
|
1446
|
+
reason: res.reason ?? ""
|
|
1447
|
+
},
|
|
1448
|
+
"Email send"
|
|
1449
|
+
);
|
|
1450
|
+
ctx.out.outro(`${template} \u2192 ${statusColor2(res.status)}.`);
|
|
1451
|
+
}
|
|
1452
|
+
async function run5(ctx) {
|
|
1453
|
+
const sub = ctx.argv[0];
|
|
1454
|
+
switch (sub) {
|
|
1455
|
+
case "send":
|
|
1456
|
+
return runSend2(ctx, ctx.argv.slice(1));
|
|
1457
|
+
case void 0:
|
|
1458
|
+
ctx.out.fail(
|
|
1459
|
+
"emails requires a subcommand: send (see hogsend emails --help)"
|
|
1460
|
+
);
|
|
1461
|
+
break;
|
|
1462
|
+
default:
|
|
1463
|
+
ctx.out.fail(`unknown emails subcommand "${sub}" \u2014 expected send`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
var emailsCommand = {
|
|
1467
|
+
name: "emails",
|
|
1468
|
+
summary: "Send a transactional email through the data plane",
|
|
1469
|
+
usage: usage5,
|
|
1470
|
+
run: run5
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
// src/commands/events.ts
|
|
1474
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
1475
|
+
var usage6 = `hogsend events <userId> [options]
|
|
1476
|
+
hogsend events send <name> [options]
|
|
1477
|
+
|
|
1478
|
+
Read a single user's event history (admin API), or send an event into the data
|
|
1479
|
+
plane to drive journeys/buckets.
|
|
1480
|
+
|
|
1481
|
+
Read mode \u2014 hogsend events <userId>:
|
|
1482
|
+
Stream the event history for a single user, newest first. Wraps
|
|
1483
|
+
GET /v1/admin/events?userId=<userId>.
|
|
1484
|
+
|
|
1485
|
+
Arguments:
|
|
1486
|
+
<userId> The user (distinct) id to fetch events for. Required.
|
|
1487
|
+
|
|
1488
|
+
Options:
|
|
1489
|
+
--event <name> Filter to a single event name.
|
|
1490
|
+
--from <iso> Only events at/after this ISO-8601 timestamp.
|
|
1491
|
+
--to <iso> Only events at/before this ISO-8601 timestamp.
|
|
1492
|
+
--limit <n> Max events to return (1-100, default 50).
|
|
1493
|
+
--offset <n> Pagination offset (default 0).
|
|
1494
|
+
|
|
1495
|
+
Send mode \u2014 hogsend events send <name>:
|
|
1496
|
+
Push an event into POST /v1/events (data plane, ingest key). At least one of
|
|
1497
|
+
--email / --user-id is required.
|
|
1498
|
+
|
|
1499
|
+
Options:
|
|
1500
|
+
--email <addr> Recipient/identity email.
|
|
1501
|
+
--user-id <id> External (distinct) id.
|
|
1502
|
+
--prop <key=value> Event property; repeatable. Value parsed as JSON,
|
|
1503
|
+
falling back to a string.
|
|
1504
|
+
--props <json> Event properties as one JSON object.
|
|
1505
|
+
--contact-prop <k=v> Contact property to merge onto the contact; repeatable.
|
|
1506
|
+
--contact-props <json> Contact properties as one JSON object.
|
|
1507
|
+
--list <id> Subscribe to a list; repeatable.
|
|
1508
|
+
--unlist <id> Unsubscribe from a list; repeatable.
|
|
1509
|
+
--idempotency-key <k> Dedup key (sent as the Idempotency-Key header).
|
|
1510
|
+
--timestamp <iso> Override the event timestamp.
|
|
1511
|
+
|
|
1512
|
+
Global options (handled by the router): --url, --admin-key, --data-key, --json,
|
|
1513
|
+
-h/--help.
|
|
934
1514
|
|
|
935
1515
|
Examples:
|
|
936
1516
|
hogsend events user_123
|
|
937
1517
|
hogsend events user_123 --event signup --limit 10
|
|
938
|
-
hogsend events user_123 --from 2026-01-01T00:00:00Z --json
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1518
|
+
hogsend events user_123 --from 2026-01-01T00:00:00Z --json
|
|
1519
|
+
hogsend events send signup --user-id user_123 --prop plan=pro
|
|
1520
|
+
hogsend events send purchase --email a@b.com --props '{"amount":49}' --json`;
|
|
1521
|
+
async function run6(ctx) {
|
|
1522
|
+
if (ctx.argv[0] === "send") {
|
|
1523
|
+
return runSend3(ctx, ctx.argv.slice(1));
|
|
1524
|
+
}
|
|
1525
|
+
return runRead(ctx, ctx.argv);
|
|
1526
|
+
}
|
|
1527
|
+
async function runRead(ctx, argv) {
|
|
1528
|
+
const { values, positionals } = parseArgs6({
|
|
1529
|
+
args: argv,
|
|
942
1530
|
allowPositionals: true,
|
|
943
1531
|
options: {
|
|
944
1532
|
event: { type: "string" },
|
|
@@ -950,7 +1538,7 @@ async function run4(ctx) {
|
|
|
950
1538
|
}
|
|
951
1539
|
});
|
|
952
1540
|
if (values.help) {
|
|
953
|
-
ctx.out.log(
|
|
1541
|
+
ctx.out.log(usage6);
|
|
954
1542
|
return;
|
|
955
1543
|
}
|
|
956
1544
|
const userId = positionals[0];
|
|
@@ -1005,6 +1593,145 @@ async function run4(ctx) {
|
|
|
1005
1593
|
`${color.green(String(shown))} event${shown === 1 ? "" : "s"} ` + color.dim(`(${data.offset + 1}-${through} of ${data.total})`)
|
|
1006
1594
|
);
|
|
1007
1595
|
}
|
|
1596
|
+
async function runSend3(ctx, argv) {
|
|
1597
|
+
const { values, positionals } = parseArgs6({
|
|
1598
|
+
args: argv,
|
|
1599
|
+
allowPositionals: true,
|
|
1600
|
+
options: {
|
|
1601
|
+
email: { type: "string" },
|
|
1602
|
+
"user-id": { type: "string" },
|
|
1603
|
+
prop: { type: "string", multiple: true },
|
|
1604
|
+
props: { type: "string" },
|
|
1605
|
+
"contact-prop": { type: "string", multiple: true },
|
|
1606
|
+
"contact-props": { type: "string" },
|
|
1607
|
+
list: { type: "string", multiple: true },
|
|
1608
|
+
unlist: { type: "string", multiple: true },
|
|
1609
|
+
"idempotency-key": { type: "string" },
|
|
1610
|
+
timestamp: { type: "string" },
|
|
1611
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
if (values.help) {
|
|
1615
|
+
ctx.out.log(usage6);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const name = positionals[0];
|
|
1619
|
+
if (!name) {
|
|
1620
|
+
ctx.out.fail(
|
|
1621
|
+
"events send requires an event name, e.g. hogsend events send signup --user-id user_123"
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
const email = values.email;
|
|
1625
|
+
const userId = values["user-id"];
|
|
1626
|
+
if (!email && !userId) {
|
|
1627
|
+
ctx.out.fail("events send requires at least one of --email or --user-id");
|
|
1628
|
+
}
|
|
1629
|
+
const eventProperties = parseProps4(ctx, values.props, values.prop, "prop");
|
|
1630
|
+
const contactProperties = parseProps4(
|
|
1631
|
+
ctx,
|
|
1632
|
+
values["contact-props"],
|
|
1633
|
+
values["contact-prop"],
|
|
1634
|
+
"contact-prop"
|
|
1635
|
+
);
|
|
1636
|
+
const lists = parseLists2(values.list, values.unlist);
|
|
1637
|
+
const body = { name };
|
|
1638
|
+
if (email) body.email = email;
|
|
1639
|
+
if (userId) body.userId = userId;
|
|
1640
|
+
if (eventProperties) body.eventProperties = eventProperties;
|
|
1641
|
+
if (contactProperties) body.contactProperties = contactProperties;
|
|
1642
|
+
if (lists) body.lists = lists;
|
|
1643
|
+
if (values["idempotency-key"]) {
|
|
1644
|
+
body.idempotencyKey = values["idempotency-key"];
|
|
1645
|
+
}
|
|
1646
|
+
if (values.timestamp) body.timestamp = values.timestamp;
|
|
1647
|
+
let res;
|
|
1648
|
+
try {
|
|
1649
|
+
res = await ctx.out.step(
|
|
1650
|
+
`Sending event ${name}`,
|
|
1651
|
+
() => ctx.dataHttp.post("/v1/events", body)
|
|
1652
|
+
);
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
if (isHttpError(error)) {
|
|
1655
|
+
ctx.out.fail(error.message);
|
|
1656
|
+
}
|
|
1657
|
+
throw error;
|
|
1658
|
+
}
|
|
1659
|
+
if (ctx.json) {
|
|
1660
|
+
ctx.out.json(res);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} events send`);
|
|
1664
|
+
const exited = res.exits.filter((e) => e.exited);
|
|
1665
|
+
ctx.out.kv(
|
|
1666
|
+
{
|
|
1667
|
+
event: name,
|
|
1668
|
+
stored: res.stored,
|
|
1669
|
+
identity: email ?? userId ?? "",
|
|
1670
|
+
exits: res.exits.length,
|
|
1671
|
+
"journeys exited": exited.length
|
|
1672
|
+
},
|
|
1673
|
+
"Event sent"
|
|
1674
|
+
);
|
|
1675
|
+
if (exited.length > 0) {
|
|
1676
|
+
ctx.out.table(
|
|
1677
|
+
exited.map((e) => ({ journeyId: e.journeyId, stateId: e.stateId })),
|
|
1678
|
+
["journeyId", "stateId"]
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
ctx.out.outro(
|
|
1682
|
+
res.stored ? `${color.green("Stored")} ${name}.` : color.dim(`${name} was deduped (not stored).`)
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
function parseProps4(ctx, json, pairs, flagName) {
|
|
1686
|
+
const out = {};
|
|
1687
|
+
let any = false;
|
|
1688
|
+
if (json !== void 0) {
|
|
1689
|
+
let parsed;
|
|
1690
|
+
try {
|
|
1691
|
+
parsed = JSON.parse(json);
|
|
1692
|
+
} catch {
|
|
1693
|
+
ctx.out.fail(`--${flagName}s must be valid JSON, got: ${json}`);
|
|
1694
|
+
}
|
|
1695
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1696
|
+
ctx.out.fail(`--${flagName}s must be a JSON object`);
|
|
1697
|
+
}
|
|
1698
|
+
Object.assign(out, parsed);
|
|
1699
|
+
any = true;
|
|
1700
|
+
}
|
|
1701
|
+
for (const pair of pairs ?? []) {
|
|
1702
|
+
const eq = pair.indexOf("=");
|
|
1703
|
+
if (eq === -1) {
|
|
1704
|
+
ctx.out.fail(`--${flagName} must be key=value, got: ${pair}`);
|
|
1705
|
+
}
|
|
1706
|
+
const key = pair.slice(0, eq).trim();
|
|
1707
|
+
if (key === "") {
|
|
1708
|
+
ctx.out.fail(`--${flagName} key cannot be empty, got: ${pair}`);
|
|
1709
|
+
}
|
|
1710
|
+
out[key] = coerceValue4(pair.slice(eq + 1));
|
|
1711
|
+
any = true;
|
|
1712
|
+
}
|
|
1713
|
+
return any ? out : void 0;
|
|
1714
|
+
}
|
|
1715
|
+
function coerceValue4(raw) {
|
|
1716
|
+
try {
|
|
1717
|
+
return JSON.parse(raw);
|
|
1718
|
+
} catch {
|
|
1719
|
+
return raw;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function parseLists2(subscribe, unsubscribe) {
|
|
1723
|
+
const out = {};
|
|
1724
|
+
let any = false;
|
|
1725
|
+
for (const id of subscribe ?? []) {
|
|
1726
|
+
out[id] = true;
|
|
1727
|
+
any = true;
|
|
1728
|
+
}
|
|
1729
|
+
for (const id of unsubscribe ?? []) {
|
|
1730
|
+
out[id] = false;
|
|
1731
|
+
any = true;
|
|
1732
|
+
}
|
|
1733
|
+
return any ? out : void 0;
|
|
1734
|
+
}
|
|
1008
1735
|
function parseNumber(raw, name, ctx) {
|
|
1009
1736
|
if (raw === void 0) return void 0;
|
|
1010
1737
|
const n = Number(raw);
|
|
@@ -1022,14 +1749,14 @@ function summarizeProps(props) {
|
|
|
1022
1749
|
}
|
|
1023
1750
|
var eventsCommand = {
|
|
1024
1751
|
name: "events",
|
|
1025
|
-
summary: "Stream a
|
|
1026
|
-
usage:
|
|
1027
|
-
run:
|
|
1752
|
+
summary: "Stream a user's event history, or send an event",
|
|
1753
|
+
usage: usage6,
|
|
1754
|
+
run: run6
|
|
1028
1755
|
};
|
|
1029
1756
|
|
|
1030
1757
|
// src/commands/journeys.ts
|
|
1031
|
-
import { parseArgs as
|
|
1032
|
-
var
|
|
1758
|
+
import { parseArgs as parseArgs7 } from "util";
|
|
1759
|
+
var usage7 = `hogsend journeys <subcommand> [options]
|
|
1033
1760
|
|
|
1034
1761
|
Inspect and toggle journeys via the admin API (/v1/admin/journeys).
|
|
1035
1762
|
|
|
@@ -1051,14 +1778,14 @@ Examples:
|
|
|
1051
1778
|
hogsend journeys list --enabled true
|
|
1052
1779
|
hogsend journeys get activation-welcome --json
|
|
1053
1780
|
hogsend journeys disable churn-prevention`;
|
|
1054
|
-
function
|
|
1781
|
+
function badge4() {
|
|
1055
1782
|
return `${color.bgMagenta(color.black(" hogsend "))} journeys`;
|
|
1056
1783
|
}
|
|
1057
|
-
function
|
|
1784
|
+
function statusColor3(enabled) {
|
|
1058
1785
|
return enabled ? color.green("enabled") : color.yellow("disabled");
|
|
1059
1786
|
}
|
|
1060
1787
|
async function runList2(ctx) {
|
|
1061
|
-
const { values } =
|
|
1788
|
+
const { values } = parseArgs7({
|
|
1062
1789
|
args: ctx.argv,
|
|
1063
1790
|
allowPositionals: true,
|
|
1064
1791
|
options: {
|
|
@@ -1069,7 +1796,7 @@ async function runList2(ctx) {
|
|
|
1069
1796
|
}
|
|
1070
1797
|
});
|
|
1071
1798
|
if (values.help) {
|
|
1072
|
-
ctx.out.log(
|
|
1799
|
+
ctx.out.log(usage7);
|
|
1073
1800
|
return;
|
|
1074
1801
|
}
|
|
1075
1802
|
if (values.enabled !== void 0 && !["true", "false"].includes(values.enabled)) {
|
|
@@ -1080,7 +1807,7 @@ async function runList2(ctx) {
|
|
|
1080
1807
|
limit: values.limit,
|
|
1081
1808
|
offset: values.offset
|
|
1082
1809
|
};
|
|
1083
|
-
if (!ctx.json) ctx.out.intro(
|
|
1810
|
+
if (!ctx.json) ctx.out.intro(badge4());
|
|
1084
1811
|
const data = await ctx.out.step(
|
|
1085
1812
|
"Fetching journeys",
|
|
1086
1813
|
() => ctx.http.get("/v1/admin/journeys", query)
|
|
@@ -1096,7 +1823,7 @@ async function runList2(ctx) {
|
|
|
1096
1823
|
data.journeys.map((j) => ({
|
|
1097
1824
|
id: j.id,
|
|
1098
1825
|
name: j.name,
|
|
1099
|
-
status:
|
|
1826
|
+
status: statusColor3(j.enabled),
|
|
1100
1827
|
trigger: j.trigger.event,
|
|
1101
1828
|
active: j.counts.active,
|
|
1102
1829
|
waiting: j.counts.waiting,
|
|
@@ -1125,7 +1852,7 @@ async function runGet2(ctx, id) {
|
|
|
1125
1852
|
"journeys get requires a journey id, e.g. hogsend journeys get activation-welcome"
|
|
1126
1853
|
);
|
|
1127
1854
|
}
|
|
1128
|
-
if (!ctx.json) ctx.out.intro(
|
|
1855
|
+
if (!ctx.json) ctx.out.intro(badge4());
|
|
1129
1856
|
const data = await ctx.out.step(
|
|
1130
1857
|
`Fetching journey ${id}`,
|
|
1131
1858
|
() => ctx.http.get(
|
|
@@ -1142,7 +1869,7 @@ async function runGet2(ctx, id) {
|
|
|
1142
1869
|
id: j.id,
|
|
1143
1870
|
name: j.name,
|
|
1144
1871
|
description: j.description ?? "",
|
|
1145
|
-
status:
|
|
1872
|
+
status: statusColor3(j.enabled),
|
|
1146
1873
|
trigger: j.trigger.event,
|
|
1147
1874
|
entryLimit: j.entryLimit,
|
|
1148
1875
|
exitOn: j.exitOn?.map((e) => e.event).join(", ") ?? "(none)"
|
|
@@ -1182,7 +1909,7 @@ async function runToggle(ctx, id, enabled) {
|
|
|
1182
1909
|
`journeys ${verb} requires a journey id, e.g. hogsend journeys ${verb} activation-welcome`
|
|
1183
1910
|
);
|
|
1184
1911
|
}
|
|
1185
|
-
if (!ctx.json) ctx.out.intro(
|
|
1912
|
+
if (!ctx.json) ctx.out.intro(badge4());
|
|
1186
1913
|
const data = await ctx.out.step(
|
|
1187
1914
|
`${enabled ? "Enabling" : "Disabling"} ${id}`,
|
|
1188
1915
|
() => ctx.http.patch(
|
|
@@ -1198,14 +1925,14 @@ async function runToggle(ctx, id, enabled) {
|
|
|
1198
1925
|
ctx.out.note(
|
|
1199
1926
|
[
|
|
1200
1927
|
`${color.bold(j.name)} (${j.id})`,
|
|
1201
|
-
`status: ${
|
|
1928
|
+
`status: ${statusColor3(j.enabled)}`,
|
|
1202
1929
|
`updated: ${j.updatedAt}`
|
|
1203
1930
|
].join("\n"),
|
|
1204
1931
|
`Journey ${enabled ? "enabled" : "disabled"}`
|
|
1205
1932
|
);
|
|
1206
|
-
ctx.out.outro(`${j.id} is now ${
|
|
1933
|
+
ctx.out.outro(`${j.id} is now ${statusColor3(j.enabled)}.`);
|
|
1207
1934
|
}
|
|
1208
|
-
async function
|
|
1935
|
+
async function run7(ctx) {
|
|
1209
1936
|
const sub = ctx.argv[0];
|
|
1210
1937
|
const rest = ctx.argv.slice(1);
|
|
1211
1938
|
const subCtx = { ...ctx, argv: rest };
|
|
@@ -1217,7 +1944,7 @@ async function run5(ctx) {
|
|
|
1217
1944
|
case "get": {
|
|
1218
1945
|
const id = rest.find((a) => !a.startsWith("-"));
|
|
1219
1946
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1220
|
-
ctx.out.log(
|
|
1947
|
+
ctx.out.log(usage7);
|
|
1221
1948
|
return;
|
|
1222
1949
|
}
|
|
1223
1950
|
await runGet2(subCtx, id);
|
|
@@ -1225,7 +1952,7 @@ async function run5(ctx) {
|
|
|
1225
1952
|
}
|
|
1226
1953
|
case "enable": {
|
|
1227
1954
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1228
|
-
ctx.out.log(
|
|
1955
|
+
ctx.out.log(usage7);
|
|
1229
1956
|
return;
|
|
1230
1957
|
}
|
|
1231
1958
|
await runToggle(
|
|
@@ -1237,7 +1964,7 @@ async function run5(ctx) {
|
|
|
1237
1964
|
}
|
|
1238
1965
|
case "disable": {
|
|
1239
1966
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1240
|
-
ctx.out.log(
|
|
1967
|
+
ctx.out.log(usage7);
|
|
1241
1968
|
return;
|
|
1242
1969
|
}
|
|
1243
1970
|
await runToggle(
|
|
@@ -1271,14 +1998,14 @@ async function run5(ctx) {
|
|
|
1271
1998
|
var journeysCommand = {
|
|
1272
1999
|
name: "journeys",
|
|
1273
2000
|
summary: "List, inspect, enable, and disable journeys",
|
|
1274
|
-
usage:
|
|
1275
|
-
run:
|
|
2001
|
+
usage: usage7,
|
|
2002
|
+
run: run7
|
|
1276
2003
|
};
|
|
1277
2004
|
|
|
1278
2005
|
// src/commands/patch.ts
|
|
1279
2006
|
import { spawnSync } from "child_process";
|
|
1280
|
-
import { parseArgs as
|
|
1281
|
-
var
|
|
2007
|
+
import { parseArgs as parseArgs8 } from "util";
|
|
2008
|
+
var usage8 = `hogsend patch <package> [--cwd <dir>]
|
|
1282
2009
|
|
|
1283
2010
|
Thin wrapper over pnpm's native patch flow. Runs \`pnpm patch <package>\`, which
|
|
1284
2011
|
extracts the package into a temp dir and prints the path to edit. After editing,
|
|
@@ -1289,8 +2016,8 @@ This does NOT replace scripts/patch-check.sh (the patch re-apply contract).
|
|
|
1289
2016
|
Options:
|
|
1290
2017
|
--cwd <dir> Project root to run pnpm in (defaults to current directory).
|
|
1291
2018
|
-h, --help Show this help.`;
|
|
1292
|
-
async function
|
|
1293
|
-
const { values, positionals } =
|
|
2019
|
+
async function run8(ctx) {
|
|
2020
|
+
const { values, positionals } = parseArgs8({
|
|
1294
2021
|
args: ctx.argv,
|
|
1295
2022
|
allowPositionals: true,
|
|
1296
2023
|
options: {
|
|
@@ -1299,7 +2026,7 @@ async function run6(ctx) {
|
|
|
1299
2026
|
}
|
|
1300
2027
|
});
|
|
1301
2028
|
if (values.help) {
|
|
1302
|
-
ctx.out.log(
|
|
2029
|
+
ctx.out.log(usage8);
|
|
1303
2030
|
return;
|
|
1304
2031
|
}
|
|
1305
2032
|
const pkg = positionals[0];
|
|
@@ -1339,8 +2066,8 @@ async function run6(ctx) {
|
|
|
1339
2066
|
var patchCommand = {
|
|
1340
2067
|
name: "patch",
|
|
1341
2068
|
summary: "Patch a package via pnpm's native patch flow",
|
|
1342
|
-
usage:
|
|
1343
|
-
run:
|
|
2069
|
+
usage: usage8,
|
|
2070
|
+
run: run8
|
|
1344
2071
|
};
|
|
1345
2072
|
|
|
1346
2073
|
// src/commands/setup.ts
|
|
@@ -1348,7 +2075,7 @@ import { spawnSync as spawnSync2 } from "child_process";
|
|
|
1348
2075
|
import { randomBytes } from "crypto";
|
|
1349
2076
|
import { copyFileSync, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1350
2077
|
import { join as join4 } from "path";
|
|
1351
|
-
import { parseArgs as
|
|
2078
|
+
import { parseArgs as parseArgs9 } from "util";
|
|
1352
2079
|
import { confirm } from "@clack/prompts";
|
|
1353
2080
|
|
|
1354
2081
|
// src/lib/prompt.ts
|
|
@@ -1362,7 +2089,7 @@ function bail(value) {
|
|
|
1362
2089
|
}
|
|
1363
2090
|
|
|
1364
2091
|
// src/commands/setup.ts
|
|
1365
|
-
var
|
|
2092
|
+
var usage9 = `hogsend setup [--cwd <dir>] [--yes] [--json]
|
|
1366
2093
|
|
|
1367
2094
|
Interactive local onboarding for a scaffolded Hogsend app. Mirrors the
|
|
1368
2095
|
create-hogsend "next steps":
|
|
@@ -1473,8 +2200,8 @@ function runCmd(cmd, args, cwd, json) {
|
|
|
1473
2200
|
});
|
|
1474
2201
|
return { status: result.status, ok: result.status === 0 };
|
|
1475
2202
|
}
|
|
1476
|
-
async function
|
|
1477
|
-
const { values } =
|
|
2203
|
+
async function run9(ctx) {
|
|
2204
|
+
const { values } = parseArgs9({
|
|
1478
2205
|
args: ctx.argv,
|
|
1479
2206
|
allowPositionals: true,
|
|
1480
2207
|
options: {
|
|
@@ -1484,7 +2211,7 @@ async function run7(ctx) {
|
|
|
1484
2211
|
}
|
|
1485
2212
|
});
|
|
1486
2213
|
if (values.help) {
|
|
1487
|
-
ctx.out.log(
|
|
2214
|
+
ctx.out.log(usage9);
|
|
1488
2215
|
return;
|
|
1489
2216
|
}
|
|
1490
2217
|
const cwd = values.cwd ?? process.cwd();
|
|
@@ -1595,16 +2322,16 @@ async function run7(ctx) {
|
|
|
1595
2322
|
var setupCommand = {
|
|
1596
2323
|
name: "setup",
|
|
1597
2324
|
summary: "Local onboarding: docker compose up, gen secret, db:migrate",
|
|
1598
|
-
usage:
|
|
1599
|
-
run:
|
|
2325
|
+
usage: usage9,
|
|
2326
|
+
run: run9
|
|
1600
2327
|
};
|
|
1601
2328
|
|
|
1602
2329
|
// src/commands/skills.ts
|
|
1603
2330
|
import { existsSync as existsSync5 } from "fs";
|
|
1604
2331
|
import { join as join5 } from "path";
|
|
1605
|
-
import { parseArgs as
|
|
2332
|
+
import { parseArgs as parseArgs10 } from "util";
|
|
1606
2333
|
import { multiselect } from "@clack/prompts";
|
|
1607
|
-
var
|
|
2334
|
+
var usage10 = `hogsend skills <subcommand> [options]
|
|
1608
2335
|
|
|
1609
2336
|
Manage the Claude Code skills bundled with @hogsend/cli. Bundled skills teach
|
|
1610
2337
|
agents how to drive the hogsend CLI; \`add\` copies them into your project's
|
|
@@ -1665,7 +2392,7 @@ function runList3(ctx) {
|
|
|
1665
2392
|
);
|
|
1666
2393
|
}
|
|
1667
2394
|
async function runAdd(ctx, argv) {
|
|
1668
|
-
const { values, positionals } =
|
|
2395
|
+
const { values, positionals } = parseArgs10({
|
|
1669
2396
|
args: argv,
|
|
1670
2397
|
allowPositionals: true,
|
|
1671
2398
|
options: {
|
|
@@ -1675,7 +2402,7 @@ async function runAdd(ctx, argv) {
|
|
|
1675
2402
|
}
|
|
1676
2403
|
});
|
|
1677
2404
|
if (values.help) {
|
|
1678
|
-
ctx.out.log(
|
|
2405
|
+
ctx.out.log(usage10);
|
|
1679
2406
|
return;
|
|
1680
2407
|
}
|
|
1681
2408
|
const cwd = process.cwd();
|
|
@@ -1743,7 +2470,7 @@ async function runAdd(ctx, argv) {
|
|
|
1743
2470
|
`Installed ${installedCount} skill${installedCount === 1 ? "" : "s"}` + (skippedCount > 0 ? `, skipped ${skippedCount}.` : ".")
|
|
1744
2471
|
);
|
|
1745
2472
|
}
|
|
1746
|
-
async function
|
|
2473
|
+
async function run10(ctx) {
|
|
1747
2474
|
const sub = ctx.argv[0];
|
|
1748
2475
|
switch (sub) {
|
|
1749
2476
|
case "list":
|
|
@@ -1755,7 +2482,7 @@ async function run8(ctx) {
|
|
|
1755
2482
|
case void 0:
|
|
1756
2483
|
case "-h":
|
|
1757
2484
|
case "--help":
|
|
1758
|
-
ctx.out.log(
|
|
2485
|
+
ctx.out.log(usage10);
|
|
1759
2486
|
return;
|
|
1760
2487
|
default:
|
|
1761
2488
|
ctx.out.fail(
|
|
@@ -1766,13 +2493,13 @@ async function run8(ctx) {
|
|
|
1766
2493
|
var skillsCommand = {
|
|
1767
2494
|
name: "skills",
|
|
1768
2495
|
summary: "List + install bundled Claude Code skills into .claude/skills",
|
|
1769
|
-
usage:
|
|
1770
|
-
run:
|
|
2496
|
+
usage: usage10,
|
|
2497
|
+
run: run10
|
|
1771
2498
|
};
|
|
1772
2499
|
|
|
1773
2500
|
// src/commands/stats.ts
|
|
1774
|
-
import { parseArgs as
|
|
1775
|
-
var
|
|
2501
|
+
import { parseArgs as parseArgs11 } from "util";
|
|
2502
|
+
var usage11 = `hogsend stats [--json]
|
|
1776
2503
|
|
|
1777
2504
|
Show system-wide overview metrics from a running Hogsend instance.
|
|
1778
2505
|
Wraps GET /v1/admin/metrics/overview.
|
|
@@ -1794,8 +2521,8 @@ Options:
|
|
|
1794
2521
|
function pct(rate) {
|
|
1795
2522
|
return `${(rate * 100).toFixed(2)}%`;
|
|
1796
2523
|
}
|
|
1797
|
-
async function
|
|
1798
|
-
const { values } =
|
|
2524
|
+
async function run11(ctx) {
|
|
2525
|
+
const { values } = parseArgs11({
|
|
1799
2526
|
args: ctx.argv,
|
|
1800
2527
|
allowPositionals: true,
|
|
1801
2528
|
options: {
|
|
@@ -1803,7 +2530,7 @@ async function run9(ctx) {
|
|
|
1803
2530
|
}
|
|
1804
2531
|
});
|
|
1805
2532
|
if (values.help) {
|
|
1806
|
-
ctx.out.log(
|
|
2533
|
+
ctx.out.log(usage11);
|
|
1807
2534
|
return;
|
|
1808
2535
|
}
|
|
1809
2536
|
const metrics = await ctx.out.step(
|
|
@@ -1832,8 +2559,8 @@ async function run9(ctx) {
|
|
|
1832
2559
|
var statsCommand = {
|
|
1833
2560
|
name: "stats",
|
|
1834
2561
|
summary: "Show system-wide overview metrics",
|
|
1835
|
-
usage:
|
|
1836
|
-
run:
|
|
2562
|
+
usage: usage11,
|
|
2563
|
+
run: run11
|
|
1837
2564
|
};
|
|
1838
2565
|
|
|
1839
2566
|
// src/commands/studio.ts
|
|
@@ -1842,8 +2569,8 @@ import { createReadStream, existsSync as existsSync6, readFileSync as readFileSy
|
|
|
1842
2569
|
import { createServer } from "http";
|
|
1843
2570
|
import { extname, join as join6, normalize, resolve, sep as sep3 } from "path";
|
|
1844
2571
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1845
|
-
import { parseArgs as
|
|
1846
|
-
var
|
|
2572
|
+
import { parseArgs as parseArgs12 } from "util";
|
|
2573
|
+
var usage12 = `hogsend studio [options]
|
|
1847
2574
|
|
|
1848
2575
|
Serve the bundled Hogsend Studio (the admin SPA) locally and open it in a
|
|
1849
2576
|
browser. The Studio is a static single-page app; this command starts a tiny
|
|
@@ -1929,8 +2656,8 @@ function openBrowser(url) {
|
|
|
1929
2656
|
} catch {
|
|
1930
2657
|
}
|
|
1931
2658
|
}
|
|
1932
|
-
async function
|
|
1933
|
-
const { values, positionals } =
|
|
2659
|
+
async function run12(ctx) {
|
|
2660
|
+
const { values, positionals } = parseArgs12({
|
|
1934
2661
|
args: ctx.argv,
|
|
1935
2662
|
allowPositionals: true,
|
|
1936
2663
|
strict: false,
|
|
@@ -1943,7 +2670,7 @@ async function run10(ctx) {
|
|
|
1943
2670
|
}
|
|
1944
2671
|
});
|
|
1945
2672
|
if (values.help) {
|
|
1946
|
-
ctx.out.log(
|
|
2673
|
+
ctx.out.log(usage12);
|
|
1947
2674
|
return;
|
|
1948
2675
|
}
|
|
1949
2676
|
const port = Number(values.port ?? "3333");
|
|
@@ -2027,17 +2754,17 @@ async function run10(ctx) {
|
|
|
2027
2754
|
var studioCommand = {
|
|
2028
2755
|
name: "studio",
|
|
2029
2756
|
summary: "Serve the bundled Hogsend Studio admin SPA locally",
|
|
2030
|
-
usage:
|
|
2031
|
-
run:
|
|
2757
|
+
usage: usage12,
|
|
2758
|
+
run: run12
|
|
2032
2759
|
};
|
|
2033
2760
|
|
|
2034
2761
|
// src/commands/upgrade.ts
|
|
2035
2762
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
2036
2763
|
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
2037
2764
|
import { join as join7 } from "path";
|
|
2038
|
-
import { parseArgs as
|
|
2765
|
+
import { parseArgs as parseArgs13 } from "util";
|
|
2039
2766
|
import { confirm as confirm2 } from "@clack/prompts";
|
|
2040
|
-
var
|
|
2767
|
+
var usage13 = `hogsend upgrade [--cwd <dir>] [--pm <pnpm|npm|yarn|bun>] [options]
|
|
2041
2768
|
|
|
2042
2769
|
Upgrade a scaffolded Hogsend app in one step:
|
|
2043
2770
|
1. bump every @hogsend/* dependency to latest (or --to <version>), then
|
|
@@ -2073,8 +2800,8 @@ function hogsendDeps(cwd) {
|
|
|
2073
2800
|
function addArgs(pm, specs) {
|
|
2074
2801
|
return [pm === "npm" ? "install" : "add", ...specs];
|
|
2075
2802
|
}
|
|
2076
|
-
async function
|
|
2077
|
-
const { values } =
|
|
2803
|
+
async function run13(ctx) {
|
|
2804
|
+
const { values } = parseArgs13({
|
|
2078
2805
|
args: ctx.argv,
|
|
2079
2806
|
allowPositionals: true,
|
|
2080
2807
|
options: {
|
|
@@ -2088,7 +2815,7 @@ async function run11(ctx) {
|
|
|
2088
2815
|
}
|
|
2089
2816
|
});
|
|
2090
2817
|
if (values.help) {
|
|
2091
|
-
ctx.out.log(
|
|
2818
|
+
ctx.out.log(usage13);
|
|
2092
2819
|
return;
|
|
2093
2820
|
}
|
|
2094
2821
|
if (values["deps-only"] && values["skills-only"]) {
|
|
@@ -2213,8 +2940,8 @@ async function run11(ctx) {
|
|
|
2213
2940
|
var upgradeCommand = {
|
|
2214
2941
|
name: "upgrade",
|
|
2215
2942
|
summary: "Bump @hogsend/* deps to latest + refresh vendored skills",
|
|
2216
|
-
usage:
|
|
2217
|
-
run:
|
|
2943
|
+
usage: usage13,
|
|
2944
|
+
run: run13
|
|
2218
2945
|
};
|
|
2219
2946
|
|
|
2220
2947
|
// src/commands/index.ts
|
|
@@ -2224,6 +2951,8 @@ var commands = [
|
|
|
2224
2951
|
contactsCommand,
|
|
2225
2952
|
statsCommand,
|
|
2226
2953
|
eventsCommand,
|
|
2954
|
+
emailsCommand,
|
|
2955
|
+
campaignsCommand,
|
|
2227
2956
|
studioCommand,
|
|
2228
2957
|
setupCommand,
|
|
2229
2958
|
skillsCommand,
|
|
@@ -2235,10 +2964,10 @@ var commands = [
|
|
|
2235
2964
|
// src/lib/config.ts
|
|
2236
2965
|
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
2237
2966
|
import { join as join8 } from "path";
|
|
2238
|
-
import { parseArgs as
|
|
2967
|
+
import { parseArgs as parseArgs14 } from "util";
|
|
2239
2968
|
var DEFAULT_BASE_URL = "http://localhost:3002";
|
|
2240
2969
|
function parseGlobalFlags(argv) {
|
|
2241
|
-
const { values, tokens } =
|
|
2970
|
+
const { values, tokens } = parseArgs14({
|
|
2242
2971
|
args: argv,
|
|
2243
2972
|
allowPositionals: true,
|
|
2244
2973
|
strict: false,
|
|
@@ -2246,11 +2975,12 @@ function parseGlobalFlags(argv) {
|
|
|
2246
2975
|
options: {
|
|
2247
2976
|
url: { type: "string" },
|
|
2248
2977
|
"admin-key": { type: "string" },
|
|
2978
|
+
"data-key": { type: "string" },
|
|
2249
2979
|
json: { type: "boolean", default: false },
|
|
2250
2980
|
help: { type: "boolean", short: "h", default: false }
|
|
2251
2981
|
}
|
|
2252
2982
|
});
|
|
2253
|
-
const owned = /* @__PURE__ */ new Set(["url", "admin-key", "json", "help", "h"]);
|
|
2983
|
+
const owned = /* @__PURE__ */ new Set(["url", "admin-key", "data-key", "json", "help", "h"]);
|
|
2254
2984
|
const rest = [];
|
|
2255
2985
|
for (const token of tokens) {
|
|
2256
2986
|
if (token.kind === "positional") {
|
|
@@ -2268,6 +2998,7 @@ function parseGlobalFlags(argv) {
|
|
|
2268
2998
|
return {
|
|
2269
2999
|
url: typeof values.url === "string" ? values.url : void 0,
|
|
2270
3000
|
adminKey: typeof values["admin-key"] === "string" ? values["admin-key"] : void 0,
|
|
3001
|
+
dataKey: typeof values["data-key"] === "string" ? values["data-key"] : void 0,
|
|
2271
3002
|
json: values.json === true,
|
|
2272
3003
|
help: values.help === true,
|
|
2273
3004
|
rest
|
|
@@ -2303,9 +3034,11 @@ function resolveConfig(flags, cwd = process.cwd()) {
|
|
|
2303
3034
|
const dotenv = loadDotEnv(cwd);
|
|
2304
3035
|
const baseUrlRaw = flags.url ?? process.env.HOGSEND_API_URL ?? dotenv.HOGSEND_API_URL ?? DEFAULT_BASE_URL;
|
|
2305
3036
|
const adminKey = flags.adminKey ?? process.env.HOGSEND_ADMIN_KEY ?? process.env.ADMIN_API_KEY ?? dotenv.HOGSEND_ADMIN_KEY ?? dotenv.ADMIN_API_KEY;
|
|
3037
|
+
const dataKey = flags.dataKey ?? process.env.HOGSEND_DATA_KEY ?? process.env.HOGSEND_API_KEY ?? dotenv.HOGSEND_DATA_KEY ?? dotenv.HOGSEND_API_KEY;
|
|
2306
3038
|
return {
|
|
2307
3039
|
baseUrl: baseUrlRaw.replace(/\/+$/, ""),
|
|
2308
|
-
adminKey: adminKey && adminKey.length > 0 ? adminKey : void 0
|
|
3040
|
+
adminKey: adminKey && adminKey.length > 0 ? adminKey : void 0,
|
|
3041
|
+
dataKey: dataKey && dataKey.length > 0 ? dataKey : void 0
|
|
2309
3042
|
};
|
|
2310
3043
|
}
|
|
2311
3044
|
|
|
@@ -2332,6 +3065,7 @@ ${list}
|
|
|
2332
3065
|
${color.dim("Global options:")}
|
|
2333
3066
|
--url <baseUrl> Target instance (default HOGSEND_API_URL or http://localhost:3002)
|
|
2334
3067
|
--admin-key <key> Admin bearer token (default HOGSEND_ADMIN_KEY / ADMIN_API_KEY)
|
|
3068
|
+
--data-key <key> Ingest bearer token for writes (default HOGSEND_DATA_KEY / HOGSEND_API_KEY)
|
|
2335
3069
|
--json Emit machine-readable JSON only (for agents)
|
|
2336
3070
|
-h, --help Show help (use after a command for command help)
|
|
2337
3071
|
-v, --version Show version
|
|
@@ -2372,10 +3106,12 @@ ${rootUsage()}
|
|
|
2372
3106
|
}
|
|
2373
3107
|
const cfg = resolveConfig(flags);
|
|
2374
3108
|
const http = createAdminClient(cfg);
|
|
3109
|
+
const dataHttp = createDataPlaneClient(cfg);
|
|
2375
3110
|
await command.run({
|
|
2376
3111
|
argv: flags.rest,
|
|
2377
3112
|
cfg,
|
|
2378
3113
|
http,
|
|
3114
|
+
dataHttp,
|
|
2379
3115
|
out,
|
|
2380
3116
|
json: flags.json
|
|
2381
3117
|
});
|