@hogsend/cli 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +1750 -58
- package/dist/bin.js.map +1 -1
- package/package.json +6 -1
- package/skills/hogsend-cli/SKILL.md +80 -0
- package/skills/hogsend-cli/references/debug-a-journey.md +66 -0
- package/skills/hogsend-cli/references/manage-journeys.md +53 -0
- package/skills/hogsend-cli/references/query-stats.md +66 -0
- package/skills/hogsend-cli/references/setup-local.md +52 -0
- package/src/bin.ts +73 -111
- package/src/commands/contacts.ts +316 -0
- package/src/commands/doctor.ts +217 -0
- package/src/commands/eject.ts +106 -0
- package/src/commands/events.ts +154 -0
- package/src/commands/index.ts +32 -0
- package/src/commands/journeys.ts +343 -0
- package/src/commands/patch.ts +80 -0
- package/src/commands/setup.ts +322 -0
- package/src/commands/skills.ts +268 -0
- package/src/commands/stats.ts +87 -0
- package/src/commands/types.ts +41 -0
- package/src/index.ts +2 -0
- package/src/lib/config.ts +147 -0
- package/src/lib/http.ts +145 -0
- package/src/lib/output.ts +185 -0
- package/src/lib/prompt.ts +17 -0
package/dist/bin.js
CHANGED
|
@@ -1,10 +1,585 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin.ts
|
|
4
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
|
+
|
|
6
|
+
// src/commands/contacts.ts
|
|
7
|
+
import { parseArgs } from "util";
|
|
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 sep3 = 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(sep3)}
|
|
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
|
+
var usage2 = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
|
|
434
|
+
|
|
435
|
+
Probe a running Hogsend instance via GET /v1/health and report its health:
|
|
436
|
+
component status (database, redis), two-track schema state (engine + client),
|
|
437
|
+
and an overall verdict.
|
|
438
|
+
|
|
439
|
+
The health route is unauthenticated, so doctor works without an admin key.
|
|
440
|
+
|
|
441
|
+
Verdict:
|
|
442
|
+
ok service healthy, all components up, schema in sync
|
|
443
|
+
degraded reachable but a component (database/redis) is down
|
|
444
|
+
migration_pending reachable but a schema track is behind (pending migrations)
|
|
445
|
+
unreachable the instance could not be reached at all
|
|
446
|
+
|
|
447
|
+
Exit code: 0 when ok, 1 when unreachable / degraded / migration_pending.
|
|
448
|
+
|
|
449
|
+
Options:
|
|
450
|
+
--url <baseUrl> Target instance (default HOGSEND_API_URL / .env / :3002).
|
|
451
|
+
--admin-key <key> Unused by doctor (health is unauthenticated).
|
|
452
|
+
--json Emit machine-readable JSON only.
|
|
453
|
+
-h, --help Show this help.`;
|
|
454
|
+
function toVerdict(status) {
|
|
455
|
+
switch (status) {
|
|
456
|
+
case "healthy":
|
|
457
|
+
return "ok";
|
|
458
|
+
case "degraded":
|
|
459
|
+
return "degraded";
|
|
460
|
+
case "migration_pending":
|
|
461
|
+
return "migration_pending";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function componentSymbol(status) {
|
|
465
|
+
return status === "up" ? color.green("up") : color.red("down");
|
|
466
|
+
}
|
|
467
|
+
function trackLine(name, track) {
|
|
468
|
+
const sync = track.inSync ? color.green("in sync") : color.yellow(
|
|
469
|
+
`behind (${track.pending.length} pending: ${track.pending.length > 0 ? track.pending.join(", ") : "n/a"})`
|
|
470
|
+
);
|
|
471
|
+
const applied = track.applied ?? color.dim("none");
|
|
472
|
+
const required = track.required ?? color.dim("none");
|
|
473
|
+
return `${color.bold(name.padEnd(7))} applied ${applied} -> required ${required} ${sync}`;
|
|
474
|
+
}
|
|
475
|
+
async function run2(ctx) {
|
|
476
|
+
const { values } = parseArgs2({
|
|
477
|
+
args: ctx.argv,
|
|
478
|
+
allowPositionals: true,
|
|
479
|
+
options: {
|
|
480
|
+
help: { type: "boolean", short: "h", default: false }
|
|
481
|
+
},
|
|
482
|
+
// doctor takes no extra flags of its own; tolerate stray tokens.
|
|
483
|
+
strict: false
|
|
484
|
+
});
|
|
485
|
+
if (values.help) {
|
|
486
|
+
ctx.out.log(usage2);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const { baseUrl } = ctx.http.cfg;
|
|
490
|
+
let health = null;
|
|
491
|
+
let reachError = null;
|
|
492
|
+
try {
|
|
493
|
+
health = await ctx.out.step(
|
|
494
|
+
`GET ${baseUrl}/v1/health`,
|
|
495
|
+
() => ctx.http.get("/v1/health", void 0, { auth: false })
|
|
496
|
+
);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (isHttpError(error) && error.status === 0) {
|
|
499
|
+
reachError = error.message;
|
|
500
|
+
} else if (isHttpError(error)) {
|
|
501
|
+
reachError = error.message;
|
|
502
|
+
} else {
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!health) {
|
|
507
|
+
const verdict2 = "unreachable";
|
|
508
|
+
if (ctx.json) {
|
|
509
|
+
ctx.out.json({
|
|
510
|
+
ok: false,
|
|
511
|
+
verdict: verdict2,
|
|
512
|
+
baseUrl,
|
|
513
|
+
error: reachError ?? "unreachable"
|
|
514
|
+
});
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
ctx.out.note(
|
|
518
|
+
[
|
|
519
|
+
`${color.red("\u25CF")} ${color.bold("unreachable")}`,
|
|
520
|
+
"",
|
|
521
|
+
reachError ?? `could not reach ${baseUrl}`,
|
|
522
|
+
"",
|
|
523
|
+
color.dim("Is the instance running? Check --url / HOGSEND_API_URL.")
|
|
524
|
+
].join("\n"),
|
|
525
|
+
"Doctor"
|
|
526
|
+
);
|
|
527
|
+
ctx.out.outro(color.red("doctor: unreachable"));
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
const verdict = toVerdict(health.status);
|
|
531
|
+
const ok = verdict === "ok";
|
|
532
|
+
if (ctx.json) {
|
|
533
|
+
ctx.out.json({
|
|
534
|
+
ok,
|
|
535
|
+
verdict,
|
|
536
|
+
baseUrl,
|
|
537
|
+
version: health.version,
|
|
538
|
+
uptime: health.uptime,
|
|
539
|
+
timestamp: health.timestamp,
|
|
540
|
+
components: health.components,
|
|
541
|
+
schema: health.schema
|
|
542
|
+
});
|
|
543
|
+
if (!ok) process.exit(1);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const badge3 = `${color.bgMagenta(color.black(" hogsend "))} doctor`;
|
|
547
|
+
ctx.out.intro(badge3);
|
|
548
|
+
const verdictColor = verdict === "ok" ? color.green : verdict === "degraded" ? color.red : color.yellow;
|
|
549
|
+
const lines = [
|
|
550
|
+
`${verdictColor("\u25CF")} ${color.bold(verdict)}`,
|
|
551
|
+
color.dim(
|
|
552
|
+
`${baseUrl} v${health.version} up ${Math.round(health.uptime)}s`
|
|
553
|
+
),
|
|
554
|
+
"",
|
|
555
|
+
color.bold("Components"),
|
|
556
|
+
` database ${componentSymbol(health.components.database.status)}${health.components.database.latencyMs !== void 0 ? color.dim(` ${health.components.database.latencyMs}ms`) : ""}`,
|
|
557
|
+
` redis ${componentSymbol(health.components.redis.status)}${health.components.redis.latencyMs !== void 0 ? color.dim(` ${health.components.redis.latencyMs}ms`) : ""}`,
|
|
558
|
+
"",
|
|
559
|
+
color.bold("Schema"),
|
|
560
|
+
` ${trackLine("engine", health.schema.engine)}`,
|
|
561
|
+
` ${trackLine("client", health.schema.client)}`
|
|
562
|
+
];
|
|
563
|
+
ctx.out.note(lines.join("\n"), "Doctor");
|
|
564
|
+
if (ok) {
|
|
565
|
+
ctx.out.outro(color.green("doctor: ok"));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
ctx.out.outro(verdictColor(`doctor: ${verdict}`));
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
var doctorCommand = {
|
|
572
|
+
name: "doctor",
|
|
573
|
+
summary: "Probe a running instance's health (GET /v1/health)",
|
|
574
|
+
usage: usage2,
|
|
575
|
+
run: run2
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// src/commands/eject.ts
|
|
4
579
|
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
5
580
|
import { createRequire } from "module";
|
|
6
581
|
import { dirname, join as join2, sep as sep2 } from "path";
|
|
7
|
-
import { parseArgs } from "util";
|
|
582
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
8
583
|
|
|
9
584
|
// src/eject.ts
|
|
10
585
|
import { existsSync } from "fs";
|
|
@@ -117,30 +692,18 @@ async function countFiles(dir) {
|
|
|
117
692
|
return count;
|
|
118
693
|
}
|
|
119
694
|
|
|
120
|
-
// src/
|
|
121
|
-
var
|
|
122
|
-
|
|
123
|
-
Usage:
|
|
124
|
-
hogsend eject <package> [--force] [--cwd <dir>]
|
|
695
|
+
// src/commands/eject.ts
|
|
696
|
+
var usage3 = `hogsend eject <package> [--force] [--cwd <dir>]
|
|
125
697
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
rewrite the consumer dependency to file:./vendor/<name>.
|
|
129
|
-
Every other dependency keeps upgrading via pnpm up.
|
|
698
|
+
Copy a @hogsend/* package's source into vendor/<name> and rewrite the consumer
|
|
699
|
+
dependency to file:./vendor/<name>. Every other dependency keeps upgrading.
|
|
130
700
|
|
|
131
701
|
Options:
|
|
132
|
-
--force
|
|
133
|
-
--cwd <dir>
|
|
134
|
-
-h, --help
|
|
702
|
+
--force Overwrite an existing vendor/<name>.
|
|
703
|
+
--cwd <dir> Consumer repo root (defaults to the current directory).
|
|
704
|
+
-h, --help Show this help.
|
|
135
705
|
|
|
136
706
|
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
707
|
function resolveSourceDir(pkg, consumerRoot) {
|
|
145
708
|
const direct = join2(consumerRoot, "node_modules", pkg, "package.json");
|
|
146
709
|
if (existsSync2(direct)) {
|
|
@@ -151,21 +714,16 @@ function resolveSourceDir(pkg, consumerRoot) {
|
|
|
151
714
|
const entry = require2.resolve(pkg);
|
|
152
715
|
let dir = dirname(entry);
|
|
153
716
|
while (dir !== dirname(dir)) {
|
|
154
|
-
|
|
155
|
-
if (existsSync2(candidate)) {
|
|
156
|
-
return dir;
|
|
157
|
-
}
|
|
717
|
+
if (existsSync2(join2(dir, "package.json"))) return dir;
|
|
158
718
|
dir = dirname(dir);
|
|
159
719
|
}
|
|
160
720
|
} catch {
|
|
161
721
|
}
|
|
162
|
-
|
|
163
|
-
`cannot resolve ${pkg} from ${consumerRoot}. Is it installed? Run pnpm install first.`
|
|
164
|
-
);
|
|
722
|
+
return null;
|
|
165
723
|
}
|
|
166
|
-
async function
|
|
167
|
-
const { values, positionals } =
|
|
168
|
-
args,
|
|
724
|
+
async function run3(ctx) {
|
|
725
|
+
const { values, positionals } = parseArgs3({
|
|
726
|
+
args: ctx.argv,
|
|
169
727
|
allowPositionals: true,
|
|
170
728
|
options: {
|
|
171
729
|
force: { type: "boolean", default: false },
|
|
@@ -174,58 +732,1192 @@ async function runEject(args) {
|
|
|
174
732
|
}
|
|
175
733
|
});
|
|
176
734
|
if (values.help) {
|
|
177
|
-
|
|
178
|
-
`);
|
|
735
|
+
ctx.out.log(usage3);
|
|
179
736
|
return;
|
|
180
737
|
}
|
|
181
738
|
const pkg = positionals[0];
|
|
182
739
|
if (!pkg) {
|
|
183
|
-
fail(
|
|
740
|
+
ctx.out.fail(
|
|
741
|
+
"eject requires a package name, e.g. hogsend eject @hogsend/engine"
|
|
742
|
+
);
|
|
184
743
|
}
|
|
185
744
|
const consumerRoot = values.cwd ?? process.cwd();
|
|
186
745
|
const sourceDir = resolveSourceDir(pkg, consumerRoot);
|
|
746
|
+
if (!sourceDir) {
|
|
747
|
+
ctx.out.fail(
|
|
748
|
+
`cannot resolve ${pkg} from ${consumerRoot}. Is it installed? Run pnpm install first.`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
187
751
|
try {
|
|
188
|
-
const result = await
|
|
189
|
-
pkg
|
|
190
|
-
consumerRoot,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
752
|
+
const result = await ctx.out.step(
|
|
753
|
+
`Ejecting ${pkg}`,
|
|
754
|
+
() => eject({ pkg, consumerRoot, sourceDir, force: values.force })
|
|
755
|
+
);
|
|
756
|
+
if (ctx.json) {
|
|
757
|
+
ctx.out.json(result);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
ctx.out.note(
|
|
761
|
+
[
|
|
762
|
+
`copied ${result.copiedFiles} files -> ${result.vendorPath}`,
|
|
763
|
+
`dependency ${result.depSpecBefore} -> ${color.cyan(result.depSpecAfter)}`,
|
|
764
|
+
"",
|
|
765
|
+
`Now run: ${color.cyan(result.followUp)}`
|
|
766
|
+
].join("\n"),
|
|
767
|
+
`Ejected ${result.pkg}`
|
|
201
768
|
);
|
|
202
769
|
} catch (error) {
|
|
203
770
|
if (error instanceof EjectError) {
|
|
204
|
-
fail(error.message);
|
|
771
|
+
ctx.out.fail(error.message);
|
|
772
|
+
}
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
var ejectCommand = {
|
|
777
|
+
name: "eject",
|
|
778
|
+
summary: "Vendor a @hogsend/* package into vendor/<name>",
|
|
779
|
+
usage: usage3,
|
|
780
|
+
run: run3
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
// src/commands/events.ts
|
|
784
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
785
|
+
var usage4 = `hogsend events <userId> [options]
|
|
786
|
+
|
|
787
|
+
Stream the event history for a single user, newest first. Wraps
|
|
788
|
+
GET /v1/admin/events?userId=<userId>.
|
|
789
|
+
|
|
790
|
+
Arguments:
|
|
791
|
+
<userId> The user (distinct) id to fetch events for. Required.
|
|
792
|
+
|
|
793
|
+
Options:
|
|
794
|
+
--event <name> Filter to a single event name.
|
|
795
|
+
--from <iso> Only events at/after this ISO-8601 timestamp.
|
|
796
|
+
--to <iso> Only events at/before this ISO-8601 timestamp.
|
|
797
|
+
--limit <n> Max events to return (1-100, default 50).
|
|
798
|
+
--offset <n> Pagination offset (default 0).
|
|
799
|
+
--json Emit machine-readable JSON only.
|
|
800
|
+
-h, --help Show this help.
|
|
801
|
+
|
|
802
|
+
Examples:
|
|
803
|
+
hogsend events user_123
|
|
804
|
+
hogsend events user_123 --event signup --limit 10
|
|
805
|
+
hogsend events user_123 --from 2026-01-01T00:00:00Z --json`;
|
|
806
|
+
async function run4(ctx) {
|
|
807
|
+
const { values, positionals } = parseArgs4({
|
|
808
|
+
args: ctx.argv,
|
|
809
|
+
allowPositionals: true,
|
|
810
|
+
options: {
|
|
811
|
+
event: { type: "string" },
|
|
812
|
+
from: { type: "string" },
|
|
813
|
+
to: { type: "string" },
|
|
814
|
+
limit: { type: "string" },
|
|
815
|
+
offset: { type: "string" },
|
|
816
|
+
help: { type: "boolean", short: "h", default: false }
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
if (values.help) {
|
|
820
|
+
ctx.out.log(usage4);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const userId = positionals[0];
|
|
824
|
+
if (!userId) {
|
|
825
|
+
ctx.out.fail("events requires a userId, e.g. hogsend events user_123");
|
|
826
|
+
}
|
|
827
|
+
const limit = parseNumber(values.limit, "limit", ctx);
|
|
828
|
+
const offset = parseNumber(values.offset, "offset", ctx);
|
|
829
|
+
const query = {
|
|
830
|
+
userId,
|
|
831
|
+
event: values.event,
|
|
832
|
+
from: values.from,
|
|
833
|
+
to: values.to,
|
|
834
|
+
limit,
|
|
835
|
+
offset
|
|
836
|
+
};
|
|
837
|
+
let data;
|
|
838
|
+
try {
|
|
839
|
+
data = await ctx.out.step(
|
|
840
|
+
`Fetching events for ${userId}`,
|
|
841
|
+
() => ctx.http.get("/v1/admin/events", query)
|
|
842
|
+
);
|
|
843
|
+
} catch (error) {
|
|
844
|
+
if (isHttpError(error)) {
|
|
845
|
+
ctx.out.fail(error.message);
|
|
205
846
|
}
|
|
206
847
|
throw error;
|
|
207
848
|
}
|
|
849
|
+
if (ctx.json) {
|
|
850
|
+
ctx.out.json(data);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} events`);
|
|
854
|
+
if (data.events.length === 0) {
|
|
855
|
+
ctx.out.note(
|
|
856
|
+
`No events found for ${color.cyan(userId)}.`,
|
|
857
|
+
"Empty event stream"
|
|
858
|
+
);
|
|
859
|
+
ctx.out.outro(color.dim("Nothing to show."));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const rows = data.events.map((e) => ({
|
|
863
|
+
occurredAt: e.occurredAt,
|
|
864
|
+
event: e.event,
|
|
865
|
+
properties: summarizeProps(e.properties),
|
|
866
|
+
id: e.id
|
|
867
|
+
}));
|
|
868
|
+
ctx.out.table(rows, ["occurredAt", "event", "properties", "id"]);
|
|
869
|
+
const shown = data.events.length;
|
|
870
|
+
const through = data.offset + shown;
|
|
871
|
+
ctx.out.outro(
|
|
872
|
+
`${color.green(String(shown))} event${shown === 1 ? "" : "s"} ` + color.dim(`(${data.offset + 1}-${through} of ${data.total})`)
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
function parseNumber(raw, name, ctx) {
|
|
876
|
+
if (raw === void 0) return void 0;
|
|
877
|
+
const n = Number(raw);
|
|
878
|
+
if (!Number.isFinite(n)) {
|
|
879
|
+
ctx.out.fail(`--${name} must be a number, got "${raw}"`);
|
|
880
|
+
}
|
|
881
|
+
return n;
|
|
882
|
+
}
|
|
883
|
+
function summarizeProps(props) {
|
|
884
|
+
if (!props) return "";
|
|
885
|
+
const keys = Object.keys(props);
|
|
886
|
+
if (keys.length === 0) return "";
|
|
887
|
+
const preview = JSON.stringify(props);
|
|
888
|
+
return preview.length > 60 ? `${preview.slice(0, 57)}...` : preview;
|
|
889
|
+
}
|
|
890
|
+
var eventsCommand = {
|
|
891
|
+
name: "events",
|
|
892
|
+
summary: "Stream a single user's event history",
|
|
893
|
+
usage: usage4,
|
|
894
|
+
run: run4
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// src/commands/journeys.ts
|
|
898
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
899
|
+
var usage5 = `hogsend journeys <subcommand> [options]
|
|
900
|
+
|
|
901
|
+
Inspect and toggle journeys via the admin API (/v1/admin/journeys).
|
|
902
|
+
|
|
903
|
+
Subcommands:
|
|
904
|
+
list List journeys with status, trigger, and state counts.
|
|
905
|
+
get <id> Show one journey: trigger, exitOn, counts, recent states.
|
|
906
|
+
enable <id> Enable a journey (PATCH { enabled: true }).
|
|
907
|
+
disable <id> Disable a journey (PATCH { enabled: false }).
|
|
908
|
+
|
|
909
|
+
Options:
|
|
910
|
+
list:
|
|
911
|
+
--enabled <true|false> Filter by enabled state.
|
|
912
|
+
--limit <n> Page size (1-100, default 50).
|
|
913
|
+
--offset <n> Page offset (default 0).
|
|
914
|
+
--json Emit machine-readable JSON only.
|
|
915
|
+
-h, --help Show this help.
|
|
916
|
+
|
|
917
|
+
Examples:
|
|
918
|
+
hogsend journeys list --enabled true
|
|
919
|
+
hogsend journeys get activation-welcome --json
|
|
920
|
+
hogsend journeys disable churn-prevention`;
|
|
921
|
+
function badge2() {
|
|
922
|
+
return `${color.bgMagenta(color.black(" hogsend "))} journeys`;
|
|
923
|
+
}
|
|
924
|
+
function statusColor(enabled) {
|
|
925
|
+
return enabled ? color.green("enabled") : color.yellow("disabled");
|
|
926
|
+
}
|
|
927
|
+
async function runList2(ctx) {
|
|
928
|
+
const { values } = parseArgs5({
|
|
929
|
+
args: ctx.argv,
|
|
930
|
+
allowPositionals: true,
|
|
931
|
+
options: {
|
|
932
|
+
enabled: { type: "string" },
|
|
933
|
+
limit: { type: "string" },
|
|
934
|
+
offset: { type: "string" },
|
|
935
|
+
help: { type: "boolean", short: "h", default: false }
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
if (values.help) {
|
|
939
|
+
ctx.out.log(usage5);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (values.enabled !== void 0 && !["true", "false"].includes(values.enabled)) {
|
|
943
|
+
ctx.out.fail("--enabled must be 'true' or 'false'");
|
|
944
|
+
}
|
|
945
|
+
const query = {
|
|
946
|
+
enabled: values.enabled,
|
|
947
|
+
limit: values.limit,
|
|
948
|
+
offset: values.offset
|
|
949
|
+
};
|
|
950
|
+
if (!ctx.json) ctx.out.intro(badge2());
|
|
951
|
+
const data = await ctx.out.step(
|
|
952
|
+
"Fetching journeys",
|
|
953
|
+
() => ctx.http.get("/v1/admin/journeys", query)
|
|
954
|
+
);
|
|
955
|
+
if (ctx.json) {
|
|
956
|
+
ctx.out.json(data);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (data.journeys.length === 0) {
|
|
960
|
+
ctx.out.note("No journeys matched.", "Journeys");
|
|
961
|
+
} else {
|
|
962
|
+
ctx.out.table(
|
|
963
|
+
data.journeys.map((j) => ({
|
|
964
|
+
id: j.id,
|
|
965
|
+
name: j.name,
|
|
966
|
+
status: statusColor(j.enabled),
|
|
967
|
+
trigger: j.trigger.event,
|
|
968
|
+
active: j.counts.active,
|
|
969
|
+
waiting: j.counts.waiting,
|
|
970
|
+
completed: j.counts.completed,
|
|
971
|
+
failed: j.counts.failed
|
|
972
|
+
})),
|
|
973
|
+
[
|
|
974
|
+
"id",
|
|
975
|
+
"name",
|
|
976
|
+
"status",
|
|
977
|
+
"trigger",
|
|
978
|
+
"active",
|
|
979
|
+
"waiting",
|
|
980
|
+
"completed",
|
|
981
|
+
"failed"
|
|
982
|
+
]
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
ctx.out.outro(
|
|
986
|
+
`${data.journeys.length} of ${data.total} journey(s) \u2014 offset ${data.offset}, limit ${data.limit}`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
async function runGet2(ctx, id) {
|
|
990
|
+
if (!id) {
|
|
991
|
+
ctx.out.fail(
|
|
992
|
+
"journeys get requires a journey id, e.g. hogsend journeys get activation-welcome"
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
if (!ctx.json) ctx.out.intro(badge2());
|
|
996
|
+
const data = await ctx.out.step(
|
|
997
|
+
`Fetching journey ${id}`,
|
|
998
|
+
() => ctx.http.get(
|
|
999
|
+
`/v1/admin/journeys/${encodeURIComponent(id)}`
|
|
1000
|
+
)
|
|
1001
|
+
);
|
|
1002
|
+
if (ctx.json) {
|
|
1003
|
+
ctx.out.json(data);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const j = data.journey;
|
|
1007
|
+
ctx.out.kv(
|
|
1008
|
+
{
|
|
1009
|
+
id: j.id,
|
|
1010
|
+
name: j.name,
|
|
1011
|
+
description: j.description ?? "",
|
|
1012
|
+
status: statusColor(j.enabled),
|
|
1013
|
+
trigger: j.trigger.event,
|
|
1014
|
+
entryLimit: j.entryLimit,
|
|
1015
|
+
exitOn: j.exitOn?.map((e) => e.event).join(", ") ?? "(none)"
|
|
1016
|
+
},
|
|
1017
|
+
"Journey"
|
|
1018
|
+
);
|
|
1019
|
+
ctx.out.kv(
|
|
1020
|
+
{
|
|
1021
|
+
active: j.counts.active,
|
|
1022
|
+
waiting: j.counts.waiting,
|
|
1023
|
+
completed: j.counts.completed,
|
|
1024
|
+
failed: j.counts.failed,
|
|
1025
|
+
exited: j.counts.exited
|
|
1026
|
+
},
|
|
1027
|
+
"Counts"
|
|
1028
|
+
);
|
|
1029
|
+
if (j.recentStates.length === 0) {
|
|
1030
|
+
ctx.out.note("No recent journey instances.", "Recent states");
|
|
1031
|
+
} else {
|
|
1032
|
+
ctx.out.table(
|
|
1033
|
+
j.recentStates.map((s) => ({
|
|
1034
|
+
userId: s.userId,
|
|
1035
|
+
email: s.userEmail,
|
|
1036
|
+
status: s.status,
|
|
1037
|
+
node: s.currentNodeId,
|
|
1038
|
+
updatedAt: s.updatedAt
|
|
1039
|
+
})),
|
|
1040
|
+
["userId", "email", "status", "node", "updatedAt"]
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
ctx.out.outro(`Journey ${j.id} is ${j.enabled ? "enabled" : "disabled"}.`);
|
|
1044
|
+
}
|
|
1045
|
+
async function runToggle(ctx, id, enabled) {
|
|
1046
|
+
const verb = enabled ? "enable" : "disable";
|
|
1047
|
+
if (!id) {
|
|
1048
|
+
ctx.out.fail(
|
|
1049
|
+
`journeys ${verb} requires a journey id, e.g. hogsend journeys ${verb} activation-welcome`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
if (!ctx.json) ctx.out.intro(badge2());
|
|
1053
|
+
const data = await ctx.out.step(
|
|
1054
|
+
`${enabled ? "Enabling" : "Disabling"} ${id}`,
|
|
1055
|
+
() => ctx.http.patch(
|
|
1056
|
+
`/v1/admin/journeys/${encodeURIComponent(id)}`,
|
|
1057
|
+
{ enabled }
|
|
1058
|
+
)
|
|
1059
|
+
);
|
|
1060
|
+
if (ctx.json) {
|
|
1061
|
+
ctx.out.json(data);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const j = data.journey;
|
|
1065
|
+
ctx.out.note(
|
|
1066
|
+
[
|
|
1067
|
+
`${color.bold(j.name)} (${j.id})`,
|
|
1068
|
+
`status: ${statusColor(j.enabled)}`,
|
|
1069
|
+
`updated: ${j.updatedAt}`
|
|
1070
|
+
].join("\n"),
|
|
1071
|
+
`Journey ${enabled ? "enabled" : "disabled"}`
|
|
1072
|
+
);
|
|
1073
|
+
ctx.out.outro(`${j.id} is now ${statusColor(j.enabled)}.`);
|
|
1074
|
+
}
|
|
1075
|
+
async function run5(ctx) {
|
|
1076
|
+
const sub = ctx.argv[0];
|
|
1077
|
+
const rest = ctx.argv.slice(1);
|
|
1078
|
+
const subCtx = { ...ctx, argv: rest };
|
|
1079
|
+
try {
|
|
1080
|
+
switch (sub) {
|
|
1081
|
+
case "list":
|
|
1082
|
+
await runList2(subCtx);
|
|
1083
|
+
return;
|
|
1084
|
+
case "get": {
|
|
1085
|
+
const id = rest.find((a) => !a.startsWith("-"));
|
|
1086
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1087
|
+
ctx.out.log(usage5);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
await runGet2(subCtx, id);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
case "enable": {
|
|
1094
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1095
|
+
ctx.out.log(usage5);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
await runToggle(
|
|
1099
|
+
subCtx,
|
|
1100
|
+
rest.find((a) => !a.startsWith("-")),
|
|
1101
|
+
true
|
|
1102
|
+
);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
case "disable": {
|
|
1106
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1107
|
+
ctx.out.log(usage5);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
await runToggle(
|
|
1111
|
+
subCtx,
|
|
1112
|
+
rest.find((a) => !a.startsWith("-")),
|
|
1113
|
+
false
|
|
1114
|
+
);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
case void 0:
|
|
1118
|
+
ctx.out.fail(
|
|
1119
|
+
`journeys requires a subcommand (list|get|enable|disable). Run: hogsend journeys --help`
|
|
1120
|
+
);
|
|
1121
|
+
return;
|
|
1122
|
+
default:
|
|
1123
|
+
ctx.out.fail(
|
|
1124
|
+
`unknown journeys subcommand '${sub}'. Expected list|get|enable|disable.`
|
|
1125
|
+
);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (isHttpError(error)) {
|
|
1130
|
+
if (error.status === 404) {
|
|
1131
|
+
ctx.out.fail("journey not found");
|
|
1132
|
+
}
|
|
1133
|
+
ctx.out.fail(error.message);
|
|
1134
|
+
}
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
var journeysCommand = {
|
|
1139
|
+
name: "journeys",
|
|
1140
|
+
summary: "List, inspect, enable, and disable journeys",
|
|
1141
|
+
usage: usage5,
|
|
1142
|
+
run: run5
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
// src/commands/patch.ts
|
|
1146
|
+
import { spawnSync } from "child_process";
|
|
1147
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
1148
|
+
var usage6 = `hogsend patch <package> [--cwd <dir>]
|
|
1149
|
+
|
|
1150
|
+
Thin wrapper over pnpm's native patch flow. Runs \`pnpm patch <package>\`, which
|
|
1151
|
+
extracts the package into a temp dir and prints the path to edit. After editing,
|
|
1152
|
+
commit the patch with the command pnpm prints (\`pnpm patch-commit <dir>\`).
|
|
1153
|
+
|
|
1154
|
+
This does NOT replace scripts/patch-check.sh (the patch re-apply contract).
|
|
1155
|
+
|
|
1156
|
+
Options:
|
|
1157
|
+
--cwd <dir> Project root to run pnpm in (defaults to current directory).
|
|
1158
|
+
-h, --help Show this help.`;
|
|
1159
|
+
async function run6(ctx) {
|
|
1160
|
+
const { values, positionals } = parseArgs6({
|
|
1161
|
+
args: ctx.argv,
|
|
1162
|
+
allowPositionals: true,
|
|
1163
|
+
options: {
|
|
1164
|
+
cwd: { type: "string" },
|
|
1165
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
if (values.help) {
|
|
1169
|
+
ctx.out.log(usage6);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const pkg = positionals[0];
|
|
1173
|
+
if (!pkg) {
|
|
1174
|
+
ctx.out.fail(
|
|
1175
|
+
"patch requires a package name, e.g. hogsend patch @hogsend/engine"
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
const cwd = values.cwd ?? process.cwd();
|
|
1179
|
+
const result = spawnSync("pnpm", ["patch", pkg], {
|
|
1180
|
+
cwd,
|
|
1181
|
+
stdio: ctx.json ? "ignore" : "inherit"
|
|
1182
|
+
});
|
|
1183
|
+
if (ctx.json) {
|
|
1184
|
+
ctx.out.json({
|
|
1185
|
+
package: pkg,
|
|
1186
|
+
command: `pnpm patch ${pkg}`,
|
|
1187
|
+
status: result.status,
|
|
1188
|
+
ok: result.status === 0
|
|
1189
|
+
});
|
|
1190
|
+
if (result.status !== 0) process.exit(1);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (result.status !== 0) {
|
|
1194
|
+
ctx.out.fail(`pnpm patch ${pkg} exited with code ${result.status ?? "?"}`);
|
|
1195
|
+
}
|
|
1196
|
+
ctx.out.note(
|
|
1197
|
+
[
|
|
1198
|
+
"pnpm extracted the package to a temp dir (printed above).",
|
|
1199
|
+
"Edit the files, then commit the patch:",
|
|
1200
|
+
"",
|
|
1201
|
+
color.cyan("pnpm patch-commit <dir>")
|
|
1202
|
+
].join("\n"),
|
|
1203
|
+
"Next steps"
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
var patchCommand = {
|
|
1207
|
+
name: "patch",
|
|
1208
|
+
summary: "Patch a package via pnpm's native patch flow",
|
|
1209
|
+
usage: usage6,
|
|
1210
|
+
run: run6
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
// src/commands/setup.ts
|
|
1214
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1215
|
+
import { randomBytes } from "crypto";
|
|
1216
|
+
import { copyFileSync, existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
|
|
1217
|
+
import { join as join3 } from "path";
|
|
1218
|
+
import { parseArgs as parseArgs7 } from "util";
|
|
1219
|
+
import { confirm } from "@clack/prompts";
|
|
1220
|
+
|
|
1221
|
+
// src/lib/prompt.ts
|
|
1222
|
+
import { cancel as cancel2, isCancel } from "@clack/prompts";
|
|
1223
|
+
function bail(value) {
|
|
1224
|
+
if (isCancel(value)) {
|
|
1225
|
+
cancel2("Cancelled.");
|
|
1226
|
+
process.exit(0);
|
|
1227
|
+
}
|
|
1228
|
+
return value;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/commands/setup.ts
|
|
1232
|
+
var usage7 = `hogsend setup [--cwd <dir>] [--yes] [--json]
|
|
1233
|
+
|
|
1234
|
+
Interactive local onboarding for a scaffolded Hogsend app. Mirrors the
|
|
1235
|
+
create-hogsend "next steps":
|
|
1236
|
+
|
|
1237
|
+
1. docker compose up -d # Postgres + Redis + Hatchet-Lite
|
|
1238
|
+
2. cp .env.example .env (if missing)
|
|
1239
|
+
3. generate a BETTER_AUTH_SECRET (if still the placeholder)
|
|
1240
|
+
4. pnpm db:migrate # engine track then client track
|
|
1241
|
+
|
|
1242
|
+
Options:
|
|
1243
|
+
--cwd <dir> Project root to run in (defaults to the current directory).
|
|
1244
|
+
--yes, -y Skip confirmation prompts (assume yes). Implied by --json.
|
|
1245
|
+
--json Run non-interactively and emit a single JSON result document.
|
|
1246
|
+
-h, --help Show this help.
|
|
1247
|
+
|
|
1248
|
+
Run ${color.cyan("hogsend doctor")} afterwards to verify the instance is healthy.`;
|
|
1249
|
+
function generateSecret() {
|
|
1250
|
+
return randomBytes(32).toString("hex");
|
|
1251
|
+
}
|
|
1252
|
+
var SECRET_KEY = "BETTER_AUTH_SECRET";
|
|
1253
|
+
var PLACEHOLDER_PREFIX = "change-me";
|
|
1254
|
+
function ensureEnv(cwd) {
|
|
1255
|
+
const envPath = join3(cwd, ".env");
|
|
1256
|
+
const examplePath = join3(cwd, ".env.example");
|
|
1257
|
+
let copied;
|
|
1258
|
+
if (existsSync3(envPath)) {
|
|
1259
|
+
copied = {
|
|
1260
|
+
step: "env",
|
|
1261
|
+
status: "skipped",
|
|
1262
|
+
detail: ".env already exists"
|
|
1263
|
+
};
|
|
1264
|
+
} else if (existsSync3(examplePath)) {
|
|
1265
|
+
copyFileSync(examplePath, envPath);
|
|
1266
|
+
copied = {
|
|
1267
|
+
step: "env",
|
|
1268
|
+
status: "ok",
|
|
1269
|
+
detail: "copied .env.example -> .env"
|
|
1270
|
+
};
|
|
1271
|
+
} else {
|
|
1272
|
+
copied = {
|
|
1273
|
+
step: "env",
|
|
1274
|
+
status: "failed",
|
|
1275
|
+
detail: "no .env and no .env.example to copy from"
|
|
1276
|
+
};
|
|
1277
|
+
return {
|
|
1278
|
+
copied,
|
|
1279
|
+
secret: {
|
|
1280
|
+
step: "secret",
|
|
1281
|
+
status: "skipped",
|
|
1282
|
+
detail: "skipped \u2014 no .env"
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
let raw;
|
|
1287
|
+
try {
|
|
1288
|
+
raw = readFileSync(envPath, "utf8");
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
return {
|
|
1291
|
+
copied,
|
|
1292
|
+
secret: {
|
|
1293
|
+
step: "secret",
|
|
1294
|
+
status: "failed",
|
|
1295
|
+
detail: `could not read .env: ${err instanceof Error ? err.message : String(err)}`
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
const lines = raw.split(/\r?\n/);
|
|
1300
|
+
const idx = lines.findIndex(
|
|
1301
|
+
(l) => l.replace(/^export\s+/, "").trimStart().startsWith(`${SECRET_KEY}=`)
|
|
1302
|
+
);
|
|
1303
|
+
const existingLine = idx === -1 ? void 0 : lines[idx];
|
|
1304
|
+
const current = existingLine === void 0 ? void 0 : existingLine.slice(existingLine.indexOf("=") + 1).trim();
|
|
1305
|
+
const isPlaceholder = current === void 0 || current === "" || current.startsWith(PLACEHOLDER_PREFIX);
|
|
1306
|
+
if (!isPlaceholder) {
|
|
1307
|
+
return {
|
|
1308
|
+
copied,
|
|
1309
|
+
secret: {
|
|
1310
|
+
step: "secret",
|
|
1311
|
+
status: "skipped",
|
|
1312
|
+
detail: `${SECRET_KEY} already set`
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
const secret = generateSecret();
|
|
1317
|
+
const newLine = `${SECRET_KEY}=${secret}`;
|
|
1318
|
+
if (idx === -1) {
|
|
1319
|
+
if (raw.length > 0 && !raw.endsWith("\n")) lines.push("");
|
|
1320
|
+
lines.push(newLine);
|
|
1321
|
+
} else {
|
|
1322
|
+
lines[idx] = newLine;
|
|
1323
|
+
}
|
|
1324
|
+
writeFileSync(envPath, lines.join("\n"));
|
|
1325
|
+
return {
|
|
1326
|
+
copied,
|
|
1327
|
+
secret: {
|
|
1328
|
+
step: "secret",
|
|
1329
|
+
status: "ok",
|
|
1330
|
+
detail: `generated ${SECRET_KEY} (64-char hex)`
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function runCmd(cmd, args, cwd, json) {
|
|
1335
|
+
const result = spawnSync2(cmd, args, {
|
|
1336
|
+
cwd,
|
|
1337
|
+
// In json mode stay silent (we report structured status); otherwise stream
|
|
1338
|
+
// so the user sees docker / migration output inline.
|
|
1339
|
+
stdio: json ? "ignore" : "inherit"
|
|
1340
|
+
});
|
|
1341
|
+
return { status: result.status, ok: result.status === 0 };
|
|
1342
|
+
}
|
|
1343
|
+
async function run7(ctx) {
|
|
1344
|
+
const { values } = parseArgs7({
|
|
1345
|
+
args: ctx.argv,
|
|
1346
|
+
allowPositionals: true,
|
|
1347
|
+
options: {
|
|
1348
|
+
cwd: { type: "string" },
|
|
1349
|
+
yes: { type: "boolean", short: "y", default: false },
|
|
1350
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
if (values.help) {
|
|
1354
|
+
ctx.out.log(usage7);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
const cwd = values.cwd ?? process.cwd();
|
|
1358
|
+
if (!existsSync3(join3(cwd, "package.json"))) {
|
|
1359
|
+
ctx.out.fail(
|
|
1360
|
+
`no package.json in ${cwd} \u2014 run setup from a scaffolded Hogsend app (or pass --cwd).`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
const hasCompose = existsSync3(join3(cwd, "docker-compose.yml")) || existsSync3(join3(cwd, "docker-compose.yaml")) || existsSync3(join3(cwd, "compose.yml")) || existsSync3(join3(cwd, "compose.yaml"));
|
|
1364
|
+
const skipConfirm = ctx.json || values.yes;
|
|
1365
|
+
if (!ctx.json) {
|
|
1366
|
+
ctx.out.intro(
|
|
1367
|
+
`${color.bgMagenta(color.black(" hogsend "))} ${color.dim("local onboarding")}`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
if (ctx.out.interactive && !skipConfirm) {
|
|
1371
|
+
const proceed = bail(
|
|
1372
|
+
await confirm({
|
|
1373
|
+
message: `Set up local infra in ${color.cyan(cwd)}? (docker compose up, .env, db:migrate)`
|
|
1374
|
+
})
|
|
1375
|
+
);
|
|
1376
|
+
if (!proceed) {
|
|
1377
|
+
ctx.out.outro(color.dim("Nothing changed."));
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
const results = [];
|
|
1382
|
+
if (hasCompose) {
|
|
1383
|
+
const docker = await ctx.out.step(
|
|
1384
|
+
"Starting infra (docker compose up -d)",
|
|
1385
|
+
async () => runCmd("docker", ["compose", "up", "-d"], cwd, ctx.json)
|
|
1386
|
+
);
|
|
1387
|
+
results.push({
|
|
1388
|
+
step: "docker",
|
|
1389
|
+
status: docker.ok ? "ok" : "failed",
|
|
1390
|
+
detail: docker.ok ? "Postgres + Redis + Hatchet-Lite up" : `docker compose exited with code ${docker.status ?? "?"}`
|
|
1391
|
+
});
|
|
1392
|
+
} else {
|
|
1393
|
+
results.push({
|
|
1394
|
+
step: "docker",
|
|
1395
|
+
status: "skipped",
|
|
1396
|
+
detail: "no docker-compose file found"
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
const env = await ctx.out.step(
|
|
1400
|
+
"Preparing .env + auth secret",
|
|
1401
|
+
async () => ensureEnv(cwd)
|
|
1402
|
+
);
|
|
1403
|
+
results.push(env.copied, env.secret);
|
|
1404
|
+
const dockerFailed = results.some(
|
|
1405
|
+
(r) => r.step === "docker" && r.status === "failed"
|
|
1406
|
+
);
|
|
1407
|
+
if (dockerFailed) {
|
|
1408
|
+
results.push({
|
|
1409
|
+
step: "migrate",
|
|
1410
|
+
status: "skipped",
|
|
1411
|
+
detail: "skipped \u2014 docker compose failed; bring infra up then run pnpm db:migrate"
|
|
1412
|
+
});
|
|
1413
|
+
} else {
|
|
1414
|
+
const migrate = await ctx.out.step(
|
|
1415
|
+
"Running migrations (pnpm db:migrate)",
|
|
1416
|
+
async () => runCmd("pnpm", ["db:migrate"], cwd, ctx.json)
|
|
1417
|
+
);
|
|
1418
|
+
results.push({
|
|
1419
|
+
step: "migrate",
|
|
1420
|
+
status: migrate.ok ? "ok" : "failed",
|
|
1421
|
+
detail: migrate.ok ? "engine + client migrations applied" : `pnpm db:migrate exited with code ${migrate.status ?? "?"}`
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
1425
|
+
const ok = failed.length === 0;
|
|
1426
|
+
if (ctx.json) {
|
|
1427
|
+
ctx.out.json({
|
|
1428
|
+
ok,
|
|
1429
|
+
cwd,
|
|
1430
|
+
steps: results
|
|
1431
|
+
});
|
|
1432
|
+
if (!ok) process.exit(1);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
ctx.out.table(
|
|
1436
|
+
results.map((r) => ({
|
|
1437
|
+
step: r.step,
|
|
1438
|
+
status: r.status === "ok" ? color.green("ok") : r.status === "skipped" ? color.dim("skipped") : color.red("failed"),
|
|
1439
|
+
detail: r.detail
|
|
1440
|
+
})),
|
|
1441
|
+
["step", "status", "detail"]
|
|
1442
|
+
);
|
|
1443
|
+
ctx.out.note(
|
|
1444
|
+
[
|
|
1445
|
+
`${color.cyan("pnpm dev")} ${color.dim("# HTTP API on :3002")}`,
|
|
1446
|
+
`${color.cyan("pnpm worker:dev")} ${color.dim("# Hatchet worker, 2nd terminal")}`,
|
|
1447
|
+
"",
|
|
1448
|
+
`${color.dim("Verify with")} ${color.cyan("hogsend doctor")}${color.dim(".")}`,
|
|
1449
|
+
`${color.dim("Grab HATCHET_CLIENT_TOKEN at")} ${color.cyan("http://localhost:8888")} ${color.dim("and set it in .env.")}`
|
|
1450
|
+
].join("\n"),
|
|
1451
|
+
"Next steps"
|
|
1452
|
+
);
|
|
1453
|
+
if (!ok) {
|
|
1454
|
+
ctx.out.fail(
|
|
1455
|
+
`${failed.length} step(s) failed \u2014 see the table above. Fix and re-run hogsend setup.`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
ctx.out.outro(
|
|
1459
|
+
`${color.green("Done.")} ${color.dim("Local infra is up \u2014 go write a journey.")}`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
var setupCommand = {
|
|
1463
|
+
name: "setup",
|
|
1464
|
+
summary: "Local onboarding: docker compose up, gen secret, db:migrate",
|
|
1465
|
+
usage: usage7,
|
|
1466
|
+
run: run7
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
// src/commands/skills.ts
|
|
1470
|
+
import {
|
|
1471
|
+
cpSync,
|
|
1472
|
+
existsSync as existsSync4,
|
|
1473
|
+
mkdirSync,
|
|
1474
|
+
readdirSync,
|
|
1475
|
+
readFileSync as readFileSync2,
|
|
1476
|
+
statSync
|
|
1477
|
+
} from "fs";
|
|
1478
|
+
import { join as join4 } from "path";
|
|
1479
|
+
import { fileURLToPath } from "url";
|
|
1480
|
+
import { parseArgs as parseArgs8 } from "util";
|
|
1481
|
+
import { multiselect } from "@clack/prompts";
|
|
1482
|
+
var usage8 = `hogsend skills <subcommand> [options]
|
|
1483
|
+
|
|
1484
|
+
Manage the Claude Code skills bundled with @hogsend/cli. Bundled skills teach
|
|
1485
|
+
agents how to drive the hogsend CLI; \`add\` copies them into your project's
|
|
1486
|
+
./.claude/skills/<name>/ so Claude Code can discover them.
|
|
1487
|
+
|
|
1488
|
+
Subcommands:
|
|
1489
|
+
list List bundled skills + whether each is installed.
|
|
1490
|
+
add [name] [--force] Copy a bundled skill into ./.claude/skills/<name>/.
|
|
1491
|
+
Omit name for an interactive multiselect (human),
|
|
1492
|
+
or copy all bundled skills (--json / non-interactive).
|
|
1493
|
+
|
|
1494
|
+
Options:
|
|
1495
|
+
--force Overwrite an already-installed skill.
|
|
1496
|
+
--json Emit machine-readable JSON only (implies non-interactive).
|
|
1497
|
+
-h, --help Show this help.
|
|
1498
|
+
|
|
1499
|
+
Examples:
|
|
1500
|
+
hogsend skills list
|
|
1501
|
+
hogsend skills list --json
|
|
1502
|
+
hogsend skills add
|
|
1503
|
+
hogsend skills add hogsend-cli --force`;
|
|
1504
|
+
function bundledSkillsDir() {
|
|
1505
|
+
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
1506
|
+
}
|
|
1507
|
+
function installDir(cwd) {
|
|
1508
|
+
return join4(cwd, ".claude", "skills");
|
|
1509
|
+
}
|
|
1510
|
+
function readFrontmatterField(skillDir, field) {
|
|
1511
|
+
const skillFile = join4(skillDir, "SKILL.md");
|
|
1512
|
+
if (!existsSync4(skillFile)) return "";
|
|
1513
|
+
const raw = readFileSyncSafe(skillFile);
|
|
1514
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
1515
|
+
if (!fmMatch) return "";
|
|
1516
|
+
const block = fmMatch[1] ?? "";
|
|
1517
|
+
for (const line of block.split("\n")) {
|
|
1518
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
1519
|
+
if (m && m[1] === field) {
|
|
1520
|
+
return (m[2] ?? "").replace(/^["']|["']$/g, "").trim();
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return "";
|
|
1524
|
+
}
|
|
1525
|
+
function readFileSyncSafe(path) {
|
|
1526
|
+
try {
|
|
1527
|
+
return readFileSync2(path, "utf8");
|
|
1528
|
+
} catch {
|
|
1529
|
+
return "";
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function listBundledSkills(cwd) {
|
|
1533
|
+
const dir = bundledSkillsDir();
|
|
1534
|
+
if (!existsSync4(dir)) return [];
|
|
1535
|
+
const target = installDir(cwd);
|
|
1536
|
+
const entries = readdirSync(dir).filter((name) => {
|
|
1537
|
+
const full = join4(dir, name);
|
|
1538
|
+
return statSync(full).isDirectory() && existsSync4(join4(full, "SKILL.md"));
|
|
1539
|
+
});
|
|
1540
|
+
return entries.sort().map((name) => ({
|
|
1541
|
+
name,
|
|
1542
|
+
description: readFrontmatterField(join4(dir, name), "description"),
|
|
1543
|
+
installed: existsSync4(join4(target, name))
|
|
1544
|
+
}));
|
|
1545
|
+
}
|
|
1546
|
+
function runList3(ctx) {
|
|
1547
|
+
const skills = listBundledSkills(process.cwd());
|
|
1548
|
+
if (ctx.json) {
|
|
1549
|
+
ctx.out.json({
|
|
1550
|
+
bundledSkillsDir: bundledSkillsDir(),
|
|
1551
|
+
installDir: installDir(process.cwd()),
|
|
1552
|
+
skills
|
|
1553
|
+
});
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} skills`);
|
|
1557
|
+
if (skills.length === 0) {
|
|
1558
|
+
ctx.out.note(
|
|
1559
|
+
"No bundled skills found in this package build.",
|
|
1560
|
+
"skills list"
|
|
1561
|
+
);
|
|
1562
|
+
ctx.out.outro("Nothing to install.");
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
ctx.out.table(
|
|
1566
|
+
skills.map((s) => ({
|
|
1567
|
+
name: s.name,
|
|
1568
|
+
installed: s.installed ? color.green("yes") : color.dim("no"),
|
|
1569
|
+
description: s.description.length > 60 ? `${s.description.slice(0, 57)}...` : s.description
|
|
1570
|
+
})),
|
|
1571
|
+
["name", "installed", "description"]
|
|
1572
|
+
);
|
|
1573
|
+
ctx.out.outro(
|
|
1574
|
+
`Install with ${color.cyan("hogsend skills add <name>")} (or just ${color.cyan("hogsend skills add")}).`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
function copySkill(name, cwd, force) {
|
|
1578
|
+
const src = join4(bundledSkillsDir(), name);
|
|
1579
|
+
const dest = join4(installDir(cwd), name);
|
|
1580
|
+
const exists = existsSync4(dest);
|
|
1581
|
+
if (exists && !force) {
|
|
1582
|
+
return { name, installed: false, skipped: true, path: dest };
|
|
1583
|
+
}
|
|
1584
|
+
mkdirSync(installDir(cwd), { recursive: true });
|
|
1585
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
1586
|
+
return { name, installed: true, skipped: false, path: dest };
|
|
1587
|
+
}
|
|
1588
|
+
async function runAdd(ctx, argv) {
|
|
1589
|
+
const { values, positionals } = parseArgs8({
|
|
1590
|
+
args: argv,
|
|
1591
|
+
allowPositionals: true,
|
|
1592
|
+
options: {
|
|
1593
|
+
force: { type: "boolean", default: false },
|
|
1594
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
if (values.help) {
|
|
1598
|
+
ctx.out.log(usage8);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const cwd = process.cwd();
|
|
1602
|
+
const bundled = listBundledSkills(cwd);
|
|
1603
|
+
if (bundled.length === 0) {
|
|
1604
|
+
ctx.out.fail("no bundled skills found in this package build");
|
|
1605
|
+
}
|
|
1606
|
+
const requested = positionals[0];
|
|
1607
|
+
const force = Boolean(values.force);
|
|
1608
|
+
let names;
|
|
1609
|
+
if (requested) {
|
|
1610
|
+
const match = bundled.find((s) => s.name === requested);
|
|
1611
|
+
if (!match) {
|
|
1612
|
+
ctx.out.fail(
|
|
1613
|
+
`unknown skill "${requested}". Available: ${bundled.map((s) => s.name).join(", ")}`
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
names = [requested];
|
|
1617
|
+
} else if (ctx.out.interactive) {
|
|
1618
|
+
const picked = bail(
|
|
1619
|
+
await multiselect({
|
|
1620
|
+
message: "Which skills do you want to install?",
|
|
1621
|
+
options: bundled.map((s) => ({
|
|
1622
|
+
value: s.name,
|
|
1623
|
+
label: s.name,
|
|
1624
|
+
hint: s.installed ? "installed" : void 0
|
|
1625
|
+
})),
|
|
1626
|
+
required: true
|
|
1627
|
+
})
|
|
1628
|
+
);
|
|
1629
|
+
names = picked;
|
|
1630
|
+
} else {
|
|
1631
|
+
names = bundled.map((s) => s.name);
|
|
1632
|
+
}
|
|
1633
|
+
const results = names.map((name) => copySkill(name, cwd, force));
|
|
1634
|
+
if (ctx.json) {
|
|
1635
|
+
ctx.out.json({
|
|
1636
|
+
installDir: installDir(cwd),
|
|
1637
|
+
force,
|
|
1638
|
+
results
|
|
1639
|
+
});
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} skills add`);
|
|
1643
|
+
for (const r of results) {
|
|
1644
|
+
if (r.skipped) {
|
|
1645
|
+
ctx.out.log(
|
|
1646
|
+
`${color.yellow("skip")} ${r.name} ${color.dim("(already installed; use --force to overwrite)")}`
|
|
1647
|
+
);
|
|
1648
|
+
} else {
|
|
1649
|
+
ctx.out.log(`${color.green("\u2713")} ${r.name} ${color.dim(`-> ${r.path}`)}`);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const installedCount = results.filter((r) => r.installed).length;
|
|
1653
|
+
const skippedCount = results.filter((r) => r.skipped).length;
|
|
1654
|
+
ctx.out.outro(
|
|
1655
|
+
`Installed ${installedCount} skill${installedCount === 1 ? "" : "s"}` + (skippedCount > 0 ? `, skipped ${skippedCount}.` : ".")
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
async function run8(ctx) {
|
|
1659
|
+
const sub = ctx.argv[0];
|
|
1660
|
+
switch (sub) {
|
|
1661
|
+
case "list":
|
|
1662
|
+
runList3(ctx);
|
|
1663
|
+
return;
|
|
1664
|
+
case "add":
|
|
1665
|
+
await runAdd(ctx, ctx.argv.slice(1));
|
|
1666
|
+
return;
|
|
1667
|
+
case void 0:
|
|
1668
|
+
case "-h":
|
|
1669
|
+
case "--help":
|
|
1670
|
+
ctx.out.log(usage8);
|
|
1671
|
+
return;
|
|
1672
|
+
default:
|
|
1673
|
+
ctx.out.fail(
|
|
1674
|
+
`unknown skills subcommand "${sub}". Use: list | add. See hogsend skills --help.`
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
var skillsCommand = {
|
|
1679
|
+
name: "skills",
|
|
1680
|
+
summary: "List + install bundled Claude Code skills into .claude/skills",
|
|
1681
|
+
usage: usage8,
|
|
1682
|
+
run: run8
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
// src/commands/stats.ts
|
|
1686
|
+
import { parseArgs as parseArgs9 } from "util";
|
|
1687
|
+
var usage9 = `hogsend stats [--json]
|
|
1688
|
+
|
|
1689
|
+
Show system-wide overview metrics from a running Hogsend instance.
|
|
1690
|
+
Wraps GET /v1/admin/metrics/overview.
|
|
1691
|
+
|
|
1692
|
+
Fields:
|
|
1693
|
+
totalContacts Live (non-deleted) contacts.
|
|
1694
|
+
activeJourneys Journey states currently active or waiting.
|
|
1695
|
+
emailsSent24h Emails sent in the last 24 hours.
|
|
1696
|
+
emailsSent7d Emails sent in the last 7 days.
|
|
1697
|
+
emailsSent30d Emails sent in the last 30 days.
|
|
1698
|
+
bounceRate30d Bounced / sent over the last 30 days (0..1).
|
|
1699
|
+
unsubscribeRate Unsubscribed / total preferences (0..1).
|
|
1700
|
+
|
|
1701
|
+
Options:
|
|
1702
|
+
--url <baseUrl> API base URL (default HOGSEND_API_URL or http://localhost:3002).
|
|
1703
|
+
--admin-key <key> Admin bearer key (default HOGSEND_ADMIN_KEY / ADMIN_API_KEY).
|
|
1704
|
+
--json Emit machine-readable JSON only.
|
|
1705
|
+
-h, --help Show this help.`;
|
|
1706
|
+
function pct(rate) {
|
|
1707
|
+
return `${(rate * 100).toFixed(2)}%`;
|
|
1708
|
+
}
|
|
1709
|
+
async function run9(ctx) {
|
|
1710
|
+
const { values } = parseArgs9({
|
|
1711
|
+
args: ctx.argv,
|
|
1712
|
+
allowPositionals: true,
|
|
1713
|
+
options: {
|
|
1714
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
if (values.help) {
|
|
1718
|
+
ctx.out.log(usage9);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
const metrics = await ctx.out.step(
|
|
1722
|
+
"Fetching overview metrics",
|
|
1723
|
+
() => ctx.http.get("/v1/admin/metrics/overview")
|
|
1724
|
+
);
|
|
1725
|
+
if (ctx.json) {
|
|
1726
|
+
ctx.out.json(metrics);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} stats`);
|
|
1730
|
+
ctx.out.kv(
|
|
1731
|
+
{
|
|
1732
|
+
"Total contacts": metrics.totalContacts,
|
|
1733
|
+
"Active journeys": metrics.activeJourneys,
|
|
1734
|
+
"Emails sent (24h)": metrics.emailsSent24h,
|
|
1735
|
+
"Emails sent (7d)": metrics.emailsSent7d,
|
|
1736
|
+
"Emails sent (30d)": metrics.emailsSent30d,
|
|
1737
|
+
"Bounce rate (30d)": pct(metrics.bounceRate30d),
|
|
1738
|
+
"Unsubscribe rate": pct(metrics.unsubscribeRate)
|
|
1739
|
+
},
|
|
1740
|
+
"Overview"
|
|
1741
|
+
);
|
|
1742
|
+
ctx.out.outro(color.dim(ctx.http.cfg.baseUrl));
|
|
1743
|
+
}
|
|
1744
|
+
var statsCommand = {
|
|
1745
|
+
name: "stats",
|
|
1746
|
+
summary: "Show system-wide overview metrics",
|
|
1747
|
+
usage: usage9,
|
|
1748
|
+
run: run9
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
// src/commands/index.ts
|
|
1752
|
+
var commands = [
|
|
1753
|
+
doctorCommand,
|
|
1754
|
+
journeysCommand,
|
|
1755
|
+
contactsCommand,
|
|
1756
|
+
statsCommand,
|
|
1757
|
+
eventsCommand,
|
|
1758
|
+
setupCommand,
|
|
1759
|
+
skillsCommand,
|
|
1760
|
+
ejectCommand,
|
|
1761
|
+
patchCommand
|
|
1762
|
+
];
|
|
1763
|
+
|
|
1764
|
+
// src/lib/config.ts
|
|
1765
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
1766
|
+
import { join as join5 } from "path";
|
|
1767
|
+
import { parseArgs as parseArgs10 } from "util";
|
|
1768
|
+
var DEFAULT_BASE_URL = "http://localhost:3002";
|
|
1769
|
+
function parseGlobalFlags(argv) {
|
|
1770
|
+
const { values, tokens } = parseArgs10({
|
|
1771
|
+
args: argv,
|
|
1772
|
+
allowPositionals: true,
|
|
1773
|
+
strict: false,
|
|
1774
|
+
tokens: true,
|
|
1775
|
+
options: {
|
|
1776
|
+
url: { type: "string" },
|
|
1777
|
+
"admin-key": { type: "string" },
|
|
1778
|
+
json: { type: "boolean", default: false },
|
|
1779
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
const owned = /* @__PURE__ */ new Set(["url", "admin-key", "json", "help", "h"]);
|
|
1783
|
+
const rest = [];
|
|
1784
|
+
for (const token of tokens) {
|
|
1785
|
+
if (token.kind === "positional") {
|
|
1786
|
+
rest.push(token.value);
|
|
1787
|
+
} else if (token.kind === "option") {
|
|
1788
|
+
if (owned.has(token.name)) continue;
|
|
1789
|
+
rest.push(token.rawName);
|
|
1790
|
+
if (token.value !== void 0 && !token.inlineValue) {
|
|
1791
|
+
rest.push(token.value);
|
|
1792
|
+
} else if (token.inlineValue && token.value !== void 0) {
|
|
1793
|
+
rest[rest.length - 1] = `${token.rawName}=${token.value}`;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return {
|
|
1798
|
+
url: typeof values.url === "string" ? values.url : void 0,
|
|
1799
|
+
adminKey: typeof values["admin-key"] === "string" ? values["admin-key"] : void 0,
|
|
1800
|
+
json: values.json === true,
|
|
1801
|
+
help: values.help === true,
|
|
1802
|
+
rest
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
function loadDotEnv(cwd = process.cwd()) {
|
|
1806
|
+
const out = {};
|
|
1807
|
+
const file = join5(cwd, ".env");
|
|
1808
|
+
if (!existsSync5(file)) return out;
|
|
1809
|
+
let raw;
|
|
1810
|
+
try {
|
|
1811
|
+
raw = readFileSync3(file, "utf8");
|
|
1812
|
+
} catch {
|
|
1813
|
+
return out;
|
|
1814
|
+
}
|
|
1815
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
1816
|
+
const line = rawLine.trim();
|
|
1817
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
1818
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length) : line;
|
|
1819
|
+
const eq = withoutExport.indexOf("=");
|
|
1820
|
+
if (eq === -1) continue;
|
|
1821
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
1822
|
+
if (key === "") continue;
|
|
1823
|
+
let value = withoutExport.slice(eq + 1).trim();
|
|
1824
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1825
|
+
value = value.slice(1, -1);
|
|
1826
|
+
}
|
|
1827
|
+
out[key] = value;
|
|
1828
|
+
}
|
|
1829
|
+
return out;
|
|
1830
|
+
}
|
|
1831
|
+
function resolveConfig(flags, cwd = process.cwd()) {
|
|
1832
|
+
const dotenv = loadDotEnv(cwd);
|
|
1833
|
+
const baseUrlRaw = flags.url ?? process.env.HOGSEND_API_URL ?? dotenv.HOGSEND_API_URL ?? DEFAULT_BASE_URL;
|
|
1834
|
+
const adminKey = flags.adminKey ?? process.env.HOGSEND_ADMIN_KEY ?? process.env.ADMIN_API_KEY ?? dotenv.HOGSEND_ADMIN_KEY ?? dotenv.ADMIN_API_KEY;
|
|
1835
|
+
return {
|
|
1836
|
+
baseUrl: baseUrlRaw.replace(/\/+$/, ""),
|
|
1837
|
+
adminKey: adminKey && adminKey.length > 0 ? adminKey : void 0
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/bin.ts
|
|
1842
|
+
function version() {
|
|
1843
|
+
try {
|
|
1844
|
+
const require2 = createRequire2(import.meta.url);
|
|
1845
|
+
const pkg = require2("../package.json");
|
|
1846
|
+
return pkg.version ?? "0.0.0";
|
|
1847
|
+
} catch {
|
|
1848
|
+
return "0.0.0";
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function rootUsage() {
|
|
1852
|
+
const longest = commands.reduce((n, c) => Math.max(n, c.name.length), 0);
|
|
1853
|
+
const list = commands.map((c) => ` ${color.cyan(c.name.padEnd(longest))} ${c.summary}`).join("\n");
|
|
1854
|
+
return `${color.bold("hogsend")} \u2014 the agent-native Hogsend CLI
|
|
1855
|
+
|
|
1856
|
+
${color.dim("Usage:")} hogsend <command> [options]
|
|
1857
|
+
|
|
1858
|
+
${color.dim("Commands:")}
|
|
1859
|
+
${list}
|
|
1860
|
+
|
|
1861
|
+
${color.dim("Global options:")}
|
|
1862
|
+
--url <baseUrl> Target instance (default HOGSEND_API_URL or http://localhost:3002)
|
|
1863
|
+
--admin-key <key> Admin bearer token (default HOGSEND_ADMIN_KEY / ADMIN_API_KEY)
|
|
1864
|
+
--json Emit machine-readable JSON only (for agents)
|
|
1865
|
+
-h, --help Show help (use after a command for command help)
|
|
1866
|
+
-v, --version Show version
|
|
1867
|
+
|
|
1868
|
+
Run ${color.cyan("hogsend <command> --help")} for command-specific options.`;
|
|
1869
|
+
}
|
|
1870
|
+
function findCommand(name) {
|
|
1871
|
+
return commands.find((c) => c.name === name);
|
|
208
1872
|
}
|
|
209
1873
|
async function main() {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
1874
|
+
const argv = process.argv.slice(2);
|
|
1875
|
+
const [token, ...afterToken] = argv;
|
|
1876
|
+
if (token === "-v" || token === "--version") {
|
|
1877
|
+
process.stdout.write(`${version()}
|
|
213
1878
|
`);
|
|
214
1879
|
return;
|
|
215
1880
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
1881
|
+
if (!token || token === "-h" || token === "--help") {
|
|
1882
|
+
process.stdout.write(`${rootUsage()}
|
|
1883
|
+
`);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
const command = findCommand(token);
|
|
1887
|
+
if (!command) {
|
|
1888
|
+
process.stderr.write(
|
|
1889
|
+
`${color.red("error")} unknown command "${token}"
|
|
222
1890
|
|
|
223
|
-
${
|
|
1891
|
+
${rootUsage()}
|
|
1892
|
+
`
|
|
1893
|
+
);
|
|
1894
|
+
process.exit(1);
|
|
1895
|
+
}
|
|
1896
|
+
const flags = parseGlobalFlags(afterToken);
|
|
1897
|
+
const out = createOutput({ json: flags.json });
|
|
1898
|
+
if (flags.help) {
|
|
1899
|
+
out.log(command.usage);
|
|
1900
|
+
return;
|
|
224
1901
|
}
|
|
1902
|
+
const cfg = resolveConfig(flags);
|
|
1903
|
+
const http = createAdminClient(cfg);
|
|
1904
|
+
await command.run({
|
|
1905
|
+
argv: flags.rest,
|
|
1906
|
+
cfg,
|
|
1907
|
+
http,
|
|
1908
|
+
out,
|
|
1909
|
+
json: flags.json
|
|
1910
|
+
});
|
|
225
1911
|
}
|
|
226
1912
|
main().catch((error) => {
|
|
227
|
-
|
|
1913
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1914
|
+
if (process.argv.includes("--json")) {
|
|
1915
|
+
process.stdout.write(`${JSON.stringify({ error: msg })}
|
|
1916
|
+
`);
|
|
1917
|
+
} else {
|
|
1918
|
+
process.stderr.write(`${color.red("error")} ${msg}
|
|
228
1919
|
`);
|
|
1920
|
+
}
|
|
229
1921
|
process.exit(1);
|
|
230
1922
|
});
|
|
231
1923
|
//# sourceMappingURL=bin.js.map
|