@hogsend/cli 0.0.1 → 0.2.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.
Files changed (63) hide show
  1. package/dist/bin.js +2238 -75
  2. package/dist/bin.js.map +1 -1
  3. package/package.json +9 -1
  4. package/skills/hogsend-authoring-buckets/SKILL.md +69 -0
  5. package/skills/hogsend-authoring-buckets/references/bucket-id-aliases.md +117 -0
  6. package/skills/hogsend-authoring-buckets/references/bucket-meta.md +142 -0
  7. package/skills/hogsend-authoring-buckets/references/buckets-vs-journeys.md +96 -0
  8. package/skills/hogsend-authoring-buckets/references/register-a-bucket.md +129 -0
  9. package/skills/hogsend-authoring-emails/SKILL.md +68 -0
  10. package/skills/hogsend-authoring-emails/references/email-components.md +112 -0
  11. package/skills/hogsend-authoring-emails/references/preview-and-render.md +116 -0
  12. package/skills/hogsend-authoring-emails/references/template-four-file-contract.md +134 -0
  13. package/skills/hogsend-authoring-emails/references/tracking-and-unsubscribe.md +127 -0
  14. package/skills/hogsend-authoring-journeys/SKILL.md +88 -0
  15. package/skills/hogsend-authoring-journeys/references/branch-on-engagement.md +93 -0
  16. package/skills/hogsend-authoring-journeys/references/journey-context.md +110 -0
  17. package/skills/hogsend-authoring-journeys/references/journey-meta.md +142 -0
  18. package/skills/hogsend-authoring-journeys/references/register-a-journey.md +99 -0
  19. package/skills/hogsend-authoring-journeys/references/sending-email-from-a-journey.md +82 -0
  20. package/skills/hogsend-cli/SKILL.md +81 -0
  21. package/skills/hogsend-cli/references/debug-a-journey.md +66 -0
  22. package/skills/hogsend-cli/references/manage-journeys.md +53 -0
  23. package/skills/hogsend-cli/references/query-stats.md +66 -0
  24. package/skills/hogsend-cli/references/setup-local.md +52 -0
  25. package/skills/hogsend-conditions/SKILL.md +70 -0
  26. package/skills/hogsend-conditions/references/condition-types.md +251 -0
  27. package/skills/hogsend-conditions/references/durations.md +90 -0
  28. package/skills/hogsend-conditions/references/examples.md +188 -0
  29. package/skills/hogsend-database/SKILL.md +70 -0
  30. package/skills/hogsend-database/references/client-track-schema.md +97 -0
  31. package/skills/hogsend-database/references/migrations.md +132 -0
  32. package/skills/hogsend-database/references/schema-drift.md +123 -0
  33. package/skills/hogsend-deploy/SKILL.md +62 -0
  34. package/skills/hogsend-deploy/references/env-and-secrets.md +118 -0
  35. package/skills/hogsend-deploy/references/railway-two-services.md +122 -0
  36. package/skills/hogsend-deploy/references/upgrade-engine.md +92 -0
  37. package/skills/hogsend-webhooks-and-workflows/SKILL.md +68 -0
  38. package/skills/hogsend-webhooks-and-workflows/references/backfill-pattern.md +148 -0
  39. package/skills/hogsend-webhooks-and-workflows/references/custom-workflow.md +156 -0
  40. package/skills/hogsend-webhooks-and-workflows/references/webhook-source.md +172 -0
  41. package/src/bin.ts +73 -111
  42. package/src/commands/contacts.ts +316 -0
  43. package/src/commands/doctor.ts +239 -0
  44. package/src/commands/eject.ts +106 -0
  45. package/src/commands/events.ts +154 -0
  46. package/src/commands/index.ts +36 -0
  47. package/src/commands/journeys.ts +343 -0
  48. package/src/commands/patch.ts +80 -0
  49. package/src/commands/setup.ts +322 -0
  50. package/src/commands/skills.ts +208 -0
  51. package/src/commands/stats.ts +87 -0
  52. package/src/commands/studio.ts +261 -0
  53. package/src/commands/types.ts +41 -0
  54. package/src/commands/upgrade.ts +245 -0
  55. package/src/index.ts +2 -0
  56. package/src/lib/config.ts +147 -0
  57. package/src/lib/http.ts +145 -0
  58. package/src/lib/output.ts +185 -0
  59. package/src/lib/prompt.ts +17 -0
  60. package/src/lib/skills.ts +186 -0
  61. package/studio/assets/index-BVA9GZqq.css +1 -0
  62. package/studio/assets/index-kPwzOOyG.js +230 -0
  63. package/studio/index.html +13 -0
