@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
package/dist/bin.js CHANGED
@@ -1,15 +1,723 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin.ts
4
- import { existsSync as existsSync2, realpathSync } from "fs";
5
- import { createRequire } from "module";
6
- import { dirname, join as join2, sep as sep2 } from "path";
4
+ import { createRequire as createRequire3 } from "module";
5
+
6
+ // src/commands/contacts.ts
7
7
  import { parseArgs } from "util";
8
8
 
9
+ // src/lib/http.ts
10
+ function isHttpError(value) {
11
+ return value instanceof Error && "status" in value;
12
+ }
13
+ function makeHttpError(message, status, body) {
14
+ const err = new Error(message);
15
+ err.name = "HttpError";
16
+ err.status = status;
17
+ err.body = body;
18
+ return err;
19
+ }
20
+ function buildUrl(baseUrl, path, query) {
21
+ const url = new URL(path.startsWith("/") ? path : `/${path}`, `${baseUrl}/`);
22
+ if (query) {
23
+ for (const [key, value] of Object.entries(query)) {
24
+ if (value === void 0) continue;
25
+ url.searchParams.set(key, String(value));
26
+ }
27
+ }
28
+ return url.toString();
29
+ }
30
+ function bodyMessage(status, body) {
31
+ if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
32
+ return `${status}: ${body.error}`;
33
+ }
34
+ return `request failed with status ${status}`;
35
+ }
36
+ function createAdminClient(cfg) {
37
+ async function request(method, path, opts) {
38
+ if (opts.auth && !cfg.adminKey) {
39
+ throw makeHttpError(
40
+ "no admin key configured \u2014 pass --admin-key, or set HOGSEND_ADMIN_KEY / ADMIN_API_KEY",
41
+ 0,
42
+ void 0
43
+ );
44
+ }
45
+ const headers = { Accept: "application/json" };
46
+ if (opts.auth && cfg.adminKey) {
47
+ headers.Authorization = `Bearer ${cfg.adminKey}`;
48
+ }
49
+ if (opts.body !== void 0) {
50
+ headers["Content-Type"] = "application/json";
51
+ }
52
+ const url = buildUrl(cfg.baseUrl, path, opts.query);
53
+ let res;
54
+ try {
55
+ res = await fetch(url, {
56
+ method,
57
+ headers,
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
+ }
72
+ }
73
+ if (!res.ok) {
74
+ throw makeHttpError(bodyMessage(res.status, parsed), res.status, parsed);
75
+ }
76
+ return parsed;
77
+ }
78
+ return {
79
+ cfg,
80
+ get: (path, query, extras) => request("GET", path, { query, auth: extras?.auth ?? true }),
81
+ patch: (path, body) => request("PATCH", path, { body, auth: true }),
82
+ post: (path, body) => request("POST", path, { body, auth: true })
83
+ };
84
+ }
85
+
86
+ // src/lib/output.ts
87
+ import {
88
+ cancel,
89
+ intro as clackIntro,
90
+ note as clackNote,
91
+ outro as clackOutro,
92
+ spinner
93
+ } from "@clack/prompts";
94
+ import color from "picocolors";
95
+ function renderTable(rows, columns) {
96
+ if (rows.length === 0) return color.dim("(no rows)");
97
+ const cols = columns ?? Array.from(
98
+ rows.reduce((set, row) => {
99
+ for (const key of Object.keys(row)) set.add(key);
100
+ return set;
101
+ }, /* @__PURE__ */ new Set())
102
+ );
103
+ const cell = (value) => {
104
+ if (value === null || value === void 0) return "";
105
+ if (typeof value === "object") return JSON.stringify(value);
106
+ return String(value);
107
+ };
108
+ const widths = cols.map(
109
+ (c) => Math.max(c.length, ...rows.map((r) => cell(r[c]).length))
110
+ );
111
+ const pad = (text, width) => text + " ".repeat(width - text.length);
112
+ const header = cols.map((c, i) => color.bold(pad(c, widths[i] ?? 0))).join(" ");
113
+ const sep4 = cols.map((_, i) => "-".repeat(widths[i] ?? 0)).join(" ");
114
+ const body = rows.map((r) => cols.map((c, i) => pad(cell(r[c]), widths[i] ?? 0)).join(" ")).join("\n");
115
+ return `${header}
116
+ ${color.dim(sep4)}
117
+ ${body}`;
118
+ }
119
+ function renderKv(obj) {
120
+ const keys = Object.keys(obj);
121
+ if (keys.length === 0) return color.dim("(empty)");
122
+ const width = Math.max(...keys.map((k) => k.length));
123
+ return keys.map((k) => {
124
+ const v = obj[k];
125
+ const str = v === null || v === void 0 ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
126
+ return `${color.dim(`${k}:`.padEnd(width + 1))} ${str}`;
127
+ }).join("\n");
128
+ }
129
+ function createOutput(opts) {
130
+ const isJson = opts.json;
131
+ const interactive = !isJson && Boolean(process.stdout.isTTY);
132
+ return {
133
+ interactive,
134
+ isJson,
135
+ intro(title) {
136
+ if (!interactive) return;
137
+ clackIntro(title);
138
+ },
139
+ async step(label, fn) {
140
+ if (interactive) {
141
+ const s = spinner();
142
+ s.start(label);
143
+ try {
144
+ const result = await fn();
145
+ s.stop(`${color.green("\u2713")} ${label}`);
146
+ return result;
147
+ } catch (err) {
148
+ s.stop(`${color.red("\u2717")} ${label}`);
149
+ throw err;
150
+ }
151
+ }
152
+ if (!isJson) console.log(` ${label} ...`);
153
+ return fn();
154
+ },
155
+ note(body, title) {
156
+ if (isJson) return;
157
+ if (interactive) {
158
+ clackNote(body, title);
159
+ return;
160
+ }
161
+ if (title) console.log(`
162
+ ${title}`);
163
+ console.log(body);
164
+ },
165
+ table(rows, columns) {
166
+ if (isJson) return;
167
+ console.log(renderTable(rows, columns));
168
+ },
169
+ kv(obj, title) {
170
+ if (isJson) return;
171
+ if (title) console.log(color.bold(title));
172
+ console.log(renderKv(obj));
173
+ },
174
+ log(msg) {
175
+ if (isJson) return;
176
+ console.log(msg);
177
+ },
178
+ json(payload) {
179
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
180
+ `);
181
+ },
182
+ outro(msg) {
183
+ if (!interactive) return;
184
+ clackOutro(msg);
185
+ },
186
+ fail(message) {
187
+ if (isJson) {
188
+ process.stdout.write(`${JSON.stringify({ error: message })}
189
+ `);
190
+ } else if (interactive) {
191
+ cancel(message);
192
+ } else {
193
+ process.stderr.write(`${color.red("error")} ${message}
194
+ `);
195
+ }
196
+ process.exit(1);
197
+ }
198
+ };
199
+ }
200
+
201
+ // src/commands/contacts.ts
202
+ var usage = `hogsend contacts <subcommand> [options]
203
+
204
+ Inspect contacts via the running app's admin API (/v1/admin/contacts).
205
+
206
+ Subcommands:
207
+ list List contacts (newest activity first).
208
+ get <id> Get one contact (by id or externalId) + preferences.
209
+ timeline <id> Merged event/email/journey activity for a contact.
210
+
211
+ list options:
212
+ --search <q> Filter by email/externalId substring.
213
+ --limit <n> Page size (1-100, default 50).
214
+ --offset <n> Page offset (default 0).
215
+
216
+ timeline options:
217
+ --type <t> Restrict to one of: event | journey | email.
218
+ --limit <n> Page size (1-100, default 50).
219
+ --offset <n> Page offset (default 0).
220
+
221
+ Global options (handled by the router): --url, --admin-key, --json, -h/--help.
222
+
223
+ Examples:
224
+ hogsend contacts list --search acme@ --json
225
+ hogsend contacts get user_123
226
+ hogsend contacts timeline user_123 --type email --json`;
227
+ var badge = `${color.bgMagenta(color.black(" hogsend "))} contacts`;
228
+ async function fetchOrFail(ctx, label, fn) {
229
+ try {
230
+ return await ctx.out.step(label, fn);
231
+ } catch (err) {
232
+ if (isHttpError(err)) {
233
+ if (err.status === 404) {
234
+ ctx.out.fail(err.message || "contact not found");
235
+ }
236
+ ctx.out.fail(err.message);
237
+ }
238
+ throw err;
239
+ }
240
+ }
241
+ async function runList(ctx, argv) {
242
+ const { values } = parseArgs({
243
+ args: argv,
244
+ allowPositionals: true,
245
+ options: {
246
+ search: { type: "string" },
247
+ limit: { type: "string" },
248
+ offset: { type: "string" },
249
+ help: { type: "boolean", short: "h", default: false }
250
+ }
251
+ });
252
+ if (values.help) {
253
+ ctx.out.log(usage);
254
+ return;
255
+ }
256
+ const query = {
257
+ search: values.search,
258
+ limit: values.limit,
259
+ offset: values.offset
260
+ };
261
+ if (!ctx.json) ctx.out.intro(`${badge} list`);
262
+ const res = await fetchOrFail(
263
+ ctx,
264
+ "Fetching contacts",
265
+ () => ctx.http.get("/v1/admin/contacts", query)
266
+ );
267
+ if (ctx.json) {
268
+ ctx.out.json(res);
269
+ return;
270
+ }
271
+ ctx.out.table(
272
+ res.contacts.map((cnt) => ({
273
+ id: cnt.id,
274
+ externalId: cnt.externalId,
275
+ email: cnt.email ?? color.dim("(none)"),
276
+ lastSeenAt: cnt.lastSeenAt
277
+ })),
278
+ ["id", "externalId", "email", "lastSeenAt"]
279
+ );
280
+ ctx.out.outro(
281
+ `${res.contacts.length} of ${res.total} contact(s) \u2014 offset ${res.offset}, limit ${res.limit}`
282
+ );
283
+ }
284
+ async function runGet(ctx, argv) {
285
+ const { values, positionals } = parseArgs({
286
+ args: argv,
287
+ allowPositionals: true,
288
+ options: {
289
+ help: { type: "boolean", short: "h", default: false }
290
+ }
291
+ });
292
+ if (values.help) {
293
+ ctx.out.log(usage);
294
+ return;
295
+ }
296
+ const id = positionals[1];
297
+ if (!id) {
298
+ ctx.out.fail(
299
+ "contacts get requires an id, e.g. hogsend contacts get user_123"
300
+ );
301
+ }
302
+ if (!ctx.json) ctx.out.intro(`${badge} get`);
303
+ const res = await fetchOrFail(
304
+ ctx,
305
+ "Fetching contact",
306
+ () => ctx.http.get(`/v1/admin/contacts/${encodeURIComponent(id)}`)
307
+ );
308
+ if (ctx.json) {
309
+ ctx.out.json(res);
310
+ return;
311
+ }
312
+ const { contact, preferences } = res;
313
+ ctx.out.kv(
314
+ {
315
+ id: contact.id,
316
+ externalId: contact.externalId,
317
+ email: contact.email ?? color.dim("(none)"),
318
+ firstSeenAt: contact.firstSeenAt,
319
+ lastSeenAt: contact.lastSeenAt,
320
+ properties: contact.properties
321
+ },
322
+ "Contact"
323
+ );
324
+ if (preferences) {
325
+ ctx.out.kv(
326
+ {
327
+ unsubscribedAll: preferences.unsubscribedAll,
328
+ suppressed: preferences.suppressed,
329
+ bounceCount: preferences.bounceCount,
330
+ categories: preferences.categories
331
+ },
332
+ "Preferences"
333
+ );
334
+ } else {
335
+ ctx.out.log(color.dim("No email preferences on record."));
336
+ }
337
+ ctx.out.outro(`Contact ${color.cyan(contact.externalId)}`);
338
+ }
339
+ async function runTimeline(ctx, argv) {
340
+ const { values, positionals } = parseArgs({
341
+ args: argv,
342
+ allowPositionals: true,
343
+ options: {
344
+ type: { type: "string" },
345
+ limit: { type: "string" },
346
+ offset: { type: "string" },
347
+ help: { type: "boolean", short: "h", default: false }
348
+ }
349
+ });
350
+ if (values.help) {
351
+ ctx.out.log(usage);
352
+ return;
353
+ }
354
+ const id = positionals[1];
355
+ if (!id) {
356
+ ctx.out.fail(
357
+ "contacts timeline requires an id, e.g. hogsend contacts timeline user_123"
358
+ );
359
+ }
360
+ if (values.type && !["event", "journey", "email"].includes(values.type)) {
361
+ ctx.out.fail("--type must be one of: event, journey, email");
362
+ }
363
+ const query = {
364
+ type: values.type,
365
+ limit: values.limit,
366
+ offset: values.offset
367
+ };
368
+ if (!ctx.json) ctx.out.intro(`${badge} timeline`);
369
+ const res = await fetchOrFail(
370
+ ctx,
371
+ "Fetching timeline",
372
+ () => ctx.http.get(
373
+ `/v1/admin/contacts/${encodeURIComponent(id)}/timeline`,
374
+ query
375
+ )
376
+ );
377
+ if (ctx.json) {
378
+ ctx.out.json(res);
379
+ return;
380
+ }
381
+ ctx.out.table(
382
+ res.timeline.map((entry) => ({
383
+ timestamp: entry.timestamp,
384
+ type: entry.type,
385
+ summary: summarizeTimelineEntry(entry)
386
+ })),
387
+ ["timestamp", "type", "summary"]
388
+ );
389
+ ctx.out.outro(
390
+ `${res.timeline.length} of ${res.total} entry(s) \u2014 offset ${res.offset}, limit ${res.limit}`
391
+ );
392
+ }
393
+ function summarizeTimelineEntry(entry) {
394
+ const d = entry.data;
395
+ if (entry.type === "event") {
396
+ return String(d.event ?? "");
397
+ }
398
+ if (entry.type === "journey") {
399
+ return `${String(d.journeyId ?? "")} (${String(d.status ?? "")})`;
400
+ }
401
+ const subject = d.subject ? String(d.subject) : String(d.templateKey ?? "");
402
+ return `${subject} [${String(d.status ?? "")}]`;
403
+ }
404
+ async function run(ctx) {
405
+ const sub = ctx.argv[0];
406
+ switch (sub) {
407
+ case "list":
408
+ return runList(ctx, ctx.argv);
409
+ case "get":
410
+ return runGet(ctx, ctx.argv);
411
+ case "timeline":
412
+ return runTimeline(ctx, ctx.argv);
413
+ case void 0:
414
+ ctx.out.fail(
415
+ "contacts requires a subcommand: list, get, or timeline (see hogsend contacts --help)"
416
+ );
417
+ break;
418
+ default:
419
+ ctx.out.fail(
420
+ `unknown contacts subcommand "${sub}" \u2014 expected list, get, or timeline`
421
+ );
422
+ }
423
+ }
424
+ var contactsCommand = {
425
+ name: "contacts",
426
+ summary: "List, inspect, and trace contact activity",
427
+ usage,
428
+ run
429
+ };
430
+
431
+ // src/commands/doctor.ts
432
+ import { parseArgs as parseArgs2 } from "util";
433
+
434
+ // src/lib/skills.ts
435
+ import {
436
+ cpSync,
437
+ existsSync,
438
+ mkdirSync,
439
+ readdirSync,
440
+ readFileSync,
441
+ statSync,
442
+ writeFileSync
443
+ } from "fs";
444
+ import { createRequire } from "module";
445
+ import { join } from "path";
446
+ import { fileURLToPath } from "url";
447
+ function bundledSkillsDir() {
448
+ return fileURLToPath(new URL("../skills", import.meta.url));
449
+ }
450
+ function installDir(cwd) {
451
+ return join(cwd, ".claude", "skills");
452
+ }
453
+ function stampPath(cwd) {
454
+ return join(cwd, ".claude", ".hogsend-skills.json");
455
+ }
456
+ function cliVersion() {
457
+ try {
458
+ const require2 = createRequire(import.meta.url);
459
+ const pkg = require2("../package.json");
460
+ return pkg.version ?? "0.0.0";
461
+ } catch {
462
+ return "0.0.0";
463
+ }
464
+ }
465
+ function readFileSyncSafe(path) {
466
+ try {
467
+ return readFileSync(path, "utf8");
468
+ } catch {
469
+ return "";
470
+ }
471
+ }
472
+ function readFrontmatterField(skillDir, field) {
473
+ const skillFile = join(skillDir, "SKILL.md");
474
+ if (!existsSync(skillFile)) return "";
475
+ const raw = readFileSyncSafe(skillFile);
476
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
477
+ if (!fmMatch) return "";
478
+ const block = fmMatch[1] ?? "";
479
+ for (const line of block.split("\n")) {
480
+ const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
481
+ if (m && m[1] === field) {
482
+ return (m[2] ?? "").replace(/^["']|["']$/g, "").trim();
483
+ }
484
+ }
485
+ return "";
486
+ }
487
+ function listBundledSkills(cwd) {
488
+ const dir = bundledSkillsDir();
489
+ if (!existsSync(dir)) return [];
490
+ const target = installDir(cwd);
491
+ const entries = readdirSync(dir).filter((name) => {
492
+ const full = join(dir, name);
493
+ return statSync(full).isDirectory() && existsSync(join(full, "SKILL.md"));
494
+ });
495
+ return entries.sort().map((name) => ({
496
+ name,
497
+ description: readFrontmatterField(join(dir, name), "description"),
498
+ installed: existsSync(join(target, name))
499
+ }));
500
+ }
501
+ function copySkill(name, cwd, force) {
502
+ const src = join(bundledSkillsDir(), name);
503
+ const dest = join(installDir(cwd), name);
504
+ const exists = existsSync(dest);
505
+ if (exists && !force) {
506
+ return { name, installed: false, skipped: true, path: dest };
507
+ }
508
+ mkdirSync(installDir(cwd), { recursive: true });
509
+ cpSync(src, dest, { recursive: true, force: true });
510
+ return { name, installed: true, skipped: false, path: dest };
511
+ }
512
+ function writeSkillsStamp(cwd, skills) {
513
+ const stamp = {
514
+ cliVersion: cliVersion(),
515
+ skills: [...skills].sort(),
516
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
517
+ };
518
+ mkdirSync(join(cwd, ".claude"), { recursive: true });
519
+ writeFileSync(stampPath(cwd), `${JSON.stringify(stamp, null, 2)}
520
+ `);
521
+ }
522
+ function readSkillsStamp(cwd) {
523
+ try {
524
+ const parsed = JSON.parse(readFileSync(stampPath(cwd), "utf8"));
525
+ return parsed && typeof parsed.cliVersion === "string" ? parsed : null;
526
+ } catch {
527
+ return null;
528
+ }
529
+ }
530
+ function compareVersions(a, b) {
531
+ const parse = (v) => (v.split("-")[0] ?? "").split(".").map((n) => Number.parseInt(n, 10) || 0);
532
+ const pa = parse(a);
533
+ const pb = parse(b);
534
+ for (let i = 0; i < 3; i++) {
535
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
536
+ if (d !== 0) return d < 0 ? -1 : 1;
537
+ }
538
+ return 0;
539
+ }
540
+ function skillsStaleness(cwd) {
541
+ const stamp = readSkillsStamp(cwd);
542
+ if (!stamp) return null;
543
+ const current = cliVersion();
544
+ return {
545
+ stale: compareVersions(stamp.cliVersion, current) < 0,
546
+ installed: stamp.cliVersion,
547
+ current
548
+ };
549
+ }
550
+
551
+ // src/commands/doctor.ts
552
+ function skillsNudge(ctx) {
553
+ const verdict = skillsStaleness(process.cwd());
554
+ if (!verdict?.stale || ctx.json) return;
555
+ ctx.out.note(
556
+ [
557
+ `Vendored Claude skills are from v${verdict.installed}; this CLI is v${verdict.current}.`,
558
+ "",
559
+ `Refresh: ${color.cyan("hogsend upgrade")} ${color.dim("(deps + skills)")} or ${color.cyan("hogsend skills add --all --force")}.`
560
+ ].join("\n"),
561
+ "Skills out of date"
562
+ );
563
+ }
564
+ var usage2 = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
565
+
566
+ Probe a running Hogsend instance via GET /v1/health and report its health:
567
+ component status (database, redis), two-track schema state (engine + client),
568
+ and an overall verdict.
569
+
570
+ The health route is unauthenticated, so doctor works without an admin key.
571
+
572
+ Verdict:
573
+ ok service healthy, all components up, schema in sync
574
+ degraded reachable but a component (database/redis) is down
575
+ migration_pending reachable but a schema track is behind (pending migrations)
576
+ unreachable the instance could not be reached at all
577
+
578
+ Exit code: 0 when ok, 1 when unreachable / degraded / migration_pending.
579
+
580
+ Options:
581
+ --url <baseUrl> Target instance (default HOGSEND_API_URL / .env / :3002).
582
+ --admin-key <key> Unused by doctor (health is unauthenticated).
583
+ --json Emit machine-readable JSON only.
584
+ -h, --help Show this help.`;
585
+ function toVerdict(status) {
586
+ switch (status) {
587
+ case "healthy":
588
+ return "ok";
589
+ case "degraded":
590
+ return "degraded";
591
+ case "migration_pending":
592
+ return "migration_pending";
593
+ }
594
+ }
595
+ function componentSymbol(status) {
596
+ return status === "up" ? color.green("up") : color.red("down");
597
+ }
598
+ function trackLine(name, track) {
599
+ const sync = track.inSync ? color.green("in sync") : color.yellow(
600
+ `behind (${track.pending.length} pending: ${track.pending.length > 0 ? track.pending.join(", ") : "n/a"})`
601
+ );
602
+ const applied = track.applied ?? color.dim("none");
603
+ const required = track.required ?? color.dim("none");
604
+ return `${color.bold(name.padEnd(7))} applied ${applied} -> required ${required} ${sync}`;
605
+ }
606
+ async function run2(ctx) {
607
+ const { values } = parseArgs2({
608
+ args: ctx.argv,
609
+ allowPositionals: true,
610
+ options: {
611
+ help: { type: "boolean", short: "h", default: false }
612
+ },
613
+ // doctor takes no extra flags of its own; tolerate stray tokens.
614
+ strict: false
615
+ });
616
+ if (values.help) {
617
+ ctx.out.log(usage2);
618
+ return;
619
+ }
620
+ const { baseUrl } = ctx.http.cfg;
621
+ let health = null;
622
+ let reachError = null;
623
+ try {
624
+ health = await ctx.out.step(
625
+ `GET ${baseUrl}/v1/health`,
626
+ () => ctx.http.get("/v1/health", void 0, { auth: false })
627
+ );
628
+ } catch (error) {
629
+ if (isHttpError(error) && error.status === 0) {
630
+ reachError = error.message;
631
+ } else if (isHttpError(error)) {
632
+ reachError = error.message;
633
+ } else {
634
+ throw error;
635
+ }
636
+ }
637
+ if (!health) {
638
+ const verdict2 = "unreachable";
639
+ if (ctx.json) {
640
+ ctx.out.json({
641
+ ok: false,
642
+ verdict: verdict2,
643
+ baseUrl,
644
+ error: reachError ?? "unreachable"
645
+ });
646
+ process.exit(1);
647
+ }
648
+ ctx.out.note(
649
+ [
650
+ `${color.red("\u25CF")} ${color.bold("unreachable")}`,
651
+ "",
652
+ reachError ?? `could not reach ${baseUrl}`,
653
+ "",
654
+ color.dim("Is the instance running? Check --url / HOGSEND_API_URL.")
655
+ ].join("\n"),
656
+ "Doctor"
657
+ );
658
+ ctx.out.outro(color.red("doctor: unreachable"));
659
+ process.exit(1);
660
+ }
661
+ const verdict = toVerdict(health.status);
662
+ const ok = verdict === "ok";
663
+ if (ctx.json) {
664
+ ctx.out.json({
665
+ ok,
666
+ verdict,
667
+ baseUrl,
668
+ version: health.version,
669
+ uptime: health.uptime,
670
+ timestamp: health.timestamp,
671
+ components: health.components,
672
+ schema: health.schema,
673
+ skills: skillsStaleness(process.cwd()) ?? void 0
674
+ });
675
+ if (!ok) process.exit(1);
676
+ return;
677
+ }
678
+ const badge3 = `${color.bgMagenta(color.black(" hogsend "))} doctor`;
679
+ ctx.out.intro(badge3);
680
+ const verdictColor = verdict === "ok" ? color.green : verdict === "degraded" ? color.red : color.yellow;
681
+ const lines = [
682
+ `${verdictColor("\u25CF")} ${color.bold(verdict)}`,
683
+ color.dim(
684
+ `${baseUrl} v${health.version} up ${Math.round(health.uptime)}s`
685
+ ),
686
+ "",
687
+ color.bold("Components"),
688
+ ` database ${componentSymbol(health.components.database.status)}${health.components.database.latencyMs !== void 0 ? color.dim(` ${health.components.database.latencyMs}ms`) : ""}`,
689
+ ` redis ${componentSymbol(health.components.redis.status)}${health.components.redis.latencyMs !== void 0 ? color.dim(` ${health.components.redis.latencyMs}ms`) : ""}`,
690
+ "",
691
+ color.bold("Schema"),
692
+ ` ${trackLine("engine", health.schema.engine)}`,
693
+ ` ${trackLine("client", health.schema.client)}`
694
+ ];
695
+ ctx.out.note(lines.join("\n"), "Doctor");
696
+ skillsNudge(ctx);
697
+ if (ok) {
698
+ ctx.out.outro(color.green("doctor: ok"));
699
+ return;
700
+ }
701
+ ctx.out.outro(verdictColor(`doctor: ${verdict}`));
702
+ process.exit(1);
703
+ }
704
+ var doctorCommand = {
705
+ name: "doctor",
706
+ summary: "Probe a running instance's health (GET /v1/health)",
707
+ usage: usage2,
708
+ run: run2
709
+ };
710
+
711
+ // src/commands/eject.ts
712
+ import { existsSync as existsSync3, realpathSync } from "fs";
713
+ import { createRequire as createRequire2 } from "module";
714
+ import { dirname, join as join3, sep as sep2 } from "path";
715
+ import { parseArgs as parseArgs3 } from "util";
716
+
9
717
  // src/eject.ts
10
- import { existsSync } from "fs";
718
+ import { existsSync as existsSync2 } from "fs";
11
719
  import { cp, readFile, rm, stat, writeFile } from "fs/promises";
12
- import { basename, join, relative, sep } from "path";
720
+ import { basename, join as join2, relative, sep } from "path";
13
721
  var EjectError = class extends Error {
14
722
  constructor(message) {
15
723
  super(message);
@@ -34,8 +742,8 @@ async function writePackageJson(file, value) {
34
742
  async function eject(opts) {
35
743
  const { pkg, consumerRoot, sourceDir, force = false } = opts;
36
744
  const vendorName = basename(pkg);
37
- const vendorPath = join(consumerRoot, "vendor", vendorName);
38
- const consumerPkgPath = join(consumerRoot, "package.json");
745
+ const vendorPath = join2(consumerRoot, "vendor", vendorName);
746
+ const consumerPkgPath = join2(consumerRoot, "package.json");
39
747
  const consumerPkg = await readPackageJson(consumerPkgPath);
40
748
  let depMap;
41
749
  let depSpecBefore;
@@ -51,7 +759,7 @@ async function eject(opts) {
51
759
  `${pkg} is not a dependency of the consumer package.json`
52
760
  );
53
761
  }
54
- if (existsSync(vendorPath)) {
762
+ if (existsSync2(vendorPath)) {
55
763
  if (!force) {
56
764
  throw new EjectError(
57
765
  `vendor/${vendorName} already exists; pass --force to overwrite`
@@ -79,7 +787,7 @@ async function eject(opts) {
79
787
  }
80
788
  });
81
789
  copiedFiles = await countFiles(vendorPath);
82
- const vendoredPkgPath = join(vendorPath, "package.json");
790
+ const vendoredPkgPath = join2(vendorPath, "package.json");
83
791
  const vendoredPkg = await readPackageJson(vendoredPkgPath);
84
792
  if (vendoredPkg.private === true) {
85
793
  delete vendoredPkg.private;
@@ -102,7 +810,7 @@ async function countFiles(dir) {
102
810
  let count = 0;
103
811
  const entries = await readdir(dir, { withFileTypes: true });
104
812
  for (const entry of entries) {
105
- const full = join(dir, entry.name);
813
+ const full = join2(dir, entry.name);
106
814
  if (entry.isDirectory()) {
107
815
  count += await countFiles(full);
108
816
  } else if (entry.isFile()) {
@@ -117,55 +825,38 @@ async function countFiles(dir) {
117
825
  return count;
118
826
  }
119
827
 
120
- // src/bin.ts
121
- var USAGE = `hogsend \u2014 Hogsend project CLI
122
-
123
- Usage:
124
- hogsend eject <package> [--force] [--cwd <dir>]
828
+ // src/commands/eject.ts
829
+ var usage3 = `hogsend eject <package> [--force] [--cwd <dir>]
125
830
 
126
- Commands:
127
- eject <package> Copy a @hogsend/* package's source into vendor/<name> and
128
- rewrite the consumer dependency to file:./vendor/<name>.
129
- Every other dependency keeps upgrading via pnpm up.
831
+ Copy a @hogsend/* package's source into vendor/<name> and rewrite the consumer
832
+ dependency to file:./vendor/<name>. Every other dependency keeps upgrading.
130
833
 
131
834
  Options:
132
- --force Overwrite an existing vendor/<name>.
133
- --cwd <dir> Consumer repo root (defaults to the current directory).
134
- -h, --help Show this help.
835
+ --force Overwrite an existing vendor/<name>.
836
+ --cwd <dir> Consumer repo root (defaults to the current directory).
837
+ -h, --help Show this help.
135
838
 
136
839
  After ejecting, run: pnpm install`;
137
- var RED = "\x1B[31m";
138
- var RESET = "\x1B[0m";
139
- function fail(message) {
140
- process.stderr.write(`${RED}error${RESET} ${message}
141
- `);
142
- process.exit(1);
143
- }
144
840
  function resolveSourceDir(pkg, consumerRoot) {
145
- const direct = join2(consumerRoot, "node_modules", pkg, "package.json");
146
- if (existsSync2(direct)) {
841
+ const direct = join3(consumerRoot, "node_modules", pkg, "package.json");
842
+ if (existsSync3(direct)) {
147
843
  return dirname(realpathSync(direct));
148
844
  }
149
- const require2 = createRequire(`${consumerRoot}${sep2}`);
845
+ const require2 = createRequire2(`${consumerRoot}${sep2}`);
150
846
  try {
151
847
  const entry = require2.resolve(pkg);
152
848
  let dir = dirname(entry);
153
849
  while (dir !== dirname(dir)) {
154
- const candidate = join2(dir, "package.json");
155
- if (existsSync2(candidate)) {
156
- return dir;
157
- }
850
+ if (existsSync3(join3(dir, "package.json"))) return dir;
158
851
  dir = dirname(dir);
159
852
  }
160
853
  } catch {
161
854
  }
162
- fail(
163
- `cannot resolve ${pkg} from ${consumerRoot}. Is it installed? Run pnpm install first.`
164
- );
855
+ return null;
165
856
  }
166
- async function runEject(args) {
167
- const { values, positionals } = parseArgs({
168
- args,
857
+ async function run3(ctx) {
858
+ const { values, positionals } = parseArgs3({
859
+ args: ctx.argv,
169
860
  allowPositionals: true,
170
861
  options: {
171
862
  force: { type: "boolean", default: false },
@@ -174,58 +865,1530 @@ async function runEject(args) {
174
865
  }
175
866
  });
176
867
  if (values.help) {
177
- process.stdout.write(`${USAGE}
178
- `);
868
+ ctx.out.log(usage3);
179
869
  return;
180
870
  }
181
871
  const pkg = positionals[0];
182
872
  if (!pkg) {
183
- fail("eject requires a package name, e.g. hogsend eject @hogsend/engine");
873
+ ctx.out.fail(
874
+ "eject requires a package name, e.g. hogsend eject @hogsend/engine"
875
+ );
184
876
  }
185
877
  const consumerRoot = values.cwd ?? process.cwd();
186
878
  const sourceDir = resolveSourceDir(pkg, consumerRoot);
879
+ if (!sourceDir) {
880
+ ctx.out.fail(
881
+ `cannot resolve ${pkg} from ${consumerRoot}. Is it installed? Run pnpm install first.`
882
+ );
883
+ }
187
884
  try {
188
- const result = await eject({
189
- pkg,
190
- consumerRoot,
191
- sourceDir,
192
- force: values.force
193
- });
194
- process.stdout.write(
195
- `Ejected ${result.pkg}
196
- copied ${result.copiedFiles} files -> ${result.vendorPath}
197
- dependency ${result.depSpecBefore} -> ${result.depSpecAfter}
198
-
199
- Now run: ${result.followUp}
200
- `
885
+ const result = await ctx.out.step(
886
+ `Ejecting ${pkg}`,
887
+ () => eject({ pkg, consumerRoot, sourceDir, force: values.force })
888
+ );
889
+ if (ctx.json) {
890
+ ctx.out.json(result);
891
+ return;
892
+ }
893
+ ctx.out.note(
894
+ [
895
+ `copied ${result.copiedFiles} files -> ${result.vendorPath}`,
896
+ `dependency ${result.depSpecBefore} -> ${color.cyan(result.depSpecAfter)}`,
897
+ "",
898
+ `Now run: ${color.cyan(result.followUp)}`
899
+ ].join("\n"),
900
+ `Ejected ${result.pkg}`
201
901
  );
202
902
  } catch (error) {
203
903
  if (error instanceof EjectError) {
204
- fail(error.message);
904
+ ctx.out.fail(error.message);
205
905
  }
206
906
  throw error;
207
907
  }
208
908
  }
209
- async function main() {
210
- const [command, ...rest] = process.argv.slice(2);
211
- if (!command || command === "--help" || command === "-h") {
212
- process.stdout.write(`${USAGE}
213
- `);
909
+ var ejectCommand = {
910
+ name: "eject",
911
+ summary: "Vendor a @hogsend/* package into vendor/<name>",
912
+ usage: usage3,
913
+ run: run3
914
+ };
915
+
916
+ // src/commands/events.ts
917
+ import { parseArgs as parseArgs4 } from "util";
918
+ var usage4 = `hogsend events <userId> [options]
919
+
920
+ Stream the event history for a single user, newest first. Wraps
921
+ GET /v1/admin/events?userId=<userId>.
922
+
923
+ Arguments:
924
+ <userId> The user (distinct) id to fetch events for. Required.
925
+
926
+ Options:
927
+ --event <name> Filter to a single event name.
928
+ --from <iso> Only events at/after this ISO-8601 timestamp.
929
+ --to <iso> Only events at/before this ISO-8601 timestamp.
930
+ --limit <n> Max events to return (1-100, default 50).
931
+ --offset <n> Pagination offset (default 0).
932
+ --json Emit machine-readable JSON only.
933
+ -h, --help Show this help.
934
+
935
+ Examples:
936
+ hogsend events user_123
937
+ hogsend events user_123 --event signup --limit 10
938
+ hogsend events user_123 --from 2026-01-01T00:00:00Z --json`;
939
+ async function run4(ctx) {
940
+ const { values, positionals } = parseArgs4({
941
+ args: ctx.argv,
942
+ allowPositionals: true,
943
+ options: {
944
+ event: { type: "string" },
945
+ from: { type: "string" },
946
+ to: { type: "string" },
947
+ limit: { type: "string" },
948
+ offset: { type: "string" },
949
+ help: { type: "boolean", short: "h", default: false }
950
+ }
951
+ });
952
+ if (values.help) {
953
+ ctx.out.log(usage4);
214
954
  return;
215
955
  }
216
- switch (command) {
217
- case "eject":
218
- await runEject(rest);
219
- break;
220
- default:
221
- fail(`unknown command "${command}"
222
-
223
- ${USAGE}`);
956
+ const userId = positionals[0];
957
+ if (!userId) {
958
+ ctx.out.fail("events requires a userId, e.g. hogsend events user_123");
959
+ }
960
+ const limit = parseNumber(values.limit, "limit", ctx);
961
+ const offset = parseNumber(values.offset, "offset", ctx);
962
+ const query = {
963
+ userId,
964
+ event: values.event,
965
+ from: values.from,
966
+ to: values.to,
967
+ limit,
968
+ offset
969
+ };
970
+ let data;
971
+ try {
972
+ data = await ctx.out.step(
973
+ `Fetching events for ${userId}`,
974
+ () => ctx.http.get("/v1/admin/events", query)
975
+ );
976
+ } catch (error) {
977
+ if (isHttpError(error)) {
978
+ ctx.out.fail(error.message);
979
+ }
980
+ throw error;
981
+ }
982
+ if (ctx.json) {
983
+ ctx.out.json(data);
984
+ return;
985
+ }
986
+ ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} events`);
987
+ if (data.events.length === 0) {
988
+ ctx.out.note(
989
+ `No events found for ${color.cyan(userId)}.`,
990
+ "Empty event stream"
991
+ );
992
+ ctx.out.outro(color.dim("Nothing to show."));
993
+ return;
994
+ }
995
+ const rows = data.events.map((e) => ({
996
+ occurredAt: e.occurredAt,
997
+ event: e.event,
998
+ properties: summarizeProps(e.properties),
999
+ id: e.id
1000
+ }));
1001
+ ctx.out.table(rows, ["occurredAt", "event", "properties", "id"]);
1002
+ const shown = data.events.length;
1003
+ const through = data.offset + shown;
1004
+ ctx.out.outro(
1005
+ `${color.green(String(shown))} event${shown === 1 ? "" : "s"} ` + color.dim(`(${data.offset + 1}-${through} of ${data.total})`)
1006
+ );
1007
+ }
1008
+ function parseNumber(raw, name, ctx) {
1009
+ if (raw === void 0) return void 0;
1010
+ const n = Number(raw);
1011
+ if (!Number.isFinite(n)) {
1012
+ ctx.out.fail(`--${name} must be a number, got "${raw}"`);
224
1013
  }
1014
+ return n;
225
1015
  }
226
- main().catch((error) => {
227
- process.stderr.write(`${RED}error${RESET} ${String(error)}
228
- `);
1016
+ function summarizeProps(props) {
1017
+ if (!props) return "";
1018
+ const keys = Object.keys(props);
1019
+ if (keys.length === 0) return "";
1020
+ const preview = JSON.stringify(props);
1021
+ return preview.length > 60 ? `${preview.slice(0, 57)}...` : preview;
1022
+ }
1023
+ var eventsCommand = {
1024
+ name: "events",
1025
+ summary: "Stream a single user's event history",
1026
+ usage: usage4,
1027
+ run: run4
1028
+ };
1029
+
1030
+ // src/commands/journeys.ts
1031
+ import { parseArgs as parseArgs5 } from "util";
1032
+ var usage5 = `hogsend journeys <subcommand> [options]
1033
+
1034
+ Inspect and toggle journeys via the admin API (/v1/admin/journeys).
1035
+
1036
+ Subcommands:
1037
+ list List journeys with status, trigger, and state counts.
1038
+ get <id> Show one journey: trigger, exitOn, counts, recent states.
1039
+ enable <id> Enable a journey (PATCH { enabled: true }).
1040
+ disable <id> Disable a journey (PATCH { enabled: false }).
1041
+
1042
+ Options:
1043
+ list:
1044
+ --enabled <true|false> Filter by enabled state.
1045
+ --limit <n> Page size (1-100, default 50).
1046
+ --offset <n> Page offset (default 0).
1047
+ --json Emit machine-readable JSON only.
1048
+ -h, --help Show this help.
1049
+
1050
+ Examples:
1051
+ hogsend journeys list --enabled true
1052
+ hogsend journeys get activation-welcome --json
1053
+ hogsend journeys disable churn-prevention`;
1054
+ function badge2() {
1055
+ return `${color.bgMagenta(color.black(" hogsend "))} journeys`;
1056
+ }
1057
+ function statusColor(enabled) {
1058
+ return enabled ? color.green("enabled") : color.yellow("disabled");
1059
+ }
1060
+ async function runList2(ctx) {
1061
+ const { values } = parseArgs5({
1062
+ args: ctx.argv,
1063
+ allowPositionals: true,
1064
+ options: {
1065
+ enabled: { type: "string" },
1066
+ limit: { type: "string" },
1067
+ offset: { type: "string" },
1068
+ help: { type: "boolean", short: "h", default: false }
1069
+ }
1070
+ });
1071
+ if (values.help) {
1072
+ ctx.out.log(usage5);
1073
+ return;
1074
+ }
1075
+ if (values.enabled !== void 0 && !["true", "false"].includes(values.enabled)) {
1076
+ ctx.out.fail("--enabled must be 'true' or 'false'");
1077
+ }
1078
+ const query = {
1079
+ enabled: values.enabled,
1080
+ limit: values.limit,
1081
+ offset: values.offset
1082
+ };
1083
+ if (!ctx.json) ctx.out.intro(badge2());
1084
+ const data = await ctx.out.step(
1085
+ "Fetching journeys",
1086
+ () => ctx.http.get("/v1/admin/journeys", query)
1087
+ );
1088
+ if (ctx.json) {
1089
+ ctx.out.json(data);
1090
+ return;
1091
+ }
1092
+ if (data.journeys.length === 0) {
1093
+ ctx.out.note("No journeys matched.", "Journeys");
1094
+ } else {
1095
+ ctx.out.table(
1096
+ data.journeys.map((j) => ({
1097
+ id: j.id,
1098
+ name: j.name,
1099
+ status: statusColor(j.enabled),
1100
+ trigger: j.trigger.event,
1101
+ active: j.counts.active,
1102
+ waiting: j.counts.waiting,
1103
+ completed: j.counts.completed,
1104
+ failed: j.counts.failed
1105
+ })),
1106
+ [
1107
+ "id",
1108
+ "name",
1109
+ "status",
1110
+ "trigger",
1111
+ "active",
1112
+ "waiting",
1113
+ "completed",
1114
+ "failed"
1115
+ ]
1116
+ );
1117
+ }
1118
+ ctx.out.outro(
1119
+ `${data.journeys.length} of ${data.total} journey(s) \u2014 offset ${data.offset}, limit ${data.limit}`
1120
+ );
1121
+ }
1122
+ async function runGet2(ctx, id) {
1123
+ if (!id) {
1124
+ ctx.out.fail(
1125
+ "journeys get requires a journey id, e.g. hogsend journeys get activation-welcome"
1126
+ );
1127
+ }
1128
+ if (!ctx.json) ctx.out.intro(badge2());
1129
+ const data = await ctx.out.step(
1130
+ `Fetching journey ${id}`,
1131
+ () => ctx.http.get(
1132
+ `/v1/admin/journeys/${encodeURIComponent(id)}`
1133
+ )
1134
+ );
1135
+ if (ctx.json) {
1136
+ ctx.out.json(data);
1137
+ return;
1138
+ }
1139
+ const j = data.journey;
1140
+ ctx.out.kv(
1141
+ {
1142
+ id: j.id,
1143
+ name: j.name,
1144
+ description: j.description ?? "",
1145
+ status: statusColor(j.enabled),
1146
+ trigger: j.trigger.event,
1147
+ entryLimit: j.entryLimit,
1148
+ exitOn: j.exitOn?.map((e) => e.event).join(", ") ?? "(none)"
1149
+ },
1150
+ "Journey"
1151
+ );
1152
+ ctx.out.kv(
1153
+ {
1154
+ active: j.counts.active,
1155
+ waiting: j.counts.waiting,
1156
+ completed: j.counts.completed,
1157
+ failed: j.counts.failed,
1158
+ exited: j.counts.exited
1159
+ },
1160
+ "Counts"
1161
+ );
1162
+ if (j.recentStates.length === 0) {
1163
+ ctx.out.note("No recent journey instances.", "Recent states");
1164
+ } else {
1165
+ ctx.out.table(
1166
+ j.recentStates.map((s) => ({
1167
+ userId: s.userId,
1168
+ email: s.userEmail,
1169
+ status: s.status,
1170
+ node: s.currentNodeId,
1171
+ updatedAt: s.updatedAt
1172
+ })),
1173
+ ["userId", "email", "status", "node", "updatedAt"]
1174
+ );
1175
+ }
1176
+ ctx.out.outro(`Journey ${j.id} is ${j.enabled ? "enabled" : "disabled"}.`);
1177
+ }
1178
+ async function runToggle(ctx, id, enabled) {
1179
+ const verb = enabled ? "enable" : "disable";
1180
+ if (!id) {
1181
+ ctx.out.fail(
1182
+ `journeys ${verb} requires a journey id, e.g. hogsend journeys ${verb} activation-welcome`
1183
+ );
1184
+ }
1185
+ if (!ctx.json) ctx.out.intro(badge2());
1186
+ const data = await ctx.out.step(
1187
+ `${enabled ? "Enabling" : "Disabling"} ${id}`,
1188
+ () => ctx.http.patch(
1189
+ `/v1/admin/journeys/${encodeURIComponent(id)}`,
1190
+ { enabled }
1191
+ )
1192
+ );
1193
+ if (ctx.json) {
1194
+ ctx.out.json(data);
1195
+ return;
1196
+ }
1197
+ const j = data.journey;
1198
+ ctx.out.note(
1199
+ [
1200
+ `${color.bold(j.name)} (${j.id})`,
1201
+ `status: ${statusColor(j.enabled)}`,
1202
+ `updated: ${j.updatedAt}`
1203
+ ].join("\n"),
1204
+ `Journey ${enabled ? "enabled" : "disabled"}`
1205
+ );
1206
+ ctx.out.outro(`${j.id} is now ${statusColor(j.enabled)}.`);
1207
+ }
1208
+ async function run5(ctx) {
1209
+ const sub = ctx.argv[0];
1210
+ const rest = ctx.argv.slice(1);
1211
+ const subCtx = { ...ctx, argv: rest };
1212
+ try {
1213
+ switch (sub) {
1214
+ case "list":
1215
+ await runList2(subCtx);
1216
+ return;
1217
+ case "get": {
1218
+ const id = rest.find((a) => !a.startsWith("-"));
1219
+ if (rest.includes("--help") || rest.includes("-h")) {
1220
+ ctx.out.log(usage5);
1221
+ return;
1222
+ }
1223
+ await runGet2(subCtx, id);
1224
+ return;
1225
+ }
1226
+ case "enable": {
1227
+ if (rest.includes("--help") || rest.includes("-h")) {
1228
+ ctx.out.log(usage5);
1229
+ return;
1230
+ }
1231
+ await runToggle(
1232
+ subCtx,
1233
+ rest.find((a) => !a.startsWith("-")),
1234
+ true
1235
+ );
1236
+ return;
1237
+ }
1238
+ case "disable": {
1239
+ if (rest.includes("--help") || rest.includes("-h")) {
1240
+ ctx.out.log(usage5);
1241
+ return;
1242
+ }
1243
+ await runToggle(
1244
+ subCtx,
1245
+ rest.find((a) => !a.startsWith("-")),
1246
+ false
1247
+ );
1248
+ return;
1249
+ }
1250
+ case void 0:
1251
+ ctx.out.fail(
1252
+ `journeys requires a subcommand (list|get|enable|disable). Run: hogsend journeys --help`
1253
+ );
1254
+ return;
1255
+ default:
1256
+ ctx.out.fail(
1257
+ `unknown journeys subcommand '${sub}'. Expected list|get|enable|disable.`
1258
+ );
1259
+ return;
1260
+ }
1261
+ } catch (error) {
1262
+ if (isHttpError(error)) {
1263
+ if (error.status === 404) {
1264
+ ctx.out.fail("journey not found");
1265
+ }
1266
+ ctx.out.fail(error.message);
1267
+ }
1268
+ throw error;
1269
+ }
1270
+ }
1271
+ var journeysCommand = {
1272
+ name: "journeys",
1273
+ summary: "List, inspect, enable, and disable journeys",
1274
+ usage: usage5,
1275
+ run: run5
1276
+ };
1277
+
1278
+ // src/commands/patch.ts
1279
+ import { spawnSync } from "child_process";
1280
+ import { parseArgs as parseArgs6 } from "util";
1281
+ var usage6 = `hogsend patch <package> [--cwd <dir>]
1282
+
1283
+ Thin wrapper over pnpm's native patch flow. Runs \`pnpm patch <package>\`, which
1284
+ extracts the package into a temp dir and prints the path to edit. After editing,
1285
+ commit the patch with the command pnpm prints (\`pnpm patch-commit <dir>\`).
1286
+
1287
+ This does NOT replace scripts/patch-check.sh (the patch re-apply contract).
1288
+
1289
+ Options:
1290
+ --cwd <dir> Project root to run pnpm in (defaults to current directory).
1291
+ -h, --help Show this help.`;
1292
+ async function run6(ctx) {
1293
+ const { values, positionals } = parseArgs6({
1294
+ args: ctx.argv,
1295
+ allowPositionals: true,
1296
+ options: {
1297
+ cwd: { type: "string" },
1298
+ help: { type: "boolean", short: "h", default: false }
1299
+ }
1300
+ });
1301
+ if (values.help) {
1302
+ ctx.out.log(usage6);
1303
+ return;
1304
+ }
1305
+ const pkg = positionals[0];
1306
+ if (!pkg) {
1307
+ ctx.out.fail(
1308
+ "patch requires a package name, e.g. hogsend patch @hogsend/engine"
1309
+ );
1310
+ }
1311
+ const cwd = values.cwd ?? process.cwd();
1312
+ const result = spawnSync("pnpm", ["patch", pkg], {
1313
+ cwd,
1314
+ stdio: ctx.json ? "ignore" : "inherit"
1315
+ });
1316
+ if (ctx.json) {
1317
+ ctx.out.json({
1318
+ package: pkg,
1319
+ command: `pnpm patch ${pkg}`,
1320
+ status: result.status,
1321
+ ok: result.status === 0
1322
+ });
1323
+ if (result.status !== 0) process.exit(1);
1324
+ return;
1325
+ }
1326
+ if (result.status !== 0) {
1327
+ ctx.out.fail(`pnpm patch ${pkg} exited with code ${result.status ?? "?"}`);
1328
+ }
1329
+ ctx.out.note(
1330
+ [
1331
+ "pnpm extracted the package to a temp dir (printed above).",
1332
+ "Edit the files, then commit the patch:",
1333
+ "",
1334
+ color.cyan("pnpm patch-commit <dir>")
1335
+ ].join("\n"),
1336
+ "Next steps"
1337
+ );
1338
+ }
1339
+ var patchCommand = {
1340
+ name: "patch",
1341
+ summary: "Patch a package via pnpm's native patch flow",
1342
+ usage: usage6,
1343
+ run: run6
1344
+ };
1345
+
1346
+ // src/commands/setup.ts
1347
+ import { spawnSync as spawnSync2 } from "child_process";
1348
+ import { randomBytes } from "crypto";
1349
+ import { copyFileSync, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1350
+ import { join as join4 } from "path";
1351
+ import { parseArgs as parseArgs7 } from "util";
1352
+ import { confirm } from "@clack/prompts";
1353
+
1354
+ // src/lib/prompt.ts
1355
+ import { cancel as cancel2, isCancel } from "@clack/prompts";
1356
+ function bail(value) {
1357
+ if (isCancel(value)) {
1358
+ cancel2("Cancelled.");
1359
+ process.exit(0);
1360
+ }
1361
+ return value;
1362
+ }
1363
+
1364
+ // src/commands/setup.ts
1365
+ var usage7 = `hogsend setup [--cwd <dir>] [--yes] [--json]
1366
+
1367
+ Interactive local onboarding for a scaffolded Hogsend app. Mirrors the
1368
+ create-hogsend "next steps":
1369
+
1370
+ 1. docker compose up -d # Postgres + Redis + Hatchet-Lite
1371
+ 2. cp .env.example .env (if missing)
1372
+ 3. generate a BETTER_AUTH_SECRET (if still the placeholder)
1373
+ 4. pnpm db:migrate # engine track then client track
1374
+
1375
+ Options:
1376
+ --cwd <dir> Project root to run in (defaults to the current directory).
1377
+ --yes, -y Skip confirmation prompts (assume yes). Implied by --json.
1378
+ --json Run non-interactively and emit a single JSON result document.
1379
+ -h, --help Show this help.
1380
+
1381
+ Run ${color.cyan("hogsend doctor")} afterwards to verify the instance is healthy.`;
1382
+ function generateSecret() {
1383
+ return randomBytes(32).toString("hex");
1384
+ }
1385
+ var SECRET_KEY = "BETTER_AUTH_SECRET";
1386
+ var PLACEHOLDER_PREFIX = "change-me";
1387
+ function ensureEnv(cwd) {
1388
+ const envPath = join4(cwd, ".env");
1389
+ const examplePath = join4(cwd, ".env.example");
1390
+ let copied;
1391
+ if (existsSync4(envPath)) {
1392
+ copied = {
1393
+ step: "env",
1394
+ status: "skipped",
1395
+ detail: ".env already exists"
1396
+ };
1397
+ } else if (existsSync4(examplePath)) {
1398
+ copyFileSync(examplePath, envPath);
1399
+ copied = {
1400
+ step: "env",
1401
+ status: "ok",
1402
+ detail: "copied .env.example -> .env"
1403
+ };
1404
+ } else {
1405
+ copied = {
1406
+ step: "env",
1407
+ status: "failed",
1408
+ detail: "no .env and no .env.example to copy from"
1409
+ };
1410
+ return {
1411
+ copied,
1412
+ secret: {
1413
+ step: "secret",
1414
+ status: "skipped",
1415
+ detail: "skipped \u2014 no .env"
1416
+ }
1417
+ };
1418
+ }
1419
+ let raw;
1420
+ try {
1421
+ raw = readFileSync2(envPath, "utf8");
1422
+ } catch (err) {
1423
+ return {
1424
+ copied,
1425
+ secret: {
1426
+ step: "secret",
1427
+ status: "failed",
1428
+ detail: `could not read .env: ${err instanceof Error ? err.message : String(err)}`
1429
+ }
1430
+ };
1431
+ }
1432
+ const lines = raw.split(/\r?\n/);
1433
+ const idx = lines.findIndex(
1434
+ (l) => l.replace(/^export\s+/, "").trimStart().startsWith(`${SECRET_KEY}=`)
1435
+ );
1436
+ const existingLine = idx === -1 ? void 0 : lines[idx];
1437
+ const current = existingLine === void 0 ? void 0 : existingLine.slice(existingLine.indexOf("=") + 1).trim();
1438
+ const isPlaceholder = current === void 0 || current === "" || current.startsWith(PLACEHOLDER_PREFIX);
1439
+ if (!isPlaceholder) {
1440
+ return {
1441
+ copied,
1442
+ secret: {
1443
+ step: "secret",
1444
+ status: "skipped",
1445
+ detail: `${SECRET_KEY} already set`
1446
+ }
1447
+ };
1448
+ }
1449
+ const secret = generateSecret();
1450
+ const newLine = `${SECRET_KEY}=${secret}`;
1451
+ if (idx === -1) {
1452
+ if (raw.length > 0 && !raw.endsWith("\n")) lines.push("");
1453
+ lines.push(newLine);
1454
+ } else {
1455
+ lines[idx] = newLine;
1456
+ }
1457
+ writeFileSync2(envPath, lines.join("\n"));
1458
+ return {
1459
+ copied,
1460
+ secret: {
1461
+ step: "secret",
1462
+ status: "ok",
1463
+ detail: `generated ${SECRET_KEY} (64-char hex)`
1464
+ }
1465
+ };
1466
+ }
1467
+ function runCmd(cmd, args, cwd, json) {
1468
+ const result = spawnSync2(cmd, args, {
1469
+ cwd,
1470
+ // In json mode stay silent (we report structured status); otherwise stream
1471
+ // so the user sees docker / migration output inline.
1472
+ stdio: json ? "ignore" : "inherit"
1473
+ });
1474
+ return { status: result.status, ok: result.status === 0 };
1475
+ }
1476
+ async function run7(ctx) {
1477
+ const { values } = parseArgs7({
1478
+ args: ctx.argv,
1479
+ allowPositionals: true,
1480
+ options: {
1481
+ cwd: { type: "string" },
1482
+ yes: { type: "boolean", short: "y", default: false },
1483
+ help: { type: "boolean", short: "h", default: false }
1484
+ }
1485
+ });
1486
+ if (values.help) {
1487
+ ctx.out.log(usage7);
1488
+ return;
1489
+ }
1490
+ const cwd = values.cwd ?? process.cwd();
1491
+ if (!existsSync4(join4(cwd, "package.json"))) {
1492
+ ctx.out.fail(
1493
+ `no package.json in ${cwd} \u2014 run setup from a scaffolded Hogsend app (or pass --cwd).`
1494
+ );
1495
+ }
1496
+ const hasCompose = existsSync4(join4(cwd, "docker-compose.yml")) || existsSync4(join4(cwd, "docker-compose.yaml")) || existsSync4(join4(cwd, "compose.yml")) || existsSync4(join4(cwd, "compose.yaml"));
1497
+ const skipConfirm = ctx.json || values.yes;
1498
+ if (!ctx.json) {
1499
+ ctx.out.intro(
1500
+ `${color.bgMagenta(color.black(" hogsend "))} ${color.dim("local onboarding")}`
1501
+ );
1502
+ }
1503
+ if (ctx.out.interactive && !skipConfirm) {
1504
+ const proceed = bail(
1505
+ await confirm({
1506
+ message: `Set up local infra in ${color.cyan(cwd)}? (docker compose up, .env, db:migrate)`
1507
+ })
1508
+ );
1509
+ if (!proceed) {
1510
+ ctx.out.outro(color.dim("Nothing changed."));
1511
+ return;
1512
+ }
1513
+ }
1514
+ const results = [];
1515
+ if (hasCompose) {
1516
+ const docker = await ctx.out.step(
1517
+ "Starting infra (docker compose up -d)",
1518
+ async () => runCmd("docker", ["compose", "up", "-d"], cwd, ctx.json)
1519
+ );
1520
+ results.push({
1521
+ step: "docker",
1522
+ status: docker.ok ? "ok" : "failed",
1523
+ detail: docker.ok ? "Postgres + Redis + Hatchet-Lite up" : `docker compose exited with code ${docker.status ?? "?"}`
1524
+ });
1525
+ } else {
1526
+ results.push({
1527
+ step: "docker",
1528
+ status: "skipped",
1529
+ detail: "no docker-compose file found"
1530
+ });
1531
+ }
1532
+ const env = await ctx.out.step(
1533
+ "Preparing .env + auth secret",
1534
+ async () => ensureEnv(cwd)
1535
+ );
1536
+ results.push(env.copied, env.secret);
1537
+ const dockerFailed = results.some(
1538
+ (r) => r.step === "docker" && r.status === "failed"
1539
+ );
1540
+ if (dockerFailed) {
1541
+ results.push({
1542
+ step: "migrate",
1543
+ status: "skipped",
1544
+ detail: "skipped \u2014 docker compose failed; bring infra up then run pnpm db:migrate"
1545
+ });
1546
+ } else {
1547
+ const migrate = await ctx.out.step(
1548
+ "Running migrations (pnpm db:migrate)",
1549
+ async () => runCmd("pnpm", ["db:migrate"], cwd, ctx.json)
1550
+ );
1551
+ results.push({
1552
+ step: "migrate",
1553
+ status: migrate.ok ? "ok" : "failed",
1554
+ detail: migrate.ok ? "engine + client migrations applied" : `pnpm db:migrate exited with code ${migrate.status ?? "?"}`
1555
+ });
1556
+ }
1557
+ const failed = results.filter((r) => r.status === "failed");
1558
+ const ok = failed.length === 0;
1559
+ if (ctx.json) {
1560
+ ctx.out.json({
1561
+ ok,
1562
+ cwd,
1563
+ steps: results
1564
+ });
1565
+ if (!ok) process.exit(1);
1566
+ return;
1567
+ }
1568
+ ctx.out.table(
1569
+ results.map((r) => ({
1570
+ step: r.step,
1571
+ status: r.status === "ok" ? color.green("ok") : r.status === "skipped" ? color.dim("skipped") : color.red("failed"),
1572
+ detail: r.detail
1573
+ })),
1574
+ ["step", "status", "detail"]
1575
+ );
1576
+ ctx.out.note(
1577
+ [
1578
+ `${color.cyan("pnpm dev")} ${color.dim("# HTTP API on :3002")}`,
1579
+ `${color.cyan("pnpm worker:dev")} ${color.dim("# Hatchet worker, 2nd terminal")}`,
1580
+ "",
1581
+ `${color.dim("Verify with")} ${color.cyan("hogsend doctor")}${color.dim(".")}`,
1582
+ `${color.dim("Grab HATCHET_CLIENT_TOKEN at")} ${color.cyan("http://localhost:8888")} ${color.dim("and set it in .env.")}`
1583
+ ].join("\n"),
1584
+ "Next steps"
1585
+ );
1586
+ if (!ok) {
1587
+ ctx.out.fail(
1588
+ `${failed.length} step(s) failed \u2014 see the table above. Fix and re-run hogsend setup.`
1589
+ );
1590
+ }
1591
+ ctx.out.outro(
1592
+ `${color.green("Done.")} ${color.dim("Local infra is up \u2014 go write a journey.")}`
1593
+ );
1594
+ }
1595
+ var setupCommand = {
1596
+ name: "setup",
1597
+ summary: "Local onboarding: docker compose up, gen secret, db:migrate",
1598
+ usage: usage7,
1599
+ run: run7
1600
+ };
1601
+
1602
+ // src/commands/skills.ts
1603
+ import { existsSync as existsSync5 } from "fs";
1604
+ import { join as join5 } from "path";
1605
+ import { parseArgs as parseArgs8 } from "util";
1606
+ import { multiselect } from "@clack/prompts";
1607
+ var usage8 = `hogsend skills <subcommand> [options]
1608
+
1609
+ Manage the Claude Code skills bundled with @hogsend/cli. Bundled skills teach
1610
+ agents how to drive the hogsend CLI; \`add\` copies them into your project's
1611
+ ./.claude/skills/<name>/ so Claude Code can discover them.
1612
+
1613
+ Subcommands:
1614
+ list List bundled skills + whether each is installed.
1615
+ add [name] [--force] Copy a bundled skill into ./.claude/skills/<name>/.
1616
+ Omit name for an interactive multiselect (human),
1617
+ or copy all bundled skills (--all / --json /
1618
+ non-interactive).
1619
+
1620
+ Options:
1621
+ --all Install every bundled skill (skips the interactive picker).
1622
+ --force Overwrite an already-installed skill. Use after upgrading the
1623
+ engine to refresh vendored skills to the latest guidance.
1624
+ --json Emit machine-readable JSON only (implies non-interactive).
1625
+ -h, --help Show this help.
1626
+
1627
+ Examples:
1628
+ hogsend skills list
1629
+ hogsend skills list --json
1630
+ hogsend skills add
1631
+ hogsend skills add --all
1632
+ hogsend skills add hogsend-cli --force
1633
+ hogsend skills add --all --force # refresh everything after an upgrade
1634
+
1635
+ Tip: \`hogsend upgrade\` bumps the engine AND refreshes these skills in one step.`;
1636
+ function runList3(ctx) {
1637
+ const skills = listBundledSkills(process.cwd());
1638
+ if (ctx.json) {
1639
+ ctx.out.json({
1640
+ bundledSkillsDir: bundledSkillsDir(),
1641
+ installDir: installDir(process.cwd()),
1642
+ skills
1643
+ });
1644
+ return;
1645
+ }
1646
+ ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} skills`);
1647
+ if (skills.length === 0) {
1648
+ ctx.out.note(
1649
+ "No bundled skills found in this package build.",
1650
+ "skills list"
1651
+ );
1652
+ ctx.out.outro("Nothing to install.");
1653
+ return;
1654
+ }
1655
+ ctx.out.table(
1656
+ skills.map((s) => ({
1657
+ name: s.name,
1658
+ installed: s.installed ? color.green("yes") : color.dim("no"),
1659
+ description: s.description.length > 60 ? `${s.description.slice(0, 57)}...` : s.description
1660
+ })),
1661
+ ["name", "installed", "description"]
1662
+ );
1663
+ ctx.out.outro(
1664
+ `Install with ${color.cyan("hogsend skills add <name>")} (or ${color.cyan("hogsend skills add --all")}). Refresh after an engine upgrade with ${color.cyan("--force")}.`
1665
+ );
1666
+ }
1667
+ async function runAdd(ctx, argv) {
1668
+ const { values, positionals } = parseArgs8({
1669
+ args: argv,
1670
+ allowPositionals: true,
1671
+ options: {
1672
+ all: { type: "boolean", default: false },
1673
+ force: { type: "boolean", default: false },
1674
+ help: { type: "boolean", short: "h", default: false }
1675
+ }
1676
+ });
1677
+ if (values.help) {
1678
+ ctx.out.log(usage8);
1679
+ return;
1680
+ }
1681
+ const cwd = process.cwd();
1682
+ const bundled = listBundledSkills(cwd);
1683
+ if (bundled.length === 0) {
1684
+ ctx.out.fail("no bundled skills found in this package build");
1685
+ }
1686
+ const requested = positionals[0];
1687
+ const force = Boolean(values.force);
1688
+ let names;
1689
+ if (requested) {
1690
+ const match = bundled.find((s) => s.name === requested);
1691
+ if (!match) {
1692
+ ctx.out.fail(
1693
+ `unknown skill "${requested}". Available: ${bundled.map((s) => s.name).join(", ")}`
1694
+ );
1695
+ }
1696
+ names = [requested];
1697
+ } else if (values.all) {
1698
+ names = bundled.map((s) => s.name);
1699
+ } else if (ctx.out.interactive) {
1700
+ const picked = bail(
1701
+ await multiselect({
1702
+ message: "Which skills do you want to install?",
1703
+ options: bundled.map((s) => ({
1704
+ value: s.name,
1705
+ label: s.name,
1706
+ hint: s.installed ? "installed" : void 0
1707
+ })),
1708
+ required: true
1709
+ })
1710
+ );
1711
+ names = picked;
1712
+ } else {
1713
+ names = bundled.map((s) => s.name);
1714
+ }
1715
+ const results = names.map(
1716
+ (name) => copySkill(name, cwd, force)
1717
+ );
1718
+ if (results.some((r) => r.installed)) {
1719
+ const installedNames = listBundledSkills(cwd).filter((s) => existsSync5(join5(installDir(cwd), s.name))).map((s) => s.name);
1720
+ writeSkillsStamp(cwd, installedNames);
1721
+ }
1722
+ if (ctx.json) {
1723
+ ctx.out.json({
1724
+ installDir: installDir(cwd),
1725
+ force,
1726
+ results
1727
+ });
1728
+ return;
1729
+ }
1730
+ ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} skills add`);
1731
+ for (const r of results) {
1732
+ if (r.skipped) {
1733
+ ctx.out.log(
1734
+ `${color.yellow("skip")} ${r.name} ${color.dim("(already installed; use --force to overwrite)")}`
1735
+ );
1736
+ } else {
1737
+ ctx.out.log(`${color.green("\u2713")} ${r.name} ${color.dim(`-> ${r.path}`)}`);
1738
+ }
1739
+ }
1740
+ const installedCount = results.filter((r) => r.installed).length;
1741
+ const skippedCount = results.filter((r) => r.skipped).length;
1742
+ ctx.out.outro(
1743
+ `Installed ${installedCount} skill${installedCount === 1 ? "" : "s"}` + (skippedCount > 0 ? `, skipped ${skippedCount}.` : ".")
1744
+ );
1745
+ }
1746
+ async function run8(ctx) {
1747
+ const sub = ctx.argv[0];
1748
+ switch (sub) {
1749
+ case "list":
1750
+ runList3(ctx);
1751
+ return;
1752
+ case "add":
1753
+ await runAdd(ctx, ctx.argv.slice(1));
1754
+ return;
1755
+ case void 0:
1756
+ case "-h":
1757
+ case "--help":
1758
+ ctx.out.log(usage8);
1759
+ return;
1760
+ default:
1761
+ ctx.out.fail(
1762
+ `unknown skills subcommand "${sub}". Use: list | add. See hogsend skills --help.`
1763
+ );
1764
+ }
1765
+ }
1766
+ var skillsCommand = {
1767
+ name: "skills",
1768
+ summary: "List + install bundled Claude Code skills into .claude/skills",
1769
+ usage: usage8,
1770
+ run: run8
1771
+ };
1772
+
1773
+ // src/commands/stats.ts
1774
+ import { parseArgs as parseArgs9 } from "util";
1775
+ var usage9 = `hogsend stats [--json]
1776
+
1777
+ Show system-wide overview metrics from a running Hogsend instance.
1778
+ Wraps GET /v1/admin/metrics/overview.
1779
+
1780
+ Fields:
1781
+ totalContacts Live (non-deleted) contacts.
1782
+ activeJourneys Journey states currently active or waiting.
1783
+ emailsSent24h Emails sent in the last 24 hours.
1784
+ emailsSent7d Emails sent in the last 7 days.
1785
+ emailsSent30d Emails sent in the last 30 days.
1786
+ bounceRate30d Bounced / sent over the last 30 days (0..1).
1787
+ unsubscribeRate Unsubscribed / total preferences (0..1).
1788
+
1789
+ Options:
1790
+ --url <baseUrl> API base URL (default HOGSEND_API_URL or http://localhost:3002).
1791
+ --admin-key <key> Admin bearer key (default HOGSEND_ADMIN_KEY / ADMIN_API_KEY).
1792
+ --json Emit machine-readable JSON only.
1793
+ -h, --help Show this help.`;
1794
+ function pct(rate) {
1795
+ return `${(rate * 100).toFixed(2)}%`;
1796
+ }
1797
+ async function run9(ctx) {
1798
+ const { values } = parseArgs9({
1799
+ args: ctx.argv,
1800
+ allowPositionals: true,
1801
+ options: {
1802
+ help: { type: "boolean", short: "h", default: false }
1803
+ }
1804
+ });
1805
+ if (values.help) {
1806
+ ctx.out.log(usage9);
1807
+ return;
1808
+ }
1809
+ const metrics = await ctx.out.step(
1810
+ "Fetching overview metrics",
1811
+ () => ctx.http.get("/v1/admin/metrics/overview")
1812
+ );
1813
+ if (ctx.json) {
1814
+ ctx.out.json(metrics);
1815
+ return;
1816
+ }
1817
+ ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} stats`);
1818
+ ctx.out.kv(
1819
+ {
1820
+ "Total contacts": metrics.totalContacts,
1821
+ "Active journeys": metrics.activeJourneys,
1822
+ "Emails sent (24h)": metrics.emailsSent24h,
1823
+ "Emails sent (7d)": metrics.emailsSent7d,
1824
+ "Emails sent (30d)": metrics.emailsSent30d,
1825
+ "Bounce rate (30d)": pct(metrics.bounceRate30d),
1826
+ "Unsubscribe rate": pct(metrics.unsubscribeRate)
1827
+ },
1828
+ "Overview"
1829
+ );
1830
+ ctx.out.outro(color.dim(ctx.http.cfg.baseUrl));
1831
+ }
1832
+ var statsCommand = {
1833
+ name: "stats",
1834
+ summary: "Show system-wide overview metrics",
1835
+ usage: usage9,
1836
+ run: run9
1837
+ };
1838
+
1839
+ // src/commands/studio.ts
1840
+ import { spawn } from "child_process";
1841
+ import { createReadStream, existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
1842
+ import { createServer } from "http";
1843
+ import { extname, join as join6, normalize, resolve, sep as sep3 } from "path";
1844
+ import { fileURLToPath as fileURLToPath2 } from "url";
1845
+ import { parseArgs as parseArgs10 } from "util";
1846
+ var usage10 = `hogsend studio [options]
1847
+
1848
+ Serve the bundled Hogsend Studio (the admin SPA) locally and open it in a
1849
+ browser. The Studio is a static single-page app; this command starts a tiny
1850
+ local web server for it on a port of your choosing.
1851
+
1852
+ By default the Studio talks to the API at the same origin it is served from,
1853
+ which won't be a running API here \u2014 so point it at your instance with
1854
+ --base-url (the SPA uses cookie auth, so the instance must allow CORS from the
1855
+ Studio origin, or you can simply open the Studio that the engine mounts at
1856
+ \`<instance>/studio\` instead).
1857
+
1858
+ Options:
1859
+ --port <n> Local port to serve on (default 3333).
1860
+ --base-url <url> API instance the Studio should call (injected at runtime).
1861
+ Omit to use same-origin (the local server, for static
1862
+ preview only).
1863
+ --open Open the Studio in your default browser after starting.
1864
+ --dist <path> Override the Studio dist directory (advanced).
1865
+ -h, --help Show this help.
1866
+
1867
+ Examples:
1868
+ hogsend studio --open
1869
+ hogsend studio --base-url https://api.example.com --open
1870
+ hogsend studio --port 4000`;
1871
+ function resolveStudioDist(distFlag) {
1872
+ const candidates = [];
1873
+ if (distFlag && distFlag.length > 0) {
1874
+ candidates.push(resolve(process.cwd(), distFlag));
1875
+ }
1876
+ candidates.push(fileURLToPath2(new URL("../studio", import.meta.url)));
1877
+ candidates.push(
1878
+ fileURLToPath2(new URL("../../studio/dist", import.meta.url)),
1879
+ fileURLToPath2(new URL("../../../studio/dist", import.meta.url))
1880
+ );
1881
+ candidates.push(resolve(process.cwd(), "packages/studio/dist"));
1882
+ for (const dir of candidates) {
1883
+ if (existsSync6(join6(dir, "index.html"))) {
1884
+ return dir;
1885
+ }
1886
+ }
1887
+ return null;
1888
+ }
1889
+ var MIME = {
1890
+ ".html": "text/html; charset=utf-8",
1891
+ ".js": "text/javascript; charset=utf-8",
1892
+ ".mjs": "text/javascript; charset=utf-8",
1893
+ ".css": "text/css; charset=utf-8",
1894
+ ".json": "application/json; charset=utf-8",
1895
+ ".svg": "image/svg+xml",
1896
+ ".png": "image/png",
1897
+ ".jpg": "image/jpeg",
1898
+ ".jpeg": "image/jpeg",
1899
+ ".gif": "image/gif",
1900
+ ".ico": "image/x-icon",
1901
+ ".woff": "font/woff",
1902
+ ".woff2": "font/woff2",
1903
+ ".ttf": "font/ttf",
1904
+ ".map": "application/json; charset=utf-8"
1905
+ };
1906
+ function mimeFor(path) {
1907
+ return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
1908
+ }
1909
+ function indexHtml(distPath, baseUrl) {
1910
+ const raw = readFileSync3(join6(distPath, "index.html"), "utf8");
1911
+ if (!baseUrl) return raw;
1912
+ const inject = `<script>window.__HOGSEND_STUDIO__=${JSON.stringify({
1913
+ baseUrl
1914
+ })};</script>`;
1915
+ if (raw.includes("</head>")) {
1916
+ return raw.replace("</head>", `${inject}</head>`);
1917
+ }
1918
+ return `${inject}${raw}`;
1919
+ }
1920
+ function openBrowser(url) {
1921
+ const platform = process.platform;
1922
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
1923
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
1924
+ try {
1925
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
1926
+ child.on("error", () => {
1927
+ });
1928
+ child.unref();
1929
+ } catch {
1930
+ }
1931
+ }
1932
+ async function run10(ctx) {
1933
+ const { values, positionals } = parseArgs10({
1934
+ args: ctx.argv,
1935
+ allowPositionals: true,
1936
+ strict: false,
1937
+ options: {
1938
+ port: { type: "string" },
1939
+ "base-url": { type: "string" },
1940
+ open: { type: "boolean", default: false },
1941
+ dist: { type: "string" },
1942
+ help: { type: "boolean", short: "h", default: false }
1943
+ }
1944
+ });
1945
+ if (values.help) {
1946
+ ctx.out.log(usage10);
1947
+ return;
1948
+ }
1949
+ const port = Number(values.port ?? "3333");
1950
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1951
+ ctx.out.fail(`invalid --port "${values.port}" (expected 1-65535)`);
1952
+ }
1953
+ const baseUrl = typeof values["base-url"] === "string" ? values["base-url"] : void 0;
1954
+ const distPath = resolveStudioDist(
1955
+ typeof values.dist === "string" ? values.dist : positionals[0]
1956
+ );
1957
+ if (!distPath) {
1958
+ ctx.out.fail(
1959
+ "could not find a built Studio (dist/). Build it with `pnpm --filter @hogsend/studio build`, or pass --dist <path>."
1960
+ );
1961
+ }
1962
+ const cleanBase = baseUrl ? baseUrl.replace(/\/+$/, "") : void 0;
1963
+ const index = indexHtml(distPath, cleanBase);
1964
+ const server = createServer((req, res) => {
1965
+ const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
1966
+ const rel = urlPath.replace(/^\/studio/, "");
1967
+ if (rel === "" || rel === "/") {
1968
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1969
+ res.end(index);
1970
+ return;
1971
+ }
1972
+ const target = normalize(join6(distPath, rel));
1973
+ if (target !== distPath && !target.startsWith(distPath + sep3)) {
1974
+ res.writeHead(403);
1975
+ res.end("Forbidden");
1976
+ return;
1977
+ }
1978
+ if (existsSync6(target) && statSync2(target).isFile()) {
1979
+ res.writeHead(200, { "content-type": mimeFor(target) });
1980
+ createReadStream(target).pipe(res);
1981
+ return;
1982
+ }
1983
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1984
+ res.end(index);
1985
+ });
1986
+ await new Promise((resolveListen, reject) => {
1987
+ server.once("error", reject);
1988
+ server.listen(port, () => resolveListen());
1989
+ }).catch((err) => {
1990
+ const msg = err instanceof Error ? err.message : String(err);
1991
+ ctx.out.fail(`could not start server on port ${port}: ${msg}`);
1992
+ });
1993
+ const localUrl = `http://localhost:${port}/studio/`;
1994
+ if (ctx.json) {
1995
+ ctx.out.json({
1996
+ url: localUrl,
1997
+ port,
1998
+ baseUrl: cleanBase ?? null,
1999
+ dist: distPath
2000
+ });
2001
+ } else {
2002
+ ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} studio`);
2003
+ ctx.out.note(
2004
+ [
2005
+ `${color.green("\u25CF")} Studio serving at ${color.cyan(localUrl)}`,
2006
+ cleanBase ? color.dim(`API instance: ${cleanBase}`) : color.dim(
2007
+ "No --base-url set (same-origin / static preview). The API calls will hit this local server and fail \u2014 pass --base-url <instance>, or open <instance>/studio directly."
2008
+ ),
2009
+ "",
2010
+ color.dim("First load shows a create-admin screen if no admin exists."),
2011
+ color.dim("Press Ctrl+C to stop.")
2012
+ ].join("\n"),
2013
+ "Studio"
2014
+ );
2015
+ }
2016
+ if (values.open) {
2017
+ openBrowser(localUrl);
2018
+ }
2019
+ await new Promise((resolveForever) => {
2020
+ const stop = () => {
2021
+ server.close(() => resolveForever());
2022
+ };
2023
+ process.on("SIGINT", stop);
2024
+ process.on("SIGTERM", stop);
2025
+ });
2026
+ }
2027
+ var studioCommand = {
2028
+ name: "studio",
2029
+ summary: "Serve the bundled Hogsend Studio admin SPA locally",
2030
+ usage: usage10,
2031
+ run: run10
2032
+ };
2033
+
2034
+ // src/commands/upgrade.ts
2035
+ import { spawnSync as spawnSync3 } from "child_process";
2036
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
2037
+ import { join as join7 } from "path";
2038
+ import { parseArgs as parseArgs11 } from "util";
2039
+ import { confirm as confirm2 } from "@clack/prompts";
2040
+ var usage11 = `hogsend upgrade [--cwd <dir>] [--pm <pnpm|npm|yarn|bun>] [options]
2041
+
2042
+ Upgrade a scaffolded Hogsend app in one step:
2043
+ 1. bump every @hogsend/* dependency to latest (or --to <version>), then
2044
+ 2. refresh the vendored Claude Code skills in ./.claude/skills to match.
2045
+
2046
+ Run this after a new engine release so your app AND the agent guidance move
2047
+ together. Skills are version-stamped so \`hogsend doctor\` can warn when they
2048
+ fall behind.
2049
+
2050
+ Options:
2051
+ --cwd <dir> Project root to upgrade (defaults to the current directory).
2052
+ --pm <manager> Package manager (default: detected from the lockfile, else pnpm).
2053
+ --to <version> Target version for @hogsend/* deps (default: latest).
2054
+ --deps-only Bump dependencies only; don't touch skills.
2055
+ --skills-only Refresh skills only; don't touch dependencies.
2056
+ --yes, -y Skip the confirmation prompt. Implied by --json.
2057
+ --json Run non-interactively and emit a single JSON result.
2058
+ -h, --help Show this help.`;
2059
+ var VALID_PMS = ["pnpm", "npm", "yarn", "bun"];
2060
+ function detectPm(cwd) {
2061
+ if (existsSync7(join7(cwd, "pnpm-lock.yaml"))) return "pnpm";
2062
+ if (existsSync7(join7(cwd, "yarn.lock"))) return "yarn";
2063
+ if (existsSync7(join7(cwd, "bun.lockb")) || existsSync7(join7(cwd, "bun.lock")))
2064
+ return "bun";
2065
+ if (existsSync7(join7(cwd, "package-lock.json"))) return "npm";
2066
+ return "pnpm";
2067
+ }
2068
+ function hogsendDeps(cwd) {
2069
+ const pkg = JSON.parse(readFileSync4(join7(cwd, "package.json"), "utf8"));
2070
+ const all = { ...pkg.dependencies, ...pkg.devDependencies };
2071
+ return Object.keys(all).filter((n) => n.startsWith("@hogsend/")).sort();
2072
+ }
2073
+ function addArgs(pm, specs) {
2074
+ return [pm === "npm" ? "install" : "add", ...specs];
2075
+ }
2076
+ async function run11(ctx) {
2077
+ const { values } = parseArgs11({
2078
+ args: ctx.argv,
2079
+ allowPositionals: true,
2080
+ options: {
2081
+ cwd: { type: "string" },
2082
+ pm: { type: "string" },
2083
+ to: { type: "string" },
2084
+ "deps-only": { type: "boolean", default: false },
2085
+ "skills-only": { type: "boolean", default: false },
2086
+ yes: { type: "boolean", short: "y", default: false },
2087
+ help: { type: "boolean", short: "h", default: false }
2088
+ }
2089
+ });
2090
+ if (values.help) {
2091
+ ctx.out.log(usage11);
2092
+ return;
2093
+ }
2094
+ if (values["deps-only"] && values["skills-only"]) {
2095
+ ctx.out.fail("--deps-only and --skills-only are mutually exclusive.");
2096
+ }
2097
+ const cwd = values.cwd ?? process.cwd();
2098
+ if (!existsSync7(join7(cwd, "package.json"))) {
2099
+ ctx.out.fail(
2100
+ `no package.json in ${cwd} \u2014 run upgrade from a scaffolded Hogsend app (or pass --cwd).`
2101
+ );
2102
+ }
2103
+ let pm;
2104
+ if (values.pm !== void 0) {
2105
+ if (!VALID_PMS.includes(values.pm)) {
2106
+ ctx.out.fail(
2107
+ `invalid --pm "${values.pm}". Expected one of: ${VALID_PMS.join(", ")}.`
2108
+ );
2109
+ }
2110
+ pm = values.pm;
2111
+ } else {
2112
+ pm = detectPm(cwd);
2113
+ }
2114
+ const target = values.to ?? "latest";
2115
+ const doDeps = !values["skills-only"];
2116
+ const doSkills = !values["deps-only"];
2117
+ const deps = doDeps ? hogsendDeps(cwd) : [];
2118
+ if (doDeps && deps.length === 0) {
2119
+ ctx.out.fail(
2120
+ `no @hogsend/* dependencies found in ${join7(cwd, "package.json")}.`
2121
+ );
2122
+ }
2123
+ const skipConfirm = ctx.json || values.yes;
2124
+ if (!ctx.json) {
2125
+ ctx.out.intro(
2126
+ `${color.bgMagenta(color.black(" hogsend "))} ${color.dim("upgrade")}`
2127
+ );
2128
+ }
2129
+ if (ctx.out.interactive && !skipConfirm) {
2130
+ const plan = [
2131
+ doDeps ? `bump ${deps.length} @hogsend/* dep(s) to ${target} (${pm})` : null,
2132
+ doSkills ? "refresh .claude/skills" : null
2133
+ ].filter(Boolean).join(" + ");
2134
+ const proceed = bail(
2135
+ await confirm2({ message: `Upgrade ${color.cyan(cwd)}: ${plan}?` })
2136
+ );
2137
+ if (!proceed) {
2138
+ ctx.out.outro(color.dim("Nothing changed."));
2139
+ return;
2140
+ }
2141
+ }
2142
+ const results = [];
2143
+ if (doDeps) {
2144
+ const specs = deps.map((n) => `${n}@${target}`);
2145
+ const dep = await ctx.out.step(
2146
+ `Bumping @hogsend/* -> ${target} (${pm})`,
2147
+ async () => spawnSync3(pm, addArgs(pm, specs), {
2148
+ cwd,
2149
+ stdio: ctx.json ? "ignore" : "inherit",
2150
+ shell: process.platform === "win32"
2151
+ })
2152
+ );
2153
+ results.push({
2154
+ step: "deps",
2155
+ status: dep.status === 0 ? "ok" : "failed",
2156
+ detail: dep.status === 0 ? `${deps.join(", ")} -> ${target}` : `${pm} exited with code ${dep.status ?? "?"}`
2157
+ });
2158
+ } else {
2159
+ results.push({ step: "deps", status: "skipped", detail: "--skills-only" });
2160
+ }
2161
+ const depsFailed = results.some(
2162
+ (r) => r.step === "deps" && r.status === "failed"
2163
+ );
2164
+ if (!doSkills) {
2165
+ results.push({
2166
+ step: "skills",
2167
+ status: "skipped",
2168
+ detail: "--deps-only"
2169
+ });
2170
+ } else if (depsFailed) {
2171
+ results.push({
2172
+ step: "skills",
2173
+ status: "skipped",
2174
+ detail: "skipped \u2014 dependency bump failed; fix it then re-run"
2175
+ });
2176
+ } else {
2177
+ const bundled = listBundledSkills(cwd);
2178
+ const copied = bundled.map((s) => copySkill(s.name, cwd, true));
2179
+ writeSkillsStamp(
2180
+ cwd,
2181
+ bundled.map((s) => s.name)
2182
+ );
2183
+ results.push({
2184
+ step: "skills",
2185
+ status: "ok",
2186
+ detail: `refreshed ${copied.length} skill(s) -> ${installDir(cwd)}`
2187
+ });
2188
+ }
2189
+ const failed = results.filter((r) => r.status === "failed");
2190
+ const ok = failed.length === 0;
2191
+ if (ctx.json) {
2192
+ ctx.out.json({ ok, cwd, pm, target, steps: results });
2193
+ if (!ok) process.exit(1);
2194
+ return;
2195
+ }
2196
+ ctx.out.table(
2197
+ results.map((r) => ({
2198
+ step: r.step,
2199
+ status: r.status === "ok" ? color.green("ok") : r.status === "skipped" ? color.dim("skipped") : color.red("failed"),
2200
+ detail: r.detail
2201
+ })),
2202
+ ["step", "status", "detail"]
2203
+ );
2204
+ if (!ok) {
2205
+ ctx.out.fail(
2206
+ `${failed.length} step(s) failed \u2014 see the table above. Fix and re-run hogsend upgrade.`
2207
+ );
2208
+ }
2209
+ ctx.out.outro(
2210
+ `${color.green("Upgraded.")} ${color.dim("Engine + agent skills are on the latest line.")}`
2211
+ );
2212
+ }
2213
+ var upgradeCommand = {
2214
+ name: "upgrade",
2215
+ summary: "Bump @hogsend/* deps to latest + refresh vendored skills",
2216
+ usage: usage11,
2217
+ run: run11
2218
+ };
2219
+
2220
+ // src/commands/index.ts
2221
+ var commands = [
2222
+ doctorCommand,
2223
+ journeysCommand,
2224
+ contactsCommand,
2225
+ statsCommand,
2226
+ eventsCommand,
2227
+ studioCommand,
2228
+ setupCommand,
2229
+ skillsCommand,
2230
+ upgradeCommand,
2231
+ ejectCommand,
2232
+ patchCommand
2233
+ ];
2234
+
2235
+ // src/lib/config.ts
2236
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
2237
+ import { join as join8 } from "path";
2238
+ import { parseArgs as parseArgs12 } from "util";
2239
+ var DEFAULT_BASE_URL = "http://localhost:3002";
2240
+ function parseGlobalFlags(argv) {
2241
+ const { values, tokens } = parseArgs12({
2242
+ args: argv,
2243
+ allowPositionals: true,
2244
+ strict: false,
2245
+ tokens: true,
2246
+ options: {
2247
+ url: { type: "string" },
2248
+ "admin-key": { type: "string" },
2249
+ json: { type: "boolean", default: false },
2250
+ help: { type: "boolean", short: "h", default: false }
2251
+ }
2252
+ });
2253
+ const owned = /* @__PURE__ */ new Set(["url", "admin-key", "json", "help", "h"]);
2254
+ const rest = [];
2255
+ for (const token of tokens) {
2256
+ if (token.kind === "positional") {
2257
+ rest.push(token.value);
2258
+ } else if (token.kind === "option") {
2259
+ if (owned.has(token.name)) continue;
2260
+ rest.push(token.rawName);
2261
+ if (token.value !== void 0 && !token.inlineValue) {
2262
+ rest.push(token.value);
2263
+ } else if (token.inlineValue && token.value !== void 0) {
2264
+ rest[rest.length - 1] = `${token.rawName}=${token.value}`;
2265
+ }
2266
+ }
2267
+ }
2268
+ return {
2269
+ url: typeof values.url === "string" ? values.url : void 0,
2270
+ adminKey: typeof values["admin-key"] === "string" ? values["admin-key"] : void 0,
2271
+ json: values.json === true,
2272
+ help: values.help === true,
2273
+ rest
2274
+ };
2275
+ }
2276
+ function loadDotEnv(cwd = process.cwd()) {
2277
+ const out = {};
2278
+ const file = join8(cwd, ".env");
2279
+ if (!existsSync8(file)) return out;
2280
+ let raw;
2281
+ try {
2282
+ raw = readFileSync5(file, "utf8");
2283
+ } catch {
2284
+ return out;
2285
+ }
2286
+ for (const rawLine of raw.split(/\r?\n/)) {
2287
+ const line = rawLine.trim();
2288
+ if (line === "" || line.startsWith("#")) continue;
2289
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length) : line;
2290
+ const eq = withoutExport.indexOf("=");
2291
+ if (eq === -1) continue;
2292
+ const key = withoutExport.slice(0, eq).trim();
2293
+ if (key === "") continue;
2294
+ let value = withoutExport.slice(eq + 1).trim();
2295
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2296
+ value = value.slice(1, -1);
2297
+ }
2298
+ out[key] = value;
2299
+ }
2300
+ return out;
2301
+ }
2302
+ function resolveConfig(flags, cwd = process.cwd()) {
2303
+ const dotenv = loadDotEnv(cwd);
2304
+ const baseUrlRaw = flags.url ?? process.env.HOGSEND_API_URL ?? dotenv.HOGSEND_API_URL ?? DEFAULT_BASE_URL;
2305
+ const adminKey = flags.adminKey ?? process.env.HOGSEND_ADMIN_KEY ?? process.env.ADMIN_API_KEY ?? dotenv.HOGSEND_ADMIN_KEY ?? dotenv.ADMIN_API_KEY;
2306
+ return {
2307
+ baseUrl: baseUrlRaw.replace(/\/+$/, ""),
2308
+ adminKey: adminKey && adminKey.length > 0 ? adminKey : void 0
2309
+ };
2310
+ }
2311
+
2312
+ // src/bin.ts
2313
+ function version() {
2314
+ try {
2315
+ const require2 = createRequire3(import.meta.url);
2316
+ const pkg = require2("../package.json");
2317
+ return pkg.version ?? "0.0.0";
2318
+ } catch {
2319
+ return "0.0.0";
2320
+ }
2321
+ }
2322
+ function rootUsage() {
2323
+ const longest = commands.reduce((n, c) => Math.max(n, c.name.length), 0);
2324
+ const list = commands.map((c) => ` ${color.cyan(c.name.padEnd(longest))} ${c.summary}`).join("\n");
2325
+ return `${color.bold("hogsend")} \u2014 the agent-native Hogsend CLI
2326
+
2327
+ ${color.dim("Usage:")} hogsend <command> [options]
2328
+
2329
+ ${color.dim("Commands:")}
2330
+ ${list}
2331
+
2332
+ ${color.dim("Global options:")}
2333
+ --url <baseUrl> Target instance (default HOGSEND_API_URL or http://localhost:3002)
2334
+ --admin-key <key> Admin bearer token (default HOGSEND_ADMIN_KEY / ADMIN_API_KEY)
2335
+ --json Emit machine-readable JSON only (for agents)
2336
+ -h, --help Show help (use after a command for command help)
2337
+ -v, --version Show version
2338
+
2339
+ Run ${color.cyan("hogsend <command> --help")} for command-specific options.`;
2340
+ }
2341
+ function findCommand(name) {
2342
+ return commands.find((c) => c.name === name);
2343
+ }
2344
+ async function main() {
2345
+ const argv = process.argv.slice(2);
2346
+ const [token, ...afterToken] = argv;
2347
+ if (token === "-v" || token === "--version") {
2348
+ process.stdout.write(`${version()}
2349
+ `);
2350
+ return;
2351
+ }
2352
+ if (!token || token === "-h" || token === "--help") {
2353
+ process.stdout.write(`${rootUsage()}
2354
+ `);
2355
+ return;
2356
+ }
2357
+ const command = findCommand(token);
2358
+ if (!command) {
2359
+ process.stderr.write(
2360
+ `${color.red("error")} unknown command "${token}"
2361
+
2362
+ ${rootUsage()}
2363
+ `
2364
+ );
2365
+ process.exit(1);
2366
+ }
2367
+ const flags = parseGlobalFlags(afterToken);
2368
+ const out = createOutput({ json: flags.json });
2369
+ if (flags.help) {
2370
+ out.log(command.usage);
2371
+ return;
2372
+ }
2373
+ const cfg = resolveConfig(flags);
2374
+ const http = createAdminClient(cfg);
2375
+ await command.run({
2376
+ argv: flags.rest,
2377
+ cfg,
2378
+ http,
2379
+ out,
2380
+ json: flags.json
2381
+ });
2382
+ }
2383
+ main().catch((error) => {
2384
+ const msg = error instanceof Error ? error.message : String(error);
2385
+ if (process.argv.includes("--json")) {
2386
+ process.stdout.write(`${JSON.stringify({ error: msg })}
2387
+ `);
2388
+ } else {
2389
+ process.stderr.write(`${color.red("error")} ${msg}
2390
+ `);
2391
+ }
229
2392
  process.exit(1);
230
2393
  });
231
2394
  //# sourceMappingURL=bin.js.map