@@ -0,0 +1,343 @@
1
+ import { parseArgs } from "node:util";
2
+ import { isHttpError } from "../lib/http.js";
3
+ import { color } from "../lib/output.js";
4
+ import type { Command, CommandContext } from "./types.js";
5
+
6
+ const usage = `hogsend journeys <subcommand> [options]
7
+
8
+ Inspect and toggle journeys via the admin API (/v1/admin/journeys).
9
+
10
+ Subcommands:
11
+ list List journeys with status, trigger, and state counts.
12
+ get <id> Show one journey: trigger, exitOn, counts, recent states.
13
+ enable <id> Enable a journey (PATCH { enabled: true }).
14
+ disable <id> Disable a journey (PATCH { enabled: false }).
15
+
16
+ Options:
17
+ list:
18
+ --enabled <true|false> Filter by enabled state.
19
+ --limit <n> Page size (1-100, default 50).
20
+ --offset <n> Page offset (default 0).
21
+ --json Emit machine-readable JSON only.
22
+ -h, --help Show this help.
23
+
24
+ Examples:
25
+ hogsend journeys list --enabled true
26
+ hogsend journeys get activation-welcome --json
27
+ hogsend journeys disable churn-prevention`;
28
+
29
+ /** Shape returned by GET /v1/admin/journeys. */
30
+ interface JourneyCounts {
31
+ active: number;
32
+ waiting: number;
33
+ completed: number;
34
+ failed: number;
35
+ exited: number;
36
+ }
37
+
38
+ interface JourneyListItem {
39
+ id: string;
40
+ name: string;
41
+ description?: string;
42
+ enabled: boolean;
43
+ trigger: { event: string };
44
+ entryLimit: string;
45
+ counts: JourneyCounts;
46
+ }
47
+
48
+ interface ListResponse {
49
+ journeys: JourneyListItem[];
50
+ total: number;
51
+ limit: number;
52
+ offset: number;
53
+ }
54
+
55
+ interface JourneyState {
56
+ id: string;
57
+ userId: string;
58
+ userEmail: string;
59
+ journeyId: string;
60
+ currentNodeId: string;
61
+ status: string;
62
+ errorMessage: string | null;
63
+ entryCount: number;
64
+ completedAt: string | null;
65
+ exitedAt: string | null;
66
+ createdAt: string;
67
+ updatedAt: string;
68
+ }
69
+
70
+ interface JourneyDetail extends Omit<JourneyListItem, "trigger"> {
71
+ trigger: { event: string; where?: Record<string, unknown>[] };
72
+ exitOn?: { event: string; where?: Record<string, unknown>[] }[];
73
+ suppress: Record<string, number>;
74
+ recentStates: JourneyState[];
75
+ }
76
+
77
+ interface GetResponse {
78
+ journey: JourneyDetail;
79
+ }
80
+
81
+ interface PatchResponse {
82
+ journey: { id: string; name: string; enabled: boolean; updatedAt: string };
83
+ }
84
+
85
+ function badge(): string {
86
+ return `${color.bgMagenta(color.black(" hogsend "))} journeys`;
87
+ }
88
+
89
+ function statusColor(enabled: boolean): string {
90
+ return enabled ? color.green("enabled") : color.yellow("disabled");
91
+ }
92
+
93
+ async function runList(ctx: CommandContext): Promise<void> {
94
+ const { values } = parseArgs({
95
+ args: ctx.argv,
96
+ allowPositionals: true,
97
+ options: {
98
+ enabled: { type: "string" },
99
+ limit: { type: "string" },
100
+ offset: { type: "string" },
101
+ help: { type: "boolean", short: "h", default: false },
102
+ },
103
+ });
104
+
105
+ if (values.help) {
106
+ ctx.out.log(usage);
107
+ return;
108
+ }
109
+
110
+ if (
111
+ values.enabled !== undefined &&
112
+ !["true", "false"].includes(values.enabled)
113
+ ) {
114
+ ctx.out.fail("--enabled must be 'true' or 'false'");
115
+ }
116
+
117
+ const query = {
118
+ enabled: values.enabled,
119
+ limit: values.limit,
120
+ offset: values.offset,
121
+ };
122
+
123
+ if (!ctx.json) ctx.out.intro(badge());
124
+
125
+ const data = await ctx.out.step("Fetching journeys", () =>
126
+ ctx.http.get<ListResponse>("/v1/admin/journeys", query),
127
+ );
128
+
129
+ if (ctx.json) {
130
+ ctx.out.json(data);
131
+ return;
132
+ }
133
+
134
+ if (data.journeys.length === 0) {
135
+ ctx.out.note("No journeys matched.", "Journeys");
136
+ } else {
137
+ ctx.out.table(
138
+ data.journeys.map((j) => ({
139
+ id: j.id,
140
+ name: j.name,
141
+ status: statusColor(j.enabled),
142
+ trigger: j.trigger.event,
143
+ active: j.counts.active,
144
+ waiting: j.counts.waiting,
145
+ completed: j.counts.completed,
146
+ failed: j.counts.failed,
147
+ })),
148
+ [
149
+ "id",
150
+ "name",
151
+ "status",
152
+ "trigger",
153
+ "active",
154
+ "waiting",
155
+ "completed",
156
+ "failed",
157
+ ],
158
+ );
159
+ }
160
+
161
+ ctx.out.outro(
162
+ `${data.journeys.length} of ${data.total} journey(s) — offset ${data.offset}, limit ${data.limit}`,
163
+ );
164
+ }
165
+
166
+ async function runGet(
167
+ ctx: CommandContext,
168
+ id: string | undefined,
169
+ ): Promise<void> {
170
+ if (!id) {
171
+ ctx.out.fail(
172
+ "journeys get requires a journey id, e.g. hogsend journeys get activation-welcome",
173
+ );
174
+ }
175
+
176
+ if (!ctx.json) ctx.out.intro(badge());
177
+
178
+ const data = await ctx.out.step(`Fetching journey ${id}`, () =>
179
+ ctx.http.get<GetResponse>(
180
+ `/v1/admin/journeys/${encodeURIComponent(id as string)}`,
181
+ ),
182
+ );
183
+
184
+ if (ctx.json) {
185
+ ctx.out.json(data);
186
+ return;
187
+ }
188
+
189
+ const j = data.journey;
190
+ ctx.out.kv(
191
+ {
192
+ id: j.id,
193
+ name: j.name,
194
+ description: j.description ?? "",
195
+ status: statusColor(j.enabled),
196
+ trigger: j.trigger.event,
197
+ entryLimit: j.entryLimit,
198
+ exitOn: j.exitOn?.map((e) => e.event).join(", ") ?? "(none)",
199
+ },
200
+ "Journey",
201
+ );
202
+
203
+ ctx.out.kv(
204
+ {
205
+ active: j.counts.active,
206
+ waiting: j.counts.waiting,
207
+ completed: j.counts.completed,
208
+ failed: j.counts.failed,
209
+ exited: j.counts.exited,
210
+ },
211
+ "Counts",
212
+ );
213
+
214
+ if (j.recentStates.length === 0) {
215
+ ctx.out.note("No recent journey instances.", "Recent states");
216
+ } else {
217
+ ctx.out.table(
218
+ j.recentStates.map((s) => ({
219
+ userId: s.userId,
220
+ email: s.userEmail,
221
+ status: s.status,
222
+ node: s.currentNodeId,
223
+ updatedAt: s.updatedAt,
224
+ })),
225
+ ["userId", "email", "status", "node", "updatedAt"],
226
+ );
227
+ }
228
+
229
+ ctx.out.outro(`Journey ${j.id} is ${j.enabled ? "enabled" : "disabled"}.`);
230
+ }
231
+
232
+ async function runToggle(
233
+ ctx: CommandContext,
234
+ id: string | undefined,
235
+ enabled: boolean,
236
+ ): Promise<void> {
237
+ const verb = enabled ? "enable" : "disable";
238
+ if (!id) {
239
+ ctx.out.fail(
240
+ `journeys ${verb} requires a journey id, e.g. hogsend journeys ${verb} activation-welcome`,
241
+ );
242
+ }
243
+
244
+ if (!ctx.json) ctx.out.intro(badge());
245
+
246
+ const data = await ctx.out.step(
247
+ `${enabled ? "Enabling" : "Disabling"} ${id}`,
248
+ () =>
249
+ ctx.http.patch<PatchResponse>(
250
+ `/v1/admin/journeys/${encodeURIComponent(id as string)}`,
251
+ { enabled },
252
+ ),
253
+ );
254
+
255
+ if (ctx.json) {
256
+ ctx.out.json(data);
257
+ return;
258
+ }
259
+
260
+ const j = data.journey;
261
+ ctx.out.note(
262
+ [
263
+ `${color.bold(j.name)} (${j.id})`,
264
+ `status: ${statusColor(j.enabled)}`,
265
+ `updated: ${j.updatedAt}`,
266
+ ].join("\n"),
267
+ `Journey ${enabled ? "enabled" : "disabled"}`,
268
+ );
269
+ ctx.out.outro(`${j.id} is now ${statusColor(j.enabled)}.`);
270
+ }
271
+
272
+ async function run(ctx: CommandContext): Promise<void> {
273
+ const sub = ctx.argv[0];
274
+ // argv after the subcommand token — positionals/flags for the subcommand.
275
+ const rest = ctx.argv.slice(1);
276
+ const subCtx: CommandContext = { ...ctx, argv: rest };
277
+
278
+ try {
279
+ switch (sub) {
280
+ case "list":
281
+ await runList(subCtx);
282
+ return;
283
+ case "get": {
284
+ const id = rest.find((a) => !a.startsWith("-"));
285
+ if (rest.includes("--help") || rest.includes("-h")) {
286
+ ctx.out.log(usage);
287
+ return;
288
+ }
289
+ await runGet(subCtx, id);
290
+ return;
291
+ }
292
+ case "enable": {
293
+ if (rest.includes("--help") || rest.includes("-h")) {
294
+ ctx.out.log(usage);
295
+ return;
296
+ }
297
+ await runToggle(
298
+ subCtx,
299
+ rest.find((a) => !a.startsWith("-")),
300
+ true,
301
+ );
302
+ return;
303
+ }
304
+ case "disable": {
305
+ if (rest.includes("--help") || rest.includes("-h")) {
306
+ ctx.out.log(usage);
307
+ return;
308
+ }
309
+ await runToggle(
310
+ subCtx,
311
+ rest.find((a) => !a.startsWith("-")),
312
+ false,
313
+ );
314
+ return;
315
+ }
316
+ case undefined:
317
+ ctx.out.fail(
318
+ `journeys requires a subcommand (list|get|enable|disable). Run: hogsend journeys --help`,
319
+ );
320
+ return;
321
+ default:
322
+ ctx.out.fail(
323
+ `unknown journeys subcommand '${sub}'. Expected list|get|enable|disable.`,
324
+ );
325
+ return;
326
+ }
327
+ } catch (error) {
328
+ if (isHttpError(error)) {
329
+ if (error.status === 404) {
330
+ ctx.out.fail("journey not found");
331
+ }
332
+ ctx.out.fail(error.message);
333
+ }
334
+ throw error;
335
+ }
336
+ }
337
+
338
+ export const journeysCommand: Command = {
339
+ name: "journeys",
340
+ summary: "List, inspect, enable, and disable journeys",
341
+ usage,
342
+ run,
343
+ };
@@ -0,0 +1,80 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { parseArgs } from "node:util";
3
+ import { color } from "../lib/output.js";
4
+ import type { Command, CommandContext } from "./types.js";
5
+
6
+ const usage = `hogsend patch <package> [--cwd <dir>]
7
+
8
+ Thin wrapper over pnpm's native patch flow. Runs \`pnpm patch <package>\`, which
9
+ extracts the package into a temp dir and prints the path to edit. After editing,
10
+ commit the patch with the command pnpm prints (\`pnpm patch-commit <dir>\`).
11
+
12
+ This does NOT replace scripts/patch-check.sh (the patch re-apply contract).
13
+
14
+ Options:
15
+ --cwd <dir> Project root to run pnpm in (defaults to current directory).
16
+ -h, --help Show this help.`;
17
+
18
+ async function run(ctx: CommandContext): Promise<void> {
19
+ const { values, positionals } = parseArgs({
20
+ args: ctx.argv,
21
+ allowPositionals: true,
22
+ options: {
23
+ cwd: { type: "string" },
24
+ help: { type: "boolean", short: "h", default: false },
25
+ },
26
+ });
27
+
28
+ if (values.help) {
29
+ ctx.out.log(usage);
30
+ return;
31
+ }
32
+
33
+ const pkg = positionals[0];
34
+ if (!pkg) {
35
+ ctx.out.fail(
36
+ "patch requires a package name, e.g. hogsend patch @hogsend/engine",
37
+ );
38
+ }
39
+
40
+ const cwd = values.cwd ?? process.cwd();
41
+
42
+ // pnpm patch is interactive-ish (prints an editable dir). Stream it through
43
+ // unless --json, where we suppress chrome and report the spawn result only.
44
+ const result = spawnSync("pnpm", ["patch", pkg], {
45
+ cwd,
46
+ stdio: ctx.json ? "ignore" : "inherit",
47
+ });
48
+
49
+ if (ctx.json) {
50
+ ctx.out.json({
51
+ package: pkg,
52
+ command: `pnpm patch ${pkg}`,
53
+ status: result.status,
54
+ ok: result.status === 0,
55
+ });
56
+ if (result.status !== 0) process.exit(1);
57
+ return;
58
+ }
59
+
60
+ if (result.status !== 0) {
61
+ ctx.out.fail(`pnpm patch ${pkg} exited with code ${result.status ?? "?"}`);
62
+ }
63
+
64
+ ctx.out.note(
65
+ [
66
+ "pnpm extracted the package to a temp dir (printed above).",
67
+ "Edit the files, then commit the patch:",
68
+ "",
69
+ color.cyan("pnpm patch-commit <dir>"),
70
+ ].join("\n"),
71
+ "Next steps",
72
+ );
73
+ }
74
+
75
+ export const patchCommand: Command = {
76
+ name: "patch",
77
+ summary: "Patch a package via pnpm's native patch flow",
78
+ usage,
79
+ run,
80
+ };