@hookstream/cli 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/index.js +1976 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1976 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command50 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/output.ts
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
var jsonMode = false;
|
|
9
|
+
function setJsonMode(enabled) {
|
|
10
|
+
jsonMode = enabled;
|
|
11
|
+
}
|
|
12
|
+
function isJsonMode() {
|
|
13
|
+
return jsonMode;
|
|
14
|
+
}
|
|
15
|
+
function printJson(data) {
|
|
16
|
+
console.log(JSON.stringify(data, null, 2));
|
|
17
|
+
}
|
|
18
|
+
function printTable(rows, columns) {
|
|
19
|
+
if (rows.length === 0) {
|
|
20
|
+
console.log(chalk.dim(" No results."));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const widths = columns.map((col) => {
|
|
24
|
+
const maxData = rows.reduce((max, row) => Math.max(max, (row[col.key] || "").length), 0);
|
|
25
|
+
return col.width || Math.max(col.label.length, Math.min(maxData, 40));
|
|
26
|
+
});
|
|
27
|
+
const header = columns.map((col, i) => chalk.bold(col.label.padEnd(widths[i]))).join(" ");
|
|
28
|
+
console.log(header);
|
|
29
|
+
console.log(chalk.dim("\u2500".repeat(header.length)));
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
const line = columns.map((col, i) => {
|
|
32
|
+
const val = row[col.key] || "";
|
|
33
|
+
return val.length > widths[i] ? val.slice(0, widths[i] - 1) + "\u2026" : val.padEnd(widths[i]);
|
|
34
|
+
}).join(" ");
|
|
35
|
+
console.log(line);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function printSuccess(msg) {
|
|
39
|
+
console.log(chalk.green("\u2713") + " " + msg);
|
|
40
|
+
}
|
|
41
|
+
function printError(msg) {
|
|
42
|
+
console.error(chalk.red("Error:") + " " + msg);
|
|
43
|
+
}
|
|
44
|
+
function printKeyValue(pairs) {
|
|
45
|
+
const maxLabel = pairs.reduce((max, [label]) => Math.max(max, label.length), 0);
|
|
46
|
+
for (const [label, value] of pairs) {
|
|
47
|
+
console.log(` ${chalk.dim(label.padEnd(maxLabel))} ${value ?? chalk.dim("\u2014")}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function formatDate(iso) {
|
|
51
|
+
if (!iso) return "\u2014";
|
|
52
|
+
const d = new Date(iso);
|
|
53
|
+
return d.toLocaleString();
|
|
54
|
+
}
|
|
55
|
+
function formatBytes(bytes) {
|
|
56
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
57
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
58
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
59
|
+
}
|
|
60
|
+
function statusColor(status) {
|
|
61
|
+
switch (status) {
|
|
62
|
+
case "active":
|
|
63
|
+
case "verified":
|
|
64
|
+
case "unique":
|
|
65
|
+
case "received":
|
|
66
|
+
return chalk.green(status);
|
|
67
|
+
case "paused":
|
|
68
|
+
case "skipped":
|
|
69
|
+
return chalk.yellow(status);
|
|
70
|
+
case "disabled":
|
|
71
|
+
case "failed":
|
|
72
|
+
case "duplicate":
|
|
73
|
+
case "error":
|
|
74
|
+
case "dlq":
|
|
75
|
+
return chalk.red(status);
|
|
76
|
+
case "success":
|
|
77
|
+
return chalk.green(status);
|
|
78
|
+
case "timeout":
|
|
79
|
+
return chalk.yellow(status);
|
|
80
|
+
default:
|
|
81
|
+
return status;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/commands/login.ts
|
|
86
|
+
import { createInterface } from "readline/promises";
|
|
87
|
+
import { stdin, stdout } from "process";
|
|
88
|
+
import { Command } from "commander";
|
|
89
|
+
|
|
90
|
+
// src/config.ts
|
|
91
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
92
|
+
import { homedir } from "os";
|
|
93
|
+
import { join } from "path";
|
|
94
|
+
var CONFIG_DIR = join(homedir(), ".config", "hookstream");
|
|
95
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
96
|
+
function loadConfig() {
|
|
97
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
100
|
+
} catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function saveConfig(config) {
|
|
105
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
106
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
107
|
+
}
|
|
108
|
+
function clearConfig() {
|
|
109
|
+
if (existsSync(CONFIG_FILE)) {
|
|
110
|
+
unlinkSync(CONFIG_FILE);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function getConfigPath() {
|
|
114
|
+
return CONFIG_FILE;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/client.ts
|
|
118
|
+
var DEFAULT_BASE_URL = "https://hookstream.io";
|
|
119
|
+
var hookstreamClient = class {
|
|
120
|
+
apiKey;
|
|
121
|
+
baseUrl;
|
|
122
|
+
constructor(opts = {}) {
|
|
123
|
+
const config = loadConfig();
|
|
124
|
+
this.apiKey = opts.apiKey || process.env.HOOKSTREAM_API_KEY || config.api_key || "";
|
|
125
|
+
this.baseUrl = (opts.baseUrl || process.env.HOOKSTREAM_BASE_URL || config.base_url || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
126
|
+
if (!this.apiKey) {
|
|
127
|
+
throw new Error("Not authenticated. Run `hookstream login` or set HOOKSTREAM_API_KEY.");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
getBaseUrl() {
|
|
131
|
+
return this.baseUrl;
|
|
132
|
+
}
|
|
133
|
+
getApiKeyPrefix() {
|
|
134
|
+
return this.apiKey.slice(0, 16) + "...";
|
|
135
|
+
}
|
|
136
|
+
async get(path) {
|
|
137
|
+
return this.request("GET", path);
|
|
138
|
+
}
|
|
139
|
+
async post(path, body) {
|
|
140
|
+
return this.request("POST", path, body);
|
|
141
|
+
}
|
|
142
|
+
async patch(path, body) {
|
|
143
|
+
return this.request("PATCH", path, body);
|
|
144
|
+
}
|
|
145
|
+
async del(path) {
|
|
146
|
+
return this.request("DELETE", path);
|
|
147
|
+
}
|
|
148
|
+
async request(method, path, body) {
|
|
149
|
+
const url = `${this.baseUrl}/v1${path}`;
|
|
150
|
+
const headers = {
|
|
151
|
+
"X-API-Key": this.apiKey,
|
|
152
|
+
"Accept": "application/json"
|
|
153
|
+
};
|
|
154
|
+
if (body !== void 0) {
|
|
155
|
+
headers["Content-Type"] = "application/json";
|
|
156
|
+
}
|
|
157
|
+
const res = await fetch(url, {
|
|
158
|
+
method,
|
|
159
|
+
headers,
|
|
160
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
161
|
+
});
|
|
162
|
+
const json = await res.json();
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
const err = json;
|
|
165
|
+
throw new Error(err.error || err.message || `HTTP ${res.status}`);
|
|
166
|
+
}
|
|
167
|
+
return json;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
async function publicRequest(baseUrl, path, opts = {}) {
|
|
171
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
172
|
+
return fetch(url, {
|
|
173
|
+
method: opts.method || "POST",
|
|
174
|
+
headers: opts.headers,
|
|
175
|
+
body: opts.body
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/commands/login.ts
|
|
180
|
+
var loginCommand = new Command("login").description("Authenticate with an API key").option("--api-key <key>", "API key (or enter interactively)").option("--base-url <url>", "Override base URL").action(async (opts) => {
|
|
181
|
+
let apiKey = opts.apiKey;
|
|
182
|
+
if (!apiKey) {
|
|
183
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
184
|
+
apiKey = await rl.question("Enter your API key: ");
|
|
185
|
+
rl.close();
|
|
186
|
+
}
|
|
187
|
+
if (!apiKey || !apiKey.trim()) {
|
|
188
|
+
printError("API key cannot be empty.");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
apiKey = apiKey.trim();
|
|
192
|
+
if (!/^hs_(live|test)_[a-f0-9]{64}$/.test(apiKey)) {
|
|
193
|
+
printError("Invalid API key format. Expected: hs_live_<64 hex> or hs_test_<64 hex>");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const client = new hookstreamClient({ apiKey, baseUrl: opts.baseUrl });
|
|
198
|
+
await client.get("/sources");
|
|
199
|
+
} catch (err) {
|
|
200
|
+
printError(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
const config = loadConfig();
|
|
204
|
+
config.api_key = apiKey;
|
|
205
|
+
if (opts.baseUrl) {
|
|
206
|
+
config.base_url = opts.baseUrl;
|
|
207
|
+
}
|
|
208
|
+
saveConfig(config);
|
|
209
|
+
if (isJsonMode()) {
|
|
210
|
+
printJson({ success: true, prefix: apiKey.slice(0, 16) + "..." });
|
|
211
|
+
} else {
|
|
212
|
+
printSuccess(`Logged in as ${apiKey.slice(0, 16)}...`);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// src/commands/logout.ts
|
|
217
|
+
import { Command as Command2 } from "commander";
|
|
218
|
+
var logoutCommand = new Command2("logout").description("Remove stored credentials").action(() => {
|
|
219
|
+
clearConfig();
|
|
220
|
+
if (isJsonMode()) {
|
|
221
|
+
printJson({ success: true });
|
|
222
|
+
} else {
|
|
223
|
+
printSuccess(`Logged out. Config removed from ${getConfigPath()}`);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// src/commands/whoami.ts
|
|
228
|
+
import { Command as Command3 } from "commander";
|
|
229
|
+
var whoamiCommand = new Command3("whoami").description("Show current authentication info").action(async () => {
|
|
230
|
+
const config = loadConfig();
|
|
231
|
+
if (!config.api_key) {
|
|
232
|
+
printError("Not logged in. Run `hookstream login`.");
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const client = new hookstreamClient();
|
|
237
|
+
const data = await client.get("/sources");
|
|
238
|
+
if (isJsonMode()) {
|
|
239
|
+
printJson({
|
|
240
|
+
api_key_prefix: client.getApiKeyPrefix(),
|
|
241
|
+
base_url: client.getBaseUrl(),
|
|
242
|
+
source_count: data.sources.length,
|
|
243
|
+
config_path: getConfigPath()
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
console.log();
|
|
247
|
+
printKeyValue([
|
|
248
|
+
["API Key", client.getApiKeyPrefix()],
|
|
249
|
+
["Base URL", client.getBaseUrl()],
|
|
250
|
+
["Sources", String(data.sources.length)],
|
|
251
|
+
["Config", getConfigPath()]
|
|
252
|
+
]);
|
|
253
|
+
console.log();
|
|
254
|
+
}
|
|
255
|
+
} catch (err) {
|
|
256
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// src/commands/sources/index.ts
|
|
262
|
+
import { Command as Command9 } from "commander";
|
|
263
|
+
|
|
264
|
+
// src/commands/sources/list.ts
|
|
265
|
+
import { Command as Command4 } from "commander";
|
|
266
|
+
var sourcesListCommand = new Command4("list").description("List all sources").option("--status <status>", "Filter by status (active, paused, disabled)").action(async (opts) => {
|
|
267
|
+
try {
|
|
268
|
+
const client = new hookstreamClient();
|
|
269
|
+
const params = new URLSearchParams();
|
|
270
|
+
if (opts.status) params.set("status", opts.status);
|
|
271
|
+
const qs = params.toString();
|
|
272
|
+
const data = await client.get(`/sources${qs ? `?${qs}` : ""}`);
|
|
273
|
+
if (isJsonMode()) {
|
|
274
|
+
printJson(data.sources);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
console.log();
|
|
278
|
+
printTable(
|
|
279
|
+
data.sources.map((s) => ({
|
|
280
|
+
id: s.id.slice(0, 12),
|
|
281
|
+
name: s.name,
|
|
282
|
+
slug: s.slug,
|
|
283
|
+
template: s.template || "custom",
|
|
284
|
+
status: statusColor(s.status),
|
|
285
|
+
events: String(s.event_count),
|
|
286
|
+
last_event: formatDate(s.last_event_at)
|
|
287
|
+
})),
|
|
288
|
+
[
|
|
289
|
+
{ key: "id", label: "ID", width: 14 },
|
|
290
|
+
{ key: "name", label: "Name", width: 20 },
|
|
291
|
+
{ key: "slug", label: "Slug", width: 16 },
|
|
292
|
+
{ key: "template", label: "Template", width: 12 },
|
|
293
|
+
{ key: "status", label: "Status", width: 10 },
|
|
294
|
+
{ key: "events", label: "Events", width: 8 },
|
|
295
|
+
{ key: "last_event", label: "Last Event" }
|
|
296
|
+
]
|
|
297
|
+
);
|
|
298
|
+
console.log();
|
|
299
|
+
} catch (err) {
|
|
300
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// src/commands/sources/create.ts
|
|
306
|
+
import { Command as Command5 } from "commander";
|
|
307
|
+
function slugify(name) {
|
|
308
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
309
|
+
}
|
|
310
|
+
var sourcesCreateCommand = new Command5("create").description("Create a new source").argument("<name>", "Source name").option("--slug <slug>", "URL slug (auto-generated from name if omitted)").option("--template <template>", "Provider template (stripe, github, shopify, etc.)").option("--org-id <id>", "Organization ID", "default").option("--env-id <id>", "Environment ID", "default").action(async (name, opts) => {
|
|
311
|
+
try {
|
|
312
|
+
const client = new hookstreamClient();
|
|
313
|
+
const slug = opts.slug || slugify(name);
|
|
314
|
+
const data = await client.post("/sources", {
|
|
315
|
+
org_id: opts.orgId,
|
|
316
|
+
env_id: opts.envId,
|
|
317
|
+
name,
|
|
318
|
+
slug,
|
|
319
|
+
template: opts.template || void 0
|
|
320
|
+
});
|
|
321
|
+
const s = data.source;
|
|
322
|
+
if (isJsonMode()) {
|
|
323
|
+
printJson(s);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
printSuccess(`Source created: ${s.name}`);
|
|
327
|
+
console.log();
|
|
328
|
+
printKeyValue([
|
|
329
|
+
["ID", s.id],
|
|
330
|
+
["Name", s.name],
|
|
331
|
+
["Slug", s.slug],
|
|
332
|
+
["Template", s.template || "custom"],
|
|
333
|
+
["Ingest URL", `${client.getBaseUrl()}/v1/ingest/${s.id}`]
|
|
334
|
+
]);
|
|
335
|
+
console.log();
|
|
336
|
+
} catch (err) {
|
|
337
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// src/commands/sources/get.ts
|
|
343
|
+
import { Command as Command6 } from "commander";
|
|
344
|
+
var sourcesGetCommand = new Command6("get").description("Get source details").argument("<id>", "Source ID").action(async (id) => {
|
|
345
|
+
try {
|
|
346
|
+
const client = new hookstreamClient();
|
|
347
|
+
const data = await client.get(`/sources/${id}`);
|
|
348
|
+
const s = data.source;
|
|
349
|
+
if (isJsonMode()) {
|
|
350
|
+
printJson(s);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
console.log();
|
|
354
|
+
printKeyValue([
|
|
355
|
+
["ID", s.id],
|
|
356
|
+
["Name", s.name],
|
|
357
|
+
["Slug", s.slug],
|
|
358
|
+
["Template", s.template || "custom"],
|
|
359
|
+
["Status", statusColor(s.status)],
|
|
360
|
+
["Events", String(s.event_count)],
|
|
361
|
+
["Last Event", formatDate(s.last_event_at)],
|
|
362
|
+
["Ingest URL", `${client.getBaseUrl()}/v1/ingest/${s.id}`],
|
|
363
|
+
["Verification", s.verification_enabled ? `${s.verification_type} (${s.verification_header})` : "disabled"],
|
|
364
|
+
["Dedup", s.dedup_enabled ? `${s.dedup_strategy} (${s.dedup_window_seconds}s)` : "disabled"],
|
|
365
|
+
["Created", formatDate(s.created_at)],
|
|
366
|
+
["Updated", formatDate(s.updated_at)]
|
|
367
|
+
]);
|
|
368
|
+
console.log();
|
|
369
|
+
} catch (err) {
|
|
370
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// src/commands/sources/delete.ts
|
|
376
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
377
|
+
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
378
|
+
import { Command as Command7 } from "commander";
|
|
379
|
+
var sourcesDeleteCommand = new Command7("delete").description("Delete a source").argument("<id>", "Source ID").option("-f, --force", "Skip confirmation prompt").action(async (id, opts) => {
|
|
380
|
+
try {
|
|
381
|
+
if (!opts.force) {
|
|
382
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
383
|
+
const answer = await rl.question(`Delete source ${id}? This cannot be undone. [y/N] `);
|
|
384
|
+
rl.close();
|
|
385
|
+
if (answer.toLowerCase() !== "y") {
|
|
386
|
+
console.log("Cancelled.");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const client = new hookstreamClient();
|
|
391
|
+
await client.del(`/sources/${id}`);
|
|
392
|
+
if (isJsonMode()) {
|
|
393
|
+
printJson({ success: true, id });
|
|
394
|
+
} else {
|
|
395
|
+
printSuccess(`Source ${id} deleted.`);
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// src/commands/sources/templates.ts
|
|
404
|
+
import { Command as Command8 } from "commander";
|
|
405
|
+
var sourcesTemplatesCommand = new Command8("templates").description("List available source templates").action(async () => {
|
|
406
|
+
try {
|
|
407
|
+
const client = new hookstreamClient();
|
|
408
|
+
const data = await client.get("/sources/templates");
|
|
409
|
+
if (isJsonMode()) {
|
|
410
|
+
printJson(data.templates);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
console.log();
|
|
414
|
+
printTable(
|
|
415
|
+
data.templates.map((t) => ({
|
|
416
|
+
id: t.id,
|
|
417
|
+
name: t.name,
|
|
418
|
+
description: t.description,
|
|
419
|
+
verification: t.verification_type || "none"
|
|
420
|
+
})),
|
|
421
|
+
[
|
|
422
|
+
{ key: "id", label: "ID", width: 14 },
|
|
423
|
+
{ key: "name", label: "Name", width: 14 },
|
|
424
|
+
{ key: "description", label: "Description", width: 36 },
|
|
425
|
+
{ key: "verification", label: "Verification", width: 20 }
|
|
426
|
+
]
|
|
427
|
+
);
|
|
428
|
+
console.log();
|
|
429
|
+
} catch (err) {
|
|
430
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// src/commands/sources/index.ts
|
|
436
|
+
var sourcesCommand = new Command9("sources").description("Manage webhook sources").addCommand(sourcesListCommand).addCommand(sourcesCreateCommand).addCommand(sourcesGetCommand).addCommand(sourcesDeleteCommand).addCommand(sourcesTemplatesCommand);
|
|
437
|
+
|
|
438
|
+
// src/commands/destinations/index.ts
|
|
439
|
+
import { Command as Command16 } from "commander";
|
|
440
|
+
|
|
441
|
+
// src/commands/destinations/list.ts
|
|
442
|
+
import { Command as Command10 } from "commander";
|
|
443
|
+
var destinationsListCommand = new Command10("list").description("List all destinations").option("--status <status>", "Filter by status (active, paused, disabled)").option("--type <type>", "Filter by type (http, aws_sqs, aws_s3, etc.)").action(async (opts) => {
|
|
444
|
+
try {
|
|
445
|
+
const client = new hookstreamClient();
|
|
446
|
+
const params = new URLSearchParams();
|
|
447
|
+
if (opts.status) params.set("status", opts.status);
|
|
448
|
+
if (opts.type) params.set("type", opts.type);
|
|
449
|
+
const qs = params.toString();
|
|
450
|
+
const data = await client.get(
|
|
451
|
+
`/destinations${qs ? `?${qs}` : ""}`
|
|
452
|
+
);
|
|
453
|
+
if (isJsonMode()) {
|
|
454
|
+
printJson(data.destinations);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
console.log();
|
|
458
|
+
printTable(
|
|
459
|
+
data.destinations.map((d) => ({
|
|
460
|
+
id: d.id.slice(0, 12),
|
|
461
|
+
name: d.name,
|
|
462
|
+
type: d.type || "http",
|
|
463
|
+
url: d.url.length > 35 ? d.url.slice(0, 34) + "\u2026" : d.url,
|
|
464
|
+
method: d.method,
|
|
465
|
+
status: statusColor(d.status),
|
|
466
|
+
deliveries: String(d.event_count),
|
|
467
|
+
last_delivery: formatDate(d.last_delivery_at)
|
|
468
|
+
})),
|
|
469
|
+
[
|
|
470
|
+
{ key: "id", label: "ID", width: 14 },
|
|
471
|
+
{ key: "name", label: "Name", width: 18 },
|
|
472
|
+
{ key: "type", label: "Type", width: 12 },
|
|
473
|
+
{ key: "url", label: "URL", width: 36 },
|
|
474
|
+
{ key: "method", label: "Method", width: 8 },
|
|
475
|
+
{ key: "status", label: "Status", width: 10 },
|
|
476
|
+
{ key: "deliveries", label: "Delivered", width: 10 },
|
|
477
|
+
{ key: "last_delivery", label: "Last Delivery" }
|
|
478
|
+
]
|
|
479
|
+
);
|
|
480
|
+
console.log();
|
|
481
|
+
} catch (err) {
|
|
482
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// src/commands/destinations/create.ts
|
|
488
|
+
import { Command as Command11 } from "commander";
|
|
489
|
+
function slugify2(name) {
|
|
490
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
491
|
+
}
|
|
492
|
+
var destinationsCreateCommand = new Command11("create").description("Create a new destination").argument("<name>", "Destination name").option("--url <url>", "Destination URL (required for HTTP type)").option("--type <type>", "Destination type (http, aws_sqs, aws_s3, aws_eventbridge, gcp_pubsub, kafka, rabbitmq, websocket)", "http").option("--provider-config <json>", "Provider config JSON (required for non-HTTP types)").option("--method <method>", "HTTP method (GET, POST, PUT, PATCH, DELETE)", "POST").option("--header <headers...>", "Custom headers (key:value format)").option("--slug <slug>", "URL slug (auto-generated from name if omitted)").option("--auth-type <type>", "Auth type (none, api_key, bearer_token, basic)", "none").option("--timeout <ms>", "Timeout in milliseconds", "30000").option("--org-id <id>", "Organization ID", "default").option("--env-id <id>", "Environment ID", "default").action(async (name, opts) => {
|
|
493
|
+
try {
|
|
494
|
+
const client = new hookstreamClient();
|
|
495
|
+
const slug = opts.slug || slugify2(name);
|
|
496
|
+
const destType = opts.type || "http";
|
|
497
|
+
if (destType === "http" && !opts.url) {
|
|
498
|
+
printError("--url is required for HTTP destinations");
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
if (destType !== "http" && !opts.providerConfig) {
|
|
502
|
+
printError("--provider-config is required for non-HTTP destinations");
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
let headers;
|
|
506
|
+
if (opts.header) {
|
|
507
|
+
headers = {};
|
|
508
|
+
for (const h of opts.header) {
|
|
509
|
+
const colonIdx = h.indexOf(":");
|
|
510
|
+
if (colonIdx > 0) {
|
|
511
|
+
headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
let providerConfig;
|
|
516
|
+
if (opts.providerConfig) {
|
|
517
|
+
try {
|
|
518
|
+
providerConfig = JSON.parse(opts.providerConfig);
|
|
519
|
+
} catch {
|
|
520
|
+
printError("Invalid --provider-config JSON");
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const body = {
|
|
525
|
+
org_id: opts.orgId,
|
|
526
|
+
env_id: opts.envId,
|
|
527
|
+
name,
|
|
528
|
+
slug,
|
|
529
|
+
type: destType,
|
|
530
|
+
timeout_ms: parseInt(opts.timeout, 10)
|
|
531
|
+
};
|
|
532
|
+
if (destType === "http") {
|
|
533
|
+
body.url = opts.url;
|
|
534
|
+
body.method = opts.method?.toUpperCase();
|
|
535
|
+
body.headers = headers;
|
|
536
|
+
body.auth_type = opts.authType;
|
|
537
|
+
} else {
|
|
538
|
+
body.provider_config = providerConfig;
|
|
539
|
+
}
|
|
540
|
+
const data = await client.post("/destinations", body);
|
|
541
|
+
const d = data.destination;
|
|
542
|
+
if (isJsonMode()) {
|
|
543
|
+
printJson(d);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
printSuccess(`Destination created: ${d.name}`);
|
|
547
|
+
console.log();
|
|
548
|
+
const entries = [
|
|
549
|
+
["ID", d.id],
|
|
550
|
+
["Name", d.name],
|
|
551
|
+
["Type", d.type || "http"],
|
|
552
|
+
["URL", d.url]
|
|
553
|
+
];
|
|
554
|
+
if (destType === "http") {
|
|
555
|
+
entries.push(["Method", d.method]);
|
|
556
|
+
entries.push(["Auth", d.auth_type]);
|
|
557
|
+
}
|
|
558
|
+
entries.push(["Timeout", `${d.timeout_ms}ms`]);
|
|
559
|
+
printKeyValue(entries);
|
|
560
|
+
console.log();
|
|
561
|
+
} catch (err) {
|
|
562
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// src/commands/destinations/get.ts
|
|
568
|
+
import { Command as Command12 } from "commander";
|
|
569
|
+
var destinationsGetCommand = new Command12("get").description("Get destination details").argument("<id>", "Destination ID").action(async (id) => {
|
|
570
|
+
try {
|
|
571
|
+
const client = new hookstreamClient();
|
|
572
|
+
const data = await client.get(`/destinations/${id}`);
|
|
573
|
+
const d = data.destination;
|
|
574
|
+
if (isJsonMode()) {
|
|
575
|
+
printJson(d);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const destType = d.type || "http";
|
|
579
|
+
console.log();
|
|
580
|
+
const entries = [
|
|
581
|
+
["ID", d.id],
|
|
582
|
+
["Name", d.name],
|
|
583
|
+
["Type", destType],
|
|
584
|
+
["Slug", d.slug],
|
|
585
|
+
["URL", d.url]
|
|
586
|
+
];
|
|
587
|
+
if (destType === "http") {
|
|
588
|
+
entries.push(["Method", d.method]);
|
|
589
|
+
entries.push(["Auth", d.auth_type]);
|
|
590
|
+
}
|
|
591
|
+
entries.push(
|
|
592
|
+
["Status", statusColor(d.status)],
|
|
593
|
+
["Timeout", `${d.timeout_ms}ms`],
|
|
594
|
+
["Deliveries", String(d.event_count)],
|
|
595
|
+
["Last Delivery", formatDate(d.last_delivery_at)],
|
|
596
|
+
["Created", formatDate(d.created_at)],
|
|
597
|
+
["Updated", formatDate(d.updated_at)]
|
|
598
|
+
);
|
|
599
|
+
printKeyValue(entries);
|
|
600
|
+
console.log();
|
|
601
|
+
} catch (err) {
|
|
602
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// src/commands/destinations/delete.ts
|
|
608
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
609
|
+
import { stdin as stdin3, stdout as stdout3 } from "process";
|
|
610
|
+
import { Command as Command13 } from "commander";
|
|
611
|
+
var destinationsDeleteCommand = new Command13("delete").description("Delete a destination").argument("<id>", "Destination ID").option("-f, --force", "Skip confirmation prompt").action(async (id, opts) => {
|
|
612
|
+
try {
|
|
613
|
+
if (!opts.force) {
|
|
614
|
+
const rl = createInterface3({ input: stdin3, output: stdout3 });
|
|
615
|
+
const answer = await rl.question(`Delete destination ${id}? This cannot be undone. [y/N] `);
|
|
616
|
+
rl.close();
|
|
617
|
+
if (answer.toLowerCase() !== "y") {
|
|
618
|
+
console.log("Cancelled.");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const client = new hookstreamClient();
|
|
623
|
+
await client.del(`/destinations/${id}`);
|
|
624
|
+
if (isJsonMode()) {
|
|
625
|
+
printJson({ success: true, id });
|
|
626
|
+
} else {
|
|
627
|
+
printSuccess(`Destination ${id} deleted.`);
|
|
628
|
+
}
|
|
629
|
+
} catch (err) {
|
|
630
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// src/commands/destinations/circuit.ts
|
|
636
|
+
import { Command as Command14 } from "commander";
|
|
637
|
+
var destinationsCircuitCommand = new Command14("circuit").description("Show circuit breaker status for a destination").argument("<id>", "Destination ID").action(async (id) => {
|
|
638
|
+
try {
|
|
639
|
+
const client = new hookstreamClient();
|
|
640
|
+
const data = await client.get(`/destinations/${id}/circuit`);
|
|
641
|
+
if (isJsonMode()) {
|
|
642
|
+
printJson(data.circuit);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const c = data.circuit;
|
|
646
|
+
console.log();
|
|
647
|
+
printKeyValue([
|
|
648
|
+
["State", statusColor(c.circuit_state === "closed" ? "active" : c.circuit_state === "open" ? "error" : "paused").replace(/active|error|paused/, c.circuit_state)],
|
|
649
|
+
["Failure count", String(c.failure_count)],
|
|
650
|
+
["Opened at", c.opened_at ?? "\u2014"],
|
|
651
|
+
["Pending retries", String(c.pending_retries)]
|
|
652
|
+
]);
|
|
653
|
+
console.log();
|
|
654
|
+
} catch (err) {
|
|
655
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// src/commands/destinations/circuit-reset.ts
|
|
661
|
+
import { Command as Command15 } from "commander";
|
|
662
|
+
var destinationsCircuitResetCommand = new Command15("circuit-reset").description("Reset circuit breaker for a destination").argument("<id>", "Destination ID").action(async (id) => {
|
|
663
|
+
try {
|
|
664
|
+
const client = new hookstreamClient();
|
|
665
|
+
const data = await client.post(
|
|
666
|
+
`/destinations/${id}/circuit/reset`
|
|
667
|
+
);
|
|
668
|
+
if (isJsonMode()) {
|
|
669
|
+
printJson(data.circuit);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
printSuccess(`Circuit breaker reset for destination ${id}`);
|
|
673
|
+
} catch (err) {
|
|
674
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// src/commands/destinations/index.ts
|
|
680
|
+
var destinationsCommand = new Command16("destinations").description("Manage webhook destinations").addCommand(destinationsListCommand).addCommand(destinationsCreateCommand).addCommand(destinationsGetCommand).addCommand(destinationsDeleteCommand).addCommand(destinationsCircuitCommand).addCommand(destinationsCircuitResetCommand);
|
|
681
|
+
|
|
682
|
+
// src/commands/connections/index.ts
|
|
683
|
+
import { Command as Command21 } from "commander";
|
|
684
|
+
|
|
685
|
+
// src/commands/connections/list.ts
|
|
686
|
+
import { Command as Command17 } from "commander";
|
|
687
|
+
var connectionsListCommand = new Command17("list").description("List all connections").option("--source <id>", "Filter by source ID").option("--destination <id>", "Filter by destination ID").option("--status <status>", "Filter by status (active, paused, disabled)").action(async (opts) => {
|
|
688
|
+
try {
|
|
689
|
+
const client = new hookstreamClient();
|
|
690
|
+
const params = new URLSearchParams();
|
|
691
|
+
if (opts.source) params.set("source_id", opts.source);
|
|
692
|
+
if (opts.destination) params.set("destination_id", opts.destination);
|
|
693
|
+
if (opts.status) params.set("status", opts.status);
|
|
694
|
+
const qs = params.toString();
|
|
695
|
+
const data = await client.get(
|
|
696
|
+
`/connections${qs ? `?${qs}` : ""}`
|
|
697
|
+
);
|
|
698
|
+
if (isJsonMode()) {
|
|
699
|
+
printJson(data.connections);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
console.log();
|
|
703
|
+
printTable(
|
|
704
|
+
data.connections.map((c) => ({
|
|
705
|
+
id: c.id.slice(0, 12),
|
|
706
|
+
name: c.name,
|
|
707
|
+
route: `${c.source_name || c.source_id.slice(0, 8)} \u2192 ${c.destination_name || c.destination_id.slice(0, 8)}`,
|
|
708
|
+
enabled: c.enabled ? "yes" : "no",
|
|
709
|
+
status: statusColor(c.status),
|
|
710
|
+
events: String(c.event_count),
|
|
711
|
+
last_event: formatDate(c.last_event_at)
|
|
712
|
+
})),
|
|
713
|
+
[
|
|
714
|
+
{ key: "id", label: "ID", width: 14 },
|
|
715
|
+
{ key: "name", label: "Name", width: 20 },
|
|
716
|
+
{ key: "route", label: "Route", width: 30 },
|
|
717
|
+
{ key: "enabled", label: "Enabled", width: 9 },
|
|
718
|
+
{ key: "status", label: "Status", width: 10 },
|
|
719
|
+
{ key: "events", label: "Events", width: 8 },
|
|
720
|
+
{ key: "last_event", label: "Last Event" }
|
|
721
|
+
]
|
|
722
|
+
);
|
|
723
|
+
console.log();
|
|
724
|
+
} catch (err) {
|
|
725
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// src/commands/connections/create.ts
|
|
731
|
+
import { Command as Command18 } from "commander";
|
|
732
|
+
var connectionsCreateCommand = new Command18("create").description("Create a new connection").argument("<name>", "Connection name").requiredOption("--source <id>", "Source ID").requiredOption("--destination <id>", "Destination ID").option("--filter <json>", "Filter rules as JSON array").option("--description <text>", "Connection description").option("--org-id <id>", "Organization ID", "default").option("--env-id <id>", "Environment ID", "default").action(async (name, opts) => {
|
|
733
|
+
try {
|
|
734
|
+
const client = new hookstreamClient();
|
|
735
|
+
let filterRules;
|
|
736
|
+
if (opts.filter) {
|
|
737
|
+
try {
|
|
738
|
+
filterRules = JSON.parse(opts.filter);
|
|
739
|
+
if (!Array.isArray(filterRules)) {
|
|
740
|
+
throw new Error("Filter rules must be a JSON array");
|
|
741
|
+
}
|
|
742
|
+
} catch (e) {
|
|
743
|
+
printError(`Invalid filter JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const data = await client.post("/connections", {
|
|
748
|
+
org_id: opts.orgId,
|
|
749
|
+
env_id: opts.envId,
|
|
750
|
+
source_id: opts.source,
|
|
751
|
+
destination_id: opts.destination,
|
|
752
|
+
name,
|
|
753
|
+
description: opts.description,
|
|
754
|
+
filter_rules: filterRules
|
|
755
|
+
});
|
|
756
|
+
const conn = data.connection;
|
|
757
|
+
if (isJsonMode()) {
|
|
758
|
+
printJson(conn);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
printSuccess(`Connection created: ${conn.name}`);
|
|
762
|
+
console.log();
|
|
763
|
+
printKeyValue([
|
|
764
|
+
["ID", conn.id],
|
|
765
|
+
["Name", conn.name],
|
|
766
|
+
["Source", conn.source_name || conn.source_id],
|
|
767
|
+
["Destination", conn.destination_name || conn.destination_id],
|
|
768
|
+
["Enabled", conn.enabled ? "yes" : "no"],
|
|
769
|
+
["Filters", conn.filter_rules ? "configured" : "none"]
|
|
770
|
+
]);
|
|
771
|
+
console.log();
|
|
772
|
+
} catch (err) {
|
|
773
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// src/commands/connections/get.ts
|
|
779
|
+
import { Command as Command19 } from "commander";
|
|
780
|
+
var connectionsGetCommand = new Command19("get").description("Get connection details").argument("<id>", "Connection ID").action(async (id) => {
|
|
781
|
+
try {
|
|
782
|
+
const client = new hookstreamClient();
|
|
783
|
+
const data = await client.get(`/connections/${id}`);
|
|
784
|
+
const conn = data.connection;
|
|
785
|
+
if (isJsonMode()) {
|
|
786
|
+
printJson(conn);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
console.log();
|
|
790
|
+
printKeyValue([
|
|
791
|
+
["ID", conn.id],
|
|
792
|
+
["Name", conn.name],
|
|
793
|
+
["Description", conn.description || "\u2014"],
|
|
794
|
+
["Source", conn.source_name || conn.source_id],
|
|
795
|
+
["Destination", conn.destination_name || conn.destination_id],
|
|
796
|
+
["Enabled", conn.enabled ? "yes" : "no"],
|
|
797
|
+
["Status", statusColor(conn.status)],
|
|
798
|
+
["Filters", conn.filter_rules ? JSON.stringify(JSON.parse(conn.filter_rules)) : "none"],
|
|
799
|
+
["Events", String(conn.event_count)],
|
|
800
|
+
["Last Event", formatDate(conn.last_event_at)],
|
|
801
|
+
["Created", formatDate(conn.created_at)],
|
|
802
|
+
["Updated", formatDate(conn.updated_at)]
|
|
803
|
+
]);
|
|
804
|
+
console.log();
|
|
805
|
+
} catch (err) {
|
|
806
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// src/commands/connections/delete.ts
|
|
812
|
+
import { createInterface as createInterface4 } from "readline/promises";
|
|
813
|
+
import { stdin as stdin4, stdout as stdout4 } from "process";
|
|
814
|
+
import { Command as Command20 } from "commander";
|
|
815
|
+
var connectionsDeleteCommand = new Command20("delete").description("Delete a connection").argument("<id>", "Connection ID").option("-f, --force", "Skip confirmation prompt").action(async (id, opts) => {
|
|
816
|
+
try {
|
|
817
|
+
if (!opts.force) {
|
|
818
|
+
const rl = createInterface4({ input: stdin4, output: stdout4 });
|
|
819
|
+
const answer = await rl.question(`Delete connection ${id}? This cannot be undone. [y/N] `);
|
|
820
|
+
rl.close();
|
|
821
|
+
if (answer.toLowerCase() !== "y") {
|
|
822
|
+
console.log("Cancelled.");
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const client = new hookstreamClient();
|
|
827
|
+
await client.del(`/connections/${id}`);
|
|
828
|
+
if (isJsonMode()) {
|
|
829
|
+
printJson({ success: true, id });
|
|
830
|
+
} else {
|
|
831
|
+
printSuccess(`Connection ${id} deleted.`);
|
|
832
|
+
}
|
|
833
|
+
} catch (err) {
|
|
834
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// src/commands/connections/index.ts
|
|
840
|
+
var connectionsCommand = new Command21("connections").description("Manage source-to-destination connections").addCommand(connectionsListCommand).addCommand(connectionsCreateCommand).addCommand(connectionsGetCommand).addCommand(connectionsDeleteCommand);
|
|
841
|
+
|
|
842
|
+
// src/commands/events/index.ts
|
|
843
|
+
import { Command as Command25 } from "commander";
|
|
844
|
+
|
|
845
|
+
// src/commands/events/list.ts
|
|
846
|
+
import { Command as Command22 } from "commander";
|
|
847
|
+
var eventsListCommand = new Command22("list").description("List events").option("--source <id>", "Filter by source ID").option("--status <status>", "Filter by status").option("--method <method>", "Filter by HTTP method").option("--verification <status>", "Filter by verification status").option("--limit <n>", "Number of events to show", "20").option("--cursor <cursor>", "Pagination cursor").action(async (opts) => {
|
|
848
|
+
try {
|
|
849
|
+
const client = new hookstreamClient();
|
|
850
|
+
const params = new URLSearchParams();
|
|
851
|
+
if (opts.source) params.set("source_id", opts.source);
|
|
852
|
+
if (opts.status) params.set("status", opts.status);
|
|
853
|
+
if (opts.method) params.set("method", opts.method.toUpperCase());
|
|
854
|
+
if (opts.verification) params.set("verification_status", opts.verification);
|
|
855
|
+
params.set("limit", opts.limit);
|
|
856
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
857
|
+
const data = await client.get(`/events?${params}`);
|
|
858
|
+
if (isJsonMode()) {
|
|
859
|
+
printJson(data);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
console.log();
|
|
863
|
+
printTable(
|
|
864
|
+
data.events.map((e) => ({
|
|
865
|
+
id: e.id.slice(0, 12),
|
|
866
|
+
source: e.source_name || e.source_id.slice(0, 12),
|
|
867
|
+
method: e.method,
|
|
868
|
+
verification: statusColor(e.verification_status),
|
|
869
|
+
size: formatBytes(e.payload_size),
|
|
870
|
+
received: formatDate(e.received_at)
|
|
871
|
+
})),
|
|
872
|
+
[
|
|
873
|
+
{ key: "id", label: "ID", width: 14 },
|
|
874
|
+
{ key: "source", label: "Source", width: 16 },
|
|
875
|
+
{ key: "method", label: "Method", width: 8 },
|
|
876
|
+
{ key: "verification", label: "Verify", width: 12 },
|
|
877
|
+
{ key: "size", label: "Size", width: 10 },
|
|
878
|
+
{ key: "received", label: "Received" }
|
|
879
|
+
]
|
|
880
|
+
);
|
|
881
|
+
const p = data.pagination;
|
|
882
|
+
console.log();
|
|
883
|
+
console.log(` ${data.events.length} of ${p.total} events${p.has_more ? " (more available)" : ""}`);
|
|
884
|
+
if (p.next_cursor) {
|
|
885
|
+
console.log(` Next: hookstream events list --cursor ${p.next_cursor}`);
|
|
886
|
+
}
|
|
887
|
+
console.log();
|
|
888
|
+
} catch (err) {
|
|
889
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// src/commands/events/get.ts
|
|
895
|
+
import { Command as Command23 } from "commander";
|
|
896
|
+
import chalk2 from "chalk";
|
|
897
|
+
var eventsGetCommand = new Command23("get").description("Get event details").argument("<id>", "Event ID").action(async (id) => {
|
|
898
|
+
try {
|
|
899
|
+
const client = new hookstreamClient();
|
|
900
|
+
const data = await client.get(`/events/${id}`);
|
|
901
|
+
const e = data.event;
|
|
902
|
+
if (isJsonMode()) {
|
|
903
|
+
printJson(e);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
console.log();
|
|
907
|
+
printKeyValue([
|
|
908
|
+
["ID", e.id],
|
|
909
|
+
["Source", e.source_name ? `${e.source_name} (${e.source_id})` : e.source_id],
|
|
910
|
+
["Method", e.method],
|
|
911
|
+
["Content-Type", e.content_type],
|
|
912
|
+
["Source IP", e.source_ip],
|
|
913
|
+
["Size", formatBytes(e.payload_size)],
|
|
914
|
+
["Status", statusColor(e.status)],
|
|
915
|
+
["Verification", statusColor(e.verification_status)],
|
|
916
|
+
["Dedup", statusColor(e.dedup_status)],
|
|
917
|
+
["Received", formatDate(e.received_at)]
|
|
918
|
+
]);
|
|
919
|
+
if (e.headers && Object.keys(e.headers).length > 0) {
|
|
920
|
+
console.log();
|
|
921
|
+
console.log(chalk2.bold(" Headers"));
|
|
922
|
+
for (const [key, val] of Object.entries(e.headers)) {
|
|
923
|
+
if (key.startsWith("cf-") || key.startsWith("x-real-") || key === "cdn-loop") continue;
|
|
924
|
+
console.log(` ${chalk2.dim(key)}: ${val}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (e.payload) {
|
|
928
|
+
console.log();
|
|
929
|
+
console.log(chalk2.bold(" Payload"));
|
|
930
|
+
try {
|
|
931
|
+
const parsed = JSON.parse(e.payload);
|
|
932
|
+
console.log(JSON.stringify(parsed, null, 2).split("\n").map((l) => ` ${l}`).join("\n"));
|
|
933
|
+
} catch {
|
|
934
|
+
const lines = e.payload.split("\n").slice(0, 50);
|
|
935
|
+
for (const line of lines) {
|
|
936
|
+
console.log(` ${line}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
console.log();
|
|
941
|
+
} catch (err) {
|
|
942
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// src/commands/events/replay.ts
|
|
948
|
+
import { Command as Command24 } from "commander";
|
|
949
|
+
import chalk3 from "chalk";
|
|
950
|
+
var SKIP_HEADERS = /* @__PURE__ */ new Set([
|
|
951
|
+
"cf-connecting-ip",
|
|
952
|
+
"cf-ipcountry",
|
|
953
|
+
"cf-ray",
|
|
954
|
+
"cf-visitor",
|
|
955
|
+
"cf-warp-tag-id",
|
|
956
|
+
"cdn-loop",
|
|
957
|
+
"x-forwarded-for",
|
|
958
|
+
"x-forwarded-proto",
|
|
959
|
+
"x-real-ip",
|
|
960
|
+
"host",
|
|
961
|
+
"content-length",
|
|
962
|
+
"accept-encoding",
|
|
963
|
+
"connection",
|
|
964
|
+
"keep-alive"
|
|
965
|
+
]);
|
|
966
|
+
var eventsReplayCommand = new Command24("replay").description("Replay an event to a URL").argument("<id>", "Event ID").requiredOption("--to <url>", "Target URL to replay the event to").action(async (id, opts) => {
|
|
967
|
+
try {
|
|
968
|
+
const client = new hookstreamClient();
|
|
969
|
+
const data = await client.get(`/events/${id}`);
|
|
970
|
+
const e = data.event;
|
|
971
|
+
if (!e.payload) {
|
|
972
|
+
printError("Event has no payload to replay.");
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
const headers = { "Content-Type": e.content_type || "application/json" };
|
|
976
|
+
if (e.headers) {
|
|
977
|
+
for (const [key, val] of Object.entries(e.headers)) {
|
|
978
|
+
if (!SKIP_HEADERS.has(key.toLowerCase())) {
|
|
979
|
+
headers[key] = val;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const startMs = Date.now();
|
|
984
|
+
const res = await fetch(opts.to, {
|
|
985
|
+
method: e.method || "POST",
|
|
986
|
+
headers,
|
|
987
|
+
body: e.payload
|
|
988
|
+
});
|
|
989
|
+
const latencyMs = Date.now() - startMs;
|
|
990
|
+
const resBody = await res.text();
|
|
991
|
+
if (isJsonMode()) {
|
|
992
|
+
printJson({
|
|
993
|
+
event_id: id,
|
|
994
|
+
target: opts.to,
|
|
995
|
+
status: res.status,
|
|
996
|
+
latency_ms: latencyMs,
|
|
997
|
+
response_size: resBody.length
|
|
998
|
+
});
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const statusStr = res.ok ? chalk3.green(String(res.status)) : chalk3.red(String(res.status));
|
|
1002
|
+
printSuccess(`Replayed event ${id.slice(0, 12)} to ${opts.to}`);
|
|
1003
|
+
console.log();
|
|
1004
|
+
printKeyValue([
|
|
1005
|
+
["Status", statusStr],
|
|
1006
|
+
["Latency", `${latencyMs}ms`],
|
|
1007
|
+
["Response", resBody.length > 200 ? resBody.slice(0, 200) + "..." : resBody || "(empty)"]
|
|
1008
|
+
]);
|
|
1009
|
+
console.log();
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1012
|
+
process.exit(1);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// src/commands/events/index.ts
|
|
1017
|
+
var eventsCommand = new Command25("events").description("Browse and replay events").addCommand(eventsListCommand).addCommand(eventsGetCommand).addCommand(eventsReplayCommand);
|
|
1018
|
+
|
|
1019
|
+
// src/commands/deliveries/index.ts
|
|
1020
|
+
import { Command as Command30 } from "commander";
|
|
1021
|
+
|
|
1022
|
+
// src/commands/deliveries/list.ts
|
|
1023
|
+
import { Command as Command26 } from "commander";
|
|
1024
|
+
var deliveriesListCommand = new Command26("list").description("List delivery attempts").option("--event <id>", "Filter by event ID").option("--connection <id>", "Filter by connection ID").option("--destination <id>", "Filter by destination ID").option("--status <status>", "Filter by status (success, failed, timeout, error, dlq)").option("--dlq", "Show only DLQ items").option("--limit <n>", "Max results", "20").action(async (opts) => {
|
|
1025
|
+
try {
|
|
1026
|
+
const client = new hookstreamClient();
|
|
1027
|
+
const params = new URLSearchParams();
|
|
1028
|
+
if (opts.event) params.set("event_id", opts.event);
|
|
1029
|
+
if (opts.connection) params.set("connection_id", opts.connection);
|
|
1030
|
+
if (opts.destination) params.set("destination_id", opts.destination);
|
|
1031
|
+
if (opts.status) params.set("status", opts.status);
|
|
1032
|
+
if (opts.dlq) params.set("is_dlq", "1");
|
|
1033
|
+
params.set("limit", opts.limit);
|
|
1034
|
+
const qs = params.toString();
|
|
1035
|
+
const data = await client.get(
|
|
1036
|
+
`/delivery-attempts${qs ? `?${qs}` : ""}`
|
|
1037
|
+
);
|
|
1038
|
+
if (isJsonMode()) {
|
|
1039
|
+
printJson(data);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
console.log();
|
|
1043
|
+
printTable(
|
|
1044
|
+
data.delivery_attempts.map((a) => ({
|
|
1045
|
+
id: a.id.slice(0, 12),
|
|
1046
|
+
event: a.event_id.slice(0, 12),
|
|
1047
|
+
destination: a.destination_name ?? a.destination_id.slice(0, 12),
|
|
1048
|
+
status: statusColor(a.status),
|
|
1049
|
+
code: a.response_status ? String(a.response_status) : "\u2014",
|
|
1050
|
+
attempt: a.attempt_number ? `#${a.attempt_number}` : "#1",
|
|
1051
|
+
duration: a.duration_ms !== null ? `${a.duration_ms}ms` : "\u2014",
|
|
1052
|
+
created: formatDate(a.created_at)
|
|
1053
|
+
})),
|
|
1054
|
+
[
|
|
1055
|
+
{ key: "id", label: "ID", width: 14 },
|
|
1056
|
+
{ key: "event", label: "Event", width: 14 },
|
|
1057
|
+
{ key: "destination", label: "Destination", width: 20 },
|
|
1058
|
+
{ key: "status", label: "Status", width: 10 },
|
|
1059
|
+
{ key: "code", label: "Code", width: 6 },
|
|
1060
|
+
{ key: "attempt", label: "Att#", width: 6 },
|
|
1061
|
+
{ key: "duration", label: "Latency", width: 10 },
|
|
1062
|
+
{ key: "created", label: "Created" }
|
|
1063
|
+
]
|
|
1064
|
+
);
|
|
1065
|
+
if (data.pagination.has_more) {
|
|
1066
|
+
console.log(` ... and more (total: ${data.pagination.total})`);
|
|
1067
|
+
}
|
|
1068
|
+
console.log();
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
// src/commands/deliveries/get.ts
|
|
1076
|
+
import { Command as Command27 } from "commander";
|
|
1077
|
+
var deliveriesGetCommand = new Command27("get").description("Get delivery attempt details").argument("<id>", "Delivery attempt ID").action(async (id) => {
|
|
1078
|
+
try {
|
|
1079
|
+
const client = new hookstreamClient();
|
|
1080
|
+
const data = await client.get(`/delivery-attempts/${id}`);
|
|
1081
|
+
const a = data.delivery_attempt;
|
|
1082
|
+
if (isJsonMode()) {
|
|
1083
|
+
printJson(a);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
console.log();
|
|
1087
|
+
printKeyValue([
|
|
1088
|
+
["ID", a.id],
|
|
1089
|
+
["Event ID", a.event_id],
|
|
1090
|
+
["Connection ID", a.connection_id],
|
|
1091
|
+
["Connection", a.connection_name],
|
|
1092
|
+
["Destination ID", a.destination_id],
|
|
1093
|
+
["Destination", a.destination_name],
|
|
1094
|
+
["Status", statusColor(a.status)],
|
|
1095
|
+
["Attempt #", String(a.attempt_number ?? 1)],
|
|
1096
|
+
["Trigger", a.trigger_type ?? "auto"],
|
|
1097
|
+
["Request", `${a.request_method} ${a.request_url}`],
|
|
1098
|
+
["Response", a.response_status ? String(a.response_status) : "\u2014"],
|
|
1099
|
+
["Latency", a.duration_ms !== null ? `${a.duration_ms}ms` : "\u2014"],
|
|
1100
|
+
["Error", a.error],
|
|
1101
|
+
["DLQ", a.is_dlq ? "Yes" : "No"],
|
|
1102
|
+
["DLQ at", a.dlq_at ? formatDate(a.dlq_at) : null],
|
|
1103
|
+
["Next retry", a.next_retry_at ? formatDate(a.next_retry_at) : null],
|
|
1104
|
+
["Created", formatDate(a.created_at)]
|
|
1105
|
+
]);
|
|
1106
|
+
if (a.response_body) {
|
|
1107
|
+
console.log("\n Response body:");
|
|
1108
|
+
console.log(" " + a.response_body.slice(0, 500));
|
|
1109
|
+
}
|
|
1110
|
+
console.log();
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
// src/commands/deliveries/retry.ts
|
|
1118
|
+
import { Command as Command28 } from "commander";
|
|
1119
|
+
var deliveriesRetryCommand = new Command28("retry").description("Retry a failed delivery attempt").argument("<id>", "Delivery attempt ID").action(async (id) => {
|
|
1120
|
+
try {
|
|
1121
|
+
const client = new hookstreamClient();
|
|
1122
|
+
const data = await client.post(
|
|
1123
|
+
`/delivery-attempts/${id}/retry`
|
|
1124
|
+
);
|
|
1125
|
+
if (isJsonMode()) {
|
|
1126
|
+
printJson(data);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const a = data.delivery_attempt;
|
|
1130
|
+
printSuccess(`Retry triggered for attempt ${id}`);
|
|
1131
|
+
console.log();
|
|
1132
|
+
printKeyValue([
|
|
1133
|
+
["New Attempt ID", String(a.id ?? "\u2014")],
|
|
1134
|
+
["Status", statusColor(String(a.status ?? "\u2014"))],
|
|
1135
|
+
["Response", a.response_status ? String(a.response_status) : "\u2014"],
|
|
1136
|
+
["Latency", a.duration_ms !== null ? `${a.duration_ms}ms` : "\u2014"]
|
|
1137
|
+
]);
|
|
1138
|
+
console.log();
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// src/commands/deliveries/dlq.ts
|
|
1146
|
+
import { Command as Command29 } from "commander";
|
|
1147
|
+
var deliveriesDlqCommand = new Command29("dlq").description("List dead-letter queue items").option("--destination <id>", "Filter by destination ID").option("--limit <n>", "Max results", "50").action(async (opts) => {
|
|
1148
|
+
try {
|
|
1149
|
+
const client = new hookstreamClient();
|
|
1150
|
+
const params = new URLSearchParams();
|
|
1151
|
+
params.set("is_dlq", "1");
|
|
1152
|
+
if (opts.destination) params.set("destination_id", opts.destination);
|
|
1153
|
+
params.set("limit", opts.limit);
|
|
1154
|
+
const qs = params.toString();
|
|
1155
|
+
const data = await client.get(
|
|
1156
|
+
`/delivery-attempts?${qs}`
|
|
1157
|
+
);
|
|
1158
|
+
if (isJsonMode()) {
|
|
1159
|
+
printJson(data);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
console.log();
|
|
1163
|
+
if (data.delivery_attempts.length === 0) {
|
|
1164
|
+
console.log(" No items in the dead-letter queue.");
|
|
1165
|
+
} else {
|
|
1166
|
+
printTable(
|
|
1167
|
+
data.delivery_attempts.map((a) => ({
|
|
1168
|
+
id: a.id.slice(0, 12),
|
|
1169
|
+
event: a.event_id.slice(0, 12),
|
|
1170
|
+
destination: a.destination_name ?? a.destination_id.slice(0, 12),
|
|
1171
|
+
status: statusColor(a.status),
|
|
1172
|
+
attempts: a.attempt_number ? String(a.attempt_number) : "1",
|
|
1173
|
+
error: a.error ? a.error.length > 30 ? a.error.slice(0, 29) + "\u2026" : a.error : "\u2014",
|
|
1174
|
+
dlq_at: formatDate(a.dlq_at)
|
|
1175
|
+
})),
|
|
1176
|
+
[
|
|
1177
|
+
{ key: "id", label: "ID", width: 14 },
|
|
1178
|
+
{ key: "event", label: "Event", width: 14 },
|
|
1179
|
+
{ key: "destination", label: "Destination", width: 20 },
|
|
1180
|
+
{ key: "status", label: "Status", width: 8 },
|
|
1181
|
+
{ key: "attempts", label: "Att#", width: 6 },
|
|
1182
|
+
{ key: "error", label: "Error", width: 30 },
|
|
1183
|
+
{ key: "dlq_at", label: "DLQ At" }
|
|
1184
|
+
]
|
|
1185
|
+
);
|
|
1186
|
+
if (data.pagination.has_more) {
|
|
1187
|
+
console.log(` ... and more (total: ${data.pagination.total})`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
console.log();
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
// src/commands/deliveries/index.ts
|
|
1198
|
+
var deliveriesCommand = new Command30("deliveries").description("Manage delivery attempts").addCommand(deliveriesListCommand).addCommand(deliveriesGetCommand).addCommand(deliveriesRetryCommand).addCommand(deliveriesDlqCommand);
|
|
1199
|
+
|
|
1200
|
+
// src/commands/metrics/index.ts
|
|
1201
|
+
import { Command as Command33 } from "commander";
|
|
1202
|
+
|
|
1203
|
+
// src/commands/metrics/overview.ts
|
|
1204
|
+
import { Command as Command31 } from "commander";
|
|
1205
|
+
var metricsOverviewCommand = new Command31("overview").description("Show metrics overview").action(async () => {
|
|
1206
|
+
try {
|
|
1207
|
+
const client = new hookstreamClient();
|
|
1208
|
+
const data = await client.get("/metrics/overview");
|
|
1209
|
+
if (isJsonMode()) {
|
|
1210
|
+
printJson(data);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
console.log();
|
|
1214
|
+
printKeyValue([
|
|
1215
|
+
["Events Today", String(data.events_today)],
|
|
1216
|
+
["Events This Week", String(data.events_this_week)],
|
|
1217
|
+
["Active Sources", String(data.active_sources)],
|
|
1218
|
+
["Active Destinations", String(data.active_destinations)],
|
|
1219
|
+
["Deliveries Today", String(data.deliveries_today)],
|
|
1220
|
+
["Success Rate", `${data.success_rate_today}%`],
|
|
1221
|
+
["DLQ Count", String(data.dlq_count)]
|
|
1222
|
+
]);
|
|
1223
|
+
console.log();
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
// src/commands/metrics/volume.ts
|
|
1231
|
+
import { Command as Command32 } from "commander";
|
|
1232
|
+
import chalk4 from "chalk";
|
|
1233
|
+
var metricsVolumeCommand = new Command32("volume").description("Show event volume over time").option("--source <id>", "Filter by source ID").option("--after <iso>", "Start time (ISO 8601)").option("--before <iso>", "End time (ISO 8601)").option("--granularity <period>", "hour or day", "hour").action(async (opts) => {
|
|
1234
|
+
try {
|
|
1235
|
+
const client = new hookstreamClient();
|
|
1236
|
+
const params = new URLSearchParams();
|
|
1237
|
+
if (opts.source) params.set("source_id", opts.source);
|
|
1238
|
+
if (opts.after) params.set("after", opts.after);
|
|
1239
|
+
if (opts.before) params.set("before", opts.before);
|
|
1240
|
+
params.set("granularity", opts.granularity);
|
|
1241
|
+
if (!opts.after) {
|
|
1242
|
+
params.set("after", new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString());
|
|
1243
|
+
}
|
|
1244
|
+
const qs = params.toString();
|
|
1245
|
+
const data = await client.get(
|
|
1246
|
+
`/metrics/volume${qs ? `?${qs}` : ""}`
|
|
1247
|
+
);
|
|
1248
|
+
if (isJsonMode()) {
|
|
1249
|
+
printJson(data);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (data.series.length === 0) {
|
|
1253
|
+
console.log(chalk4.dim("\n No volume data for this time range.\n"));
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
console.log();
|
|
1257
|
+
printTable(
|
|
1258
|
+
data.series.map((row) => ({
|
|
1259
|
+
bucket: formatBucket(row.bucket, data.granularity),
|
|
1260
|
+
source: row.source_name ?? row.source_id.slice(0, 12),
|
|
1261
|
+
events: String(row.event_count),
|
|
1262
|
+
bar: renderBar(row.event_count, Math.max(...data.series.map((r) => r.event_count)))
|
|
1263
|
+
})),
|
|
1264
|
+
[
|
|
1265
|
+
{ key: "bucket", label: "Time", width: 20 },
|
|
1266
|
+
{ key: "source", label: "Source", width: 20 },
|
|
1267
|
+
{ key: "events", label: "Events", width: 8 },
|
|
1268
|
+
{ key: "bar", label: "Volume", width: 20 }
|
|
1269
|
+
]
|
|
1270
|
+
);
|
|
1271
|
+
console.log();
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
function formatBucket(bucket, granularity) {
|
|
1278
|
+
const d = new Date(bucket);
|
|
1279
|
+
if (granularity === "day") {
|
|
1280
|
+
return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
|
|
1281
|
+
}
|
|
1282
|
+
return d.toLocaleString(void 0, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
1283
|
+
}
|
|
1284
|
+
function renderBar(value, max) {
|
|
1285
|
+
if (max === 0) return "";
|
|
1286
|
+
const width = 15;
|
|
1287
|
+
const filled = Math.round(value / max * width);
|
|
1288
|
+
return chalk4.cyan("\u2588".repeat(filled)) + chalk4.dim("\u2591".repeat(width - filled));
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/commands/metrics/index.ts
|
|
1292
|
+
var metricsCommand = new Command33("metrics").description("View metrics and stats").addCommand(metricsOverviewCommand).addCommand(metricsVolumeCommand);
|
|
1293
|
+
|
|
1294
|
+
// src/commands/test.ts
|
|
1295
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1296
|
+
import { Command as Command34 } from "commander";
|
|
1297
|
+
var DEFAULT_BASE_URL2 = "https://hookstream.io";
|
|
1298
|
+
var testCommand = new Command34("test").description("Send a test event to a source").argument("<source-id>", "Source ID to send the test event to").option("--payload <json>", "JSON payload string").option("--file <path>", "Read payload from a file").option("--method <method>", "HTTP method", "POST").option("-H, --header <header...>", "Headers in key:value format").option("--base-url <url>", "Override base URL").action(async (sourceId, opts) => {
|
|
1299
|
+
const config = loadConfig();
|
|
1300
|
+
const baseUrl = (opts.baseUrl || process.env.HOOKSTREAM_BASE_URL || config.base_url || DEFAULT_BASE_URL2).replace(/\/$/, "");
|
|
1301
|
+
let payload;
|
|
1302
|
+
if (opts.file) {
|
|
1303
|
+
try {
|
|
1304
|
+
payload = readFileSync2(opts.file, "utf-8");
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
printError(`Cannot read file: ${opts.file}`);
|
|
1307
|
+
process.exit(1);
|
|
1308
|
+
}
|
|
1309
|
+
} else if (opts.payload) {
|
|
1310
|
+
payload = opts.payload;
|
|
1311
|
+
} else {
|
|
1312
|
+
payload = JSON.stringify({ test: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1313
|
+
}
|
|
1314
|
+
const headers = {
|
|
1315
|
+
"Content-Type": "application/json"
|
|
1316
|
+
};
|
|
1317
|
+
if (opts.header) {
|
|
1318
|
+
for (const h of opts.header) {
|
|
1319
|
+
const idx = h.indexOf(":");
|
|
1320
|
+
if (idx === -1) {
|
|
1321
|
+
printError(`Invalid header format: ${h} (expected key:value)`);
|
|
1322
|
+
process.exit(1);
|
|
1323
|
+
}
|
|
1324
|
+
headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
try {
|
|
1328
|
+
const startMs = Date.now();
|
|
1329
|
+
const res = await publicRequest(baseUrl, `/v1/ingest/${sourceId}`, {
|
|
1330
|
+
method: opts.method.toUpperCase(),
|
|
1331
|
+
headers,
|
|
1332
|
+
body: payload
|
|
1333
|
+
});
|
|
1334
|
+
const latencyMs = Date.now() - startMs;
|
|
1335
|
+
const body = await res.json();
|
|
1336
|
+
if (!res.ok) {
|
|
1337
|
+
printError(body.error || `HTTP ${res.status}`);
|
|
1338
|
+
process.exit(1);
|
|
1339
|
+
}
|
|
1340
|
+
const eventId = body.event_id;
|
|
1341
|
+
if (isJsonMode()) {
|
|
1342
|
+
const result = { ...body, latency_ms: latencyMs };
|
|
1343
|
+
if (eventId && config.api_key) {
|
|
1344
|
+
try {
|
|
1345
|
+
const client = new hookstreamClient();
|
|
1346
|
+
const detail = await client.get(`/events/${eventId}`);
|
|
1347
|
+
result.event = detail.event;
|
|
1348
|
+
} catch {
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
printJson(result);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
printSuccess(`Event sent to ${sourceId.slice(0, 12)}`);
|
|
1355
|
+
console.log();
|
|
1356
|
+
printKeyValue([
|
|
1357
|
+
["Event ID", eventId || "\u2014"],
|
|
1358
|
+
["Method", opts.method.toUpperCase()],
|
|
1359
|
+
["Latency", `${latencyMs}ms`],
|
|
1360
|
+
["Payload", `${payload.length} bytes`]
|
|
1361
|
+
]);
|
|
1362
|
+
if (eventId && config.api_key) {
|
|
1363
|
+
try {
|
|
1364
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1365
|
+
const client = new hookstreamClient();
|
|
1366
|
+
const detail = await client.get(`/events/${eventId}`);
|
|
1367
|
+
const e = detail.event;
|
|
1368
|
+
printKeyValue([
|
|
1369
|
+
["Verification", statusColor(e.verification_status)],
|
|
1370
|
+
["Dedup", statusColor(e.dedup_status)]
|
|
1371
|
+
]);
|
|
1372
|
+
} catch {
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
console.log();
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1378
|
+
process.exit(1);
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
// src/commands/listen.ts
|
|
1383
|
+
import { Command as Command35 } from "commander";
|
|
1384
|
+
import chalk5 from "chalk";
|
|
1385
|
+
var DEFAULT_BASE_URL3 = "https://hookstream.io";
|
|
1386
|
+
var PROVIDER_SIGNATURES = [
|
|
1387
|
+
{ header: "stripe-signature", name: "Stripe" },
|
|
1388
|
+
{ header: "x-shopify-hmac-sha256", name: "Shopify" },
|
|
1389
|
+
{ header: "x-hub-signature-256", name: "GitHub" },
|
|
1390
|
+
{ header: "x-slack-signature", name: "Slack" },
|
|
1391
|
+
{ header: "x-twilio-signature", name: "Twilio" },
|
|
1392
|
+
{ header: "linear-signature", name: "Linear" },
|
|
1393
|
+
{ header: "paddle-signature", name: "Paddle" },
|
|
1394
|
+
{ header: "webhook-signature", name: "Webhooks" }
|
|
1395
|
+
];
|
|
1396
|
+
function detectProviderCli(headers) {
|
|
1397
|
+
const lower = {};
|
|
1398
|
+
for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v;
|
|
1399
|
+
for (const sig of PROVIDER_SIGNATURES) {
|
|
1400
|
+
if (lower[sig.header]) return sig.name;
|
|
1401
|
+
}
|
|
1402
|
+
const ua = lower["user-agent"] ?? "";
|
|
1403
|
+
if (/GitHub-Hookshot/i.test(ua)) return "GitHub";
|
|
1404
|
+
if (/Stripe/i.test(ua)) return "Stripe";
|
|
1405
|
+
if (/Shopify/i.test(ua)) return "Shopify";
|
|
1406
|
+
if (/Slackbot/i.test(ua)) return "Slack";
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
function formatBytes2(bytes) {
|
|
1410
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1411
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1412
|
+
}
|
|
1413
|
+
function truncate(str, max) {
|
|
1414
|
+
if (str.length <= max) return str;
|
|
1415
|
+
return str.slice(0, max - 3) + "...";
|
|
1416
|
+
}
|
|
1417
|
+
var listenCommand = new Command35("listen").description("Create a test URL and stream webhooks in real-time").option("--forward <url>", "Forward received webhooks to this URL").option("--full", "Show full payload (no truncation)").option("--base-url <url>", "Override base URL").action(async (opts) => {
|
|
1418
|
+
const config = loadConfig();
|
|
1419
|
+
const baseUrl = (opts.baseUrl || process.env.HOOKSTREAM_BASE_URL || config.base_url || DEFAULT_BASE_URL3).replace(/\/$/, "");
|
|
1420
|
+
const jsonMode2 = isJsonMode();
|
|
1421
|
+
try {
|
|
1422
|
+
let connect2 = function() {
|
|
1423
|
+
const ws = new WebSocket(wsFullUrl);
|
|
1424
|
+
ws.addEventListener("open", () => {
|
|
1425
|
+
reconnectAttempt = 0;
|
|
1426
|
+
});
|
|
1427
|
+
ws.addEventListener("message", async (event) => {
|
|
1428
|
+
try {
|
|
1429
|
+
const msg = JSON.parse(String(event.data));
|
|
1430
|
+
if (msg.type === "event.received") {
|
|
1431
|
+
const eventId = msg.data.id;
|
|
1432
|
+
try {
|
|
1433
|
+
const detailRes = await publicRequest(
|
|
1434
|
+
baseUrl,
|
|
1435
|
+
`/v1/test/sessions/${session.session_id}/events/${eventId}`,
|
|
1436
|
+
{ method: "GET" }
|
|
1437
|
+
);
|
|
1438
|
+
if (detailRes.ok) {
|
|
1439
|
+
const detail = await detailRes.json();
|
|
1440
|
+
if (jsonMode2) {
|
|
1441
|
+
console.log(JSON.stringify(detail));
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const time = new Date(detail.received_at).toLocaleTimeString();
|
|
1445
|
+
const method = detail.method;
|
|
1446
|
+
const ct = detail.content_type ?? "unknown";
|
|
1447
|
+
const size = formatBytes2(detail.payload_size);
|
|
1448
|
+
const headers = (() => {
|
|
1449
|
+
try {
|
|
1450
|
+
return JSON.parse(detail.headers);
|
|
1451
|
+
} catch {
|
|
1452
|
+
return {};
|
|
1453
|
+
}
|
|
1454
|
+
})();
|
|
1455
|
+
const provider = detectProviderCli(headers);
|
|
1456
|
+
const methodColor = method === "POST" ? chalk5.green : method === "GET" ? chalk5.blue : method === "PUT" ? chalk5.yellow : method === "DELETE" ? chalk5.red : chalk5.white;
|
|
1457
|
+
console.log(
|
|
1458
|
+
` ${chalk5.dim(`[${time}]`)} ${methodColor(method.padEnd(6))} ${chalk5.dim(ct.padEnd(24))} ${size.padEnd(8)} ${provider ? chalk5.magenta(`\u2190 ${provider}`) : ""}`
|
|
1459
|
+
);
|
|
1460
|
+
const payload = detail.payload ?? detail.payload_inline ?? "";
|
|
1461
|
+
if (payload) {
|
|
1462
|
+
const display = opts.full ? payload : truncate(payload, 120);
|
|
1463
|
+
console.log(` ${chalk5.dim(" ")} ${chalk5.dim(display)}`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (msg.type === "forward.result") {
|
|
1470
|
+
if (!jsonMode2) {
|
|
1471
|
+
const status = msg.data.status;
|
|
1472
|
+
const latency = msg.data.latency_ms;
|
|
1473
|
+
const color = status >= 200 && status < 300 ? chalk5.green : chalk5.red;
|
|
1474
|
+
console.log(
|
|
1475
|
+
` ${chalk5.dim(" ")} ${chalk5.dim("\u2192 Forward:")} ${color(String(status))} ${chalk5.dim(`${latency}ms`)}`
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
ws.addEventListener("close", () => {
|
|
1483
|
+
if (reconnectAttempt < maxReconnect) {
|
|
1484
|
+
reconnectAttempt++;
|
|
1485
|
+
const delay = Math.min(1e3 * Math.pow(2, reconnectAttempt), 3e4);
|
|
1486
|
+
setTimeout(connect2, delay);
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
ws.addEventListener("error", () => {
|
|
1490
|
+
});
|
|
1491
|
+
const shutdown = () => {
|
|
1492
|
+
ws.close();
|
|
1493
|
+
if (!jsonMode2) {
|
|
1494
|
+
console.log();
|
|
1495
|
+
console.log(
|
|
1496
|
+
chalk5.dim(` Session active for 14 days at ${baseUrl.replace(/https:\/\/hookstream\.\w+\.workers\.dev/, "https://hookstream.io")}/test/${session.session_id}`)
|
|
1497
|
+
);
|
|
1498
|
+
console.log();
|
|
1499
|
+
}
|
|
1500
|
+
process.exit(0);
|
|
1501
|
+
};
|
|
1502
|
+
process.on("SIGINT", shutdown);
|
|
1503
|
+
process.on("SIGTERM", shutdown);
|
|
1504
|
+
};
|
|
1505
|
+
var connect = connect2;
|
|
1506
|
+
const createRes = await publicRequest(baseUrl, "/v1/test/sessions", {
|
|
1507
|
+
method: "POST",
|
|
1508
|
+
headers: { "Content-Type": "application/json" },
|
|
1509
|
+
body: "{}"
|
|
1510
|
+
});
|
|
1511
|
+
if (!createRes.ok) {
|
|
1512
|
+
const err = await createRes.json();
|
|
1513
|
+
printError(err.error ?? `HTTP ${createRes.status}`);
|
|
1514
|
+
process.exit(1);
|
|
1515
|
+
}
|
|
1516
|
+
const session = await createRes.json();
|
|
1517
|
+
if (!jsonMode2) {
|
|
1518
|
+
console.log();
|
|
1519
|
+
console.log(chalk5.bold.magenta(" hookstream Webhook Listener"));
|
|
1520
|
+
console.log();
|
|
1521
|
+
console.log(` ${chalk5.dim("URL:")} ${chalk5.cyan(session.url)}`);
|
|
1522
|
+
console.log(` ${chalk5.dim("Inspector:")} ${chalk5.cyan(`${baseUrl.replace(/https:\/\/hookstream\.\w+\.workers\.dev/, "https://hookstream.io")}/test/${session.session_id}`)}`);
|
|
1523
|
+
console.log(` ${chalk5.dim("Expires:")} ${new Date(session.expires_at).toLocaleDateString()} (14 days)`);
|
|
1524
|
+
console.log(` ${chalk5.dim("Limit:")} ${session.request_limit} requests`);
|
|
1525
|
+
console.log();
|
|
1526
|
+
}
|
|
1527
|
+
if (opts.forward) {
|
|
1528
|
+
try {
|
|
1529
|
+
await publicRequest(baseUrl, `/v1/test/sessions/${session.session_id}/forwarding`, {
|
|
1530
|
+
method: "PATCH",
|
|
1531
|
+
headers: { "Content-Type": "application/json" },
|
|
1532
|
+
body: JSON.stringify({ url: opts.forward })
|
|
1533
|
+
});
|
|
1534
|
+
if (!jsonMode2) {
|
|
1535
|
+
console.log(` ${chalk5.dim("Forward:")} ${chalk5.yellow(opts.forward)}`);
|
|
1536
|
+
console.log();
|
|
1537
|
+
}
|
|
1538
|
+
} catch {
|
|
1539
|
+
if (!jsonMode2) {
|
|
1540
|
+
console.log(chalk5.yellow(" Warning: Could not configure forwarding"));
|
|
1541
|
+
console.log();
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (!jsonMode2) {
|
|
1546
|
+
console.log(chalk5.dim(" Listening... (Ctrl+C to stop)"));
|
|
1547
|
+
console.log();
|
|
1548
|
+
}
|
|
1549
|
+
const wsUrl = baseUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
1550
|
+
const wsFullUrl = `${wsUrl}/v1/ws/test/${session.session_id}`;
|
|
1551
|
+
let reconnectAttempt = 0;
|
|
1552
|
+
const maxReconnect = 10;
|
|
1553
|
+
connect2();
|
|
1554
|
+
await new Promise(() => {
|
|
1555
|
+
});
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1558
|
+
process.exit(1);
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
// src/commands/topics/index.ts
|
|
1563
|
+
import { Command as Command43 } from "commander";
|
|
1564
|
+
|
|
1565
|
+
// src/commands/topics/list.ts
|
|
1566
|
+
import { Command as Command36 } from "commander";
|
|
1567
|
+
var topicsListCommand = new Command36("list").description("List all topics").option("--status <status>", "Filter by status (active, paused)").action(async (opts) => {
|
|
1568
|
+
try {
|
|
1569
|
+
const client = new hookstreamClient();
|
|
1570
|
+
const params = new URLSearchParams();
|
|
1571
|
+
if (opts.status) params.set("status", opts.status);
|
|
1572
|
+
const qs = params.toString();
|
|
1573
|
+
const data = await client.get(`/topics${qs ? `?${qs}` : ""}`);
|
|
1574
|
+
if (isJsonMode()) {
|
|
1575
|
+
printJson(data.topics);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
console.log();
|
|
1579
|
+
printTable(
|
|
1580
|
+
data.topics.map((t) => ({
|
|
1581
|
+
id: t.id.slice(0, 12),
|
|
1582
|
+
name: t.name,
|
|
1583
|
+
slug: t.slug,
|
|
1584
|
+
status: statusColor(t.status),
|
|
1585
|
+
created: formatDate(t.created_at)
|
|
1586
|
+
})),
|
|
1587
|
+
[
|
|
1588
|
+
{ key: "id", label: "ID", width: 14 },
|
|
1589
|
+
{ key: "name", label: "Name", width: 24 },
|
|
1590
|
+
{ key: "slug", label: "Slug", width: 20 },
|
|
1591
|
+
{ key: "status", label: "Status", width: 10 },
|
|
1592
|
+
{ key: "created", label: "Created" }
|
|
1593
|
+
]
|
|
1594
|
+
);
|
|
1595
|
+
console.log();
|
|
1596
|
+
} catch (err) {
|
|
1597
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
// src/commands/topics/create.ts
|
|
1603
|
+
import { Command as Command37 } from "commander";
|
|
1604
|
+
function slugify3(name) {
|
|
1605
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1606
|
+
}
|
|
1607
|
+
var topicsCreateCommand = new Command37("create").description("Create a new topic").argument("<name>", "Topic name").option("--slug <slug>", "URL slug (auto-generated from name if omitted)").option("--description <desc>", "Topic description").action(async (name, opts) => {
|
|
1608
|
+
try {
|
|
1609
|
+
const client = new hookstreamClient();
|
|
1610
|
+
const slug = opts.slug || slugify3(name);
|
|
1611
|
+
const data = await client.post("/topics", {
|
|
1612
|
+
name,
|
|
1613
|
+
slug,
|
|
1614
|
+
description: opts.description || void 0
|
|
1615
|
+
});
|
|
1616
|
+
const t = data.topic;
|
|
1617
|
+
if (isJsonMode()) {
|
|
1618
|
+
printJson(t);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
printSuccess(`Topic created: ${t.name}`);
|
|
1622
|
+
console.log();
|
|
1623
|
+
printKeyValue([
|
|
1624
|
+
["ID", t.id],
|
|
1625
|
+
["Name", t.name],
|
|
1626
|
+
["Slug", t.slug],
|
|
1627
|
+
["Description", t.description],
|
|
1628
|
+
["Status", t.status]
|
|
1629
|
+
]);
|
|
1630
|
+
console.log();
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1633
|
+
process.exit(1);
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
// src/commands/topics/get.ts
|
|
1638
|
+
import { Command as Command38 } from "commander";
|
|
1639
|
+
var topicsGetCommand = new Command38("get").description("Get topic details").argument("<id>", "Topic ID").action(async (id) => {
|
|
1640
|
+
try {
|
|
1641
|
+
const client = new hookstreamClient();
|
|
1642
|
+
const data = await client.get(`/topics/${id}`);
|
|
1643
|
+
const t = data.topic;
|
|
1644
|
+
if (isJsonMode()) {
|
|
1645
|
+
printJson(data);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
console.log();
|
|
1649
|
+
printKeyValue([
|
|
1650
|
+
["ID", t.id],
|
|
1651
|
+
["Name", t.name],
|
|
1652
|
+
["Slug", t.slug],
|
|
1653
|
+
["Description", t.description],
|
|
1654
|
+
["Status", statusColor(t.status)],
|
|
1655
|
+
["Created", formatDate(t.created_at)],
|
|
1656
|
+
["Updated", formatDate(t.updated_at)]
|
|
1657
|
+
]);
|
|
1658
|
+
if (data.subscriptions && data.subscriptions.length > 0) {
|
|
1659
|
+
console.log();
|
|
1660
|
+
console.log(" Subscriptions:");
|
|
1661
|
+
console.log();
|
|
1662
|
+
printTable(
|
|
1663
|
+
data.subscriptions.map((s) => ({
|
|
1664
|
+
id: s.id.slice(0, 12),
|
|
1665
|
+
destination: s.destination_name || s.destination_id.slice(0, 12),
|
|
1666
|
+
filter: s.filter_rules ? "yes" : "\u2014",
|
|
1667
|
+
transform: s.transform_expression ? "yes" : "\u2014",
|
|
1668
|
+
created: formatDate(s.created_at)
|
|
1669
|
+
})),
|
|
1670
|
+
[
|
|
1671
|
+
{ key: "id", label: "Sub ID", width: 14 },
|
|
1672
|
+
{ key: "destination", label: "Destination", width: 20 },
|
|
1673
|
+
{ key: "filter", label: "Filter", width: 8 },
|
|
1674
|
+
{ key: "transform", label: "Transform", width: 10 },
|
|
1675
|
+
{ key: "created", label: "Created" }
|
|
1676
|
+
]
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
console.log();
|
|
1680
|
+
} catch (err) {
|
|
1681
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1682
|
+
process.exit(1);
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
// src/commands/topics/delete.ts
|
|
1687
|
+
import { createInterface as createInterface5 } from "readline/promises";
|
|
1688
|
+
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
1689
|
+
import { Command as Command39 } from "commander";
|
|
1690
|
+
var topicsDeleteCommand = new Command39("delete").description("Delete a topic").argument("<id>", "Topic ID").option("-f, --force", "Skip confirmation prompt").action(async (id, opts) => {
|
|
1691
|
+
try {
|
|
1692
|
+
if (!opts.force) {
|
|
1693
|
+
const rl = createInterface5({ input: stdin5, output: stdout5 });
|
|
1694
|
+
const answer = await rl.question(`Delete topic ${id}? This cannot be undone. [y/N] `);
|
|
1695
|
+
rl.close();
|
|
1696
|
+
if (answer.toLowerCase() !== "y") {
|
|
1697
|
+
console.log("Cancelled.");
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
const client = new hookstreamClient();
|
|
1702
|
+
await client.del(`/topics/${id}`);
|
|
1703
|
+
if (isJsonMode()) {
|
|
1704
|
+
printJson({ success: true, id });
|
|
1705
|
+
} else {
|
|
1706
|
+
printSuccess(`Topic ${id} deleted.`);
|
|
1707
|
+
}
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1710
|
+
process.exit(1);
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
// src/commands/topics/subscribe.ts
|
|
1715
|
+
import { Command as Command40 } from "commander";
|
|
1716
|
+
var topicsSubscribeCommand = new Command40("subscribe").description("Subscribe a destination to a topic").argument("<topic-id>", "Topic ID").argument("<destination-id>", "Destination ID").option("--filter <json>", "Filter rules as JSON").option("--transform <expr>", "JSONata transform expression").action(async (topicId, destinationId, opts) => {
|
|
1717
|
+
try {
|
|
1718
|
+
const client = new hookstreamClient();
|
|
1719
|
+
const body = {
|
|
1720
|
+
destination_id: destinationId
|
|
1721
|
+
};
|
|
1722
|
+
if (opts.filter) {
|
|
1723
|
+
body.filter_rules = JSON.parse(opts.filter);
|
|
1724
|
+
}
|
|
1725
|
+
if (opts.transform) {
|
|
1726
|
+
body.transform_expression = opts.transform;
|
|
1727
|
+
}
|
|
1728
|
+
const data = await client.post(
|
|
1729
|
+
`/topics/${topicId}/subscriptions`,
|
|
1730
|
+
body
|
|
1731
|
+
);
|
|
1732
|
+
const s = data.subscription;
|
|
1733
|
+
if (isJsonMode()) {
|
|
1734
|
+
printJson(s);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
printSuccess(`Subscription created`);
|
|
1738
|
+
console.log();
|
|
1739
|
+
printKeyValue([
|
|
1740
|
+
["Subscription ID", s.id],
|
|
1741
|
+
["Topic ID", s.topic_id],
|
|
1742
|
+
["Destination ID", s.destination_id],
|
|
1743
|
+
["Filter", s.filter_rules || "none"],
|
|
1744
|
+
["Transform", s.transform_expression || "none"]
|
|
1745
|
+
]);
|
|
1746
|
+
console.log();
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1749
|
+
process.exit(1);
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
// src/commands/topics/unsubscribe.ts
|
|
1754
|
+
import { Command as Command41 } from "commander";
|
|
1755
|
+
var topicsUnsubscribeCommand = new Command41("unsubscribe").description("Remove a subscription from a topic").argument("<topic-id>", "Topic ID").argument("<subscription-id>", "Subscription ID").action(async (topicId, subscriptionId) => {
|
|
1756
|
+
try {
|
|
1757
|
+
const client = new hookstreamClient();
|
|
1758
|
+
await client.del(`/topics/${topicId}/subscriptions/${subscriptionId}`);
|
|
1759
|
+
if (isJsonMode()) {
|
|
1760
|
+
printJson({ success: true, topic_id: topicId, subscription_id: subscriptionId });
|
|
1761
|
+
} else {
|
|
1762
|
+
printSuccess(`Subscription ${subscriptionId} removed from topic ${topicId}.`);
|
|
1763
|
+
}
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1766
|
+
process.exit(1);
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
// src/commands/topics/publish.ts
|
|
1771
|
+
import { Command as Command42 } from "commander";
|
|
1772
|
+
var topicsPublishCommand = new Command42("publish").description("Publish an event to a topic").argument("<slug>", "Topic slug").requiredOption("--data <json>", "Event payload as JSON").action(async (slug, opts) => {
|
|
1773
|
+
try {
|
|
1774
|
+
const client = new hookstreamClient();
|
|
1775
|
+
const payload = JSON.parse(opts.data);
|
|
1776
|
+
const data = await client.post(
|
|
1777
|
+
`/topics/${slug}/publish`,
|
|
1778
|
+
payload
|
|
1779
|
+
);
|
|
1780
|
+
if (isJsonMode()) {
|
|
1781
|
+
printJson(data);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
printSuccess(`Event published to topic "${slug}"`);
|
|
1785
|
+
console.log(` Event ID: ${data.event_id}`);
|
|
1786
|
+
console.log(` Deliveries: ${data.deliveries}`);
|
|
1787
|
+
console.log();
|
|
1788
|
+
} catch (err) {
|
|
1789
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1790
|
+
process.exit(1);
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
// src/commands/topics/index.ts
|
|
1795
|
+
var topicsCommand = new Command43("topics").description("Manage topics and pub/sub").addCommand(topicsListCommand).addCommand(topicsCreateCommand).addCommand(topicsGetCommand).addCommand(topicsDeleteCommand).addCommand(topicsSubscribeCommand).addCommand(topicsUnsubscribeCommand).addCommand(topicsPublishCommand);
|
|
1796
|
+
|
|
1797
|
+
// src/commands/replay/index.ts
|
|
1798
|
+
import { Command as Command46 } from "commander";
|
|
1799
|
+
|
|
1800
|
+
// src/commands/replay/create.ts
|
|
1801
|
+
import { Command as Command44 } from "commander";
|
|
1802
|
+
var replayCreateCommand = new Command44("create").description("Create an event replay job").requiredOption("--destination <id>", "Destination ID to replay events to").requiredOption("--from <iso>", "Start time (ISO 8601)").requiredOption("--to <iso>", "End time (ISO 8601)").option("--source <id>", "Filter by source ID").option("--rate-limit <n>", "Max events per second", parseInt).option("--max-events <n>", "Maximum events to replay", parseInt).action(async (opts) => {
|
|
1803
|
+
try {
|
|
1804
|
+
const client = new hookstreamClient();
|
|
1805
|
+
const body = {
|
|
1806
|
+
destination_id: opts.destination,
|
|
1807
|
+
from: opts.from,
|
|
1808
|
+
to: opts.to
|
|
1809
|
+
};
|
|
1810
|
+
if (opts.source) body.source_id = opts.source;
|
|
1811
|
+
if (opts.rateLimit) body.rate_limit = opts.rateLimit;
|
|
1812
|
+
if (opts.maxEvents) body.max_events = opts.maxEvents;
|
|
1813
|
+
const data = await client.post("/replay", body);
|
|
1814
|
+
const j = data.job;
|
|
1815
|
+
if (isJsonMode()) {
|
|
1816
|
+
printJson(j);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
printSuccess(`Replay job created`);
|
|
1820
|
+
console.log();
|
|
1821
|
+
printKeyValue([
|
|
1822
|
+
["Job ID", j.id],
|
|
1823
|
+
["Destination", j.destination_id],
|
|
1824
|
+
["Source", j.source_id || "all"],
|
|
1825
|
+
["From", j.from],
|
|
1826
|
+
["To", j.to],
|
|
1827
|
+
["Total Events", String(j.total)],
|
|
1828
|
+
["Rate Limit", String(j.rate_limit)],
|
|
1829
|
+
["Status", j.status]
|
|
1830
|
+
]);
|
|
1831
|
+
console.log();
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1834
|
+
process.exit(1);
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
// src/commands/replay/status.ts
|
|
1839
|
+
import { Command as Command45 } from "commander";
|
|
1840
|
+
var replayStatusCommand = new Command45("status").description("Get replay job status").argument("<id>", "Replay job ID").action(async (id) => {
|
|
1841
|
+
try {
|
|
1842
|
+
const client = new hookstreamClient();
|
|
1843
|
+
const data = await client.get(`/replay/${id}`);
|
|
1844
|
+
const j = data.job;
|
|
1845
|
+
if (isJsonMode()) {
|
|
1846
|
+
printJson(j);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const progress = j.total > 0 ? Math.round(j.processed / j.total * 100) : 0;
|
|
1850
|
+
console.log();
|
|
1851
|
+
printKeyValue([
|
|
1852
|
+
["Job ID", j.id],
|
|
1853
|
+
["Status", statusColor(j.status)],
|
|
1854
|
+
["Destination", j.destination_id],
|
|
1855
|
+
["Source", j.source_id || "all"],
|
|
1856
|
+
["From", j.from],
|
|
1857
|
+
["To", j.to],
|
|
1858
|
+
["Progress", `${j.processed}/${j.total} (${progress}%)`],
|
|
1859
|
+
["Succeeded", String(j.succeeded)],
|
|
1860
|
+
["Failed", String(j.failed)],
|
|
1861
|
+
["Rate Limit", String(j.rate_limit)],
|
|
1862
|
+
["Started", formatDate(j.started_at)],
|
|
1863
|
+
["Completed", formatDate(j.completed_at)]
|
|
1864
|
+
]);
|
|
1865
|
+
console.log();
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1868
|
+
process.exit(1);
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
// src/commands/replay/index.ts
|
|
1873
|
+
var replayCommand = new Command46("replay").description("Replay historical events").addCommand(replayCreateCommand).addCommand(replayStatusCommand);
|
|
1874
|
+
|
|
1875
|
+
// src/commands/billing/index.ts
|
|
1876
|
+
import { Command as Command49 } from "commander";
|
|
1877
|
+
|
|
1878
|
+
// src/commands/billing/usage.ts
|
|
1879
|
+
import { Command as Command47 } from "commander";
|
|
1880
|
+
import chalk6 from "chalk";
|
|
1881
|
+
var billingUsageCommand = new Command47("usage").description("Show current plan usage").action(async () => {
|
|
1882
|
+
try {
|
|
1883
|
+
const client = new hookstreamClient();
|
|
1884
|
+
const data = await client.get("/billing/usage");
|
|
1885
|
+
if (isJsonMode()) {
|
|
1886
|
+
printJson(data);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const pct = data.events.percentage;
|
|
1890
|
+
const bar = progressBar(pct, 30);
|
|
1891
|
+
const pctColor = pct >= 90 ? chalk6.red : pct >= 80 ? chalk6.yellow : chalk6.green;
|
|
1892
|
+
console.log();
|
|
1893
|
+
console.log(chalk6.bold(" Plan:"), chalk6.cyan(data.plan.toUpperCase()));
|
|
1894
|
+
console.log();
|
|
1895
|
+
console.log(chalk6.dim(" Events this month"));
|
|
1896
|
+
console.log(` ${bar} ${pctColor(`${pct}%`)}`);
|
|
1897
|
+
console.log(chalk6.dim(` ${data.events.used.toLocaleString()} / ${data.events.limit === Infinity ? "Unlimited" : data.events.limit.toLocaleString()}`));
|
|
1898
|
+
console.log();
|
|
1899
|
+
printKeyValue([
|
|
1900
|
+
["Sources", data.sources.limit === Infinity ? `${data.sources.used} (unlimited)` : `${data.sources.used} / ${data.sources.limit}`],
|
|
1901
|
+
["Destinations", data.destinations.limit === Infinity ? `${data.destinations.used} (unlimited)` : `${data.destinations.used} / ${data.destinations.limit}`],
|
|
1902
|
+
["Retention", `${data.retention_days} days`]
|
|
1903
|
+
]);
|
|
1904
|
+
console.log();
|
|
1905
|
+
} catch (err) {
|
|
1906
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1907
|
+
process.exit(1);
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
function progressBar(pct, width) {
|
|
1911
|
+
const filled = Math.round(Math.min(pct, 100) / 100 * width);
|
|
1912
|
+
const empty = width - filled;
|
|
1913
|
+
const color = pct >= 90 ? chalk6.red : pct >= 80 ? chalk6.yellow : chalk6.green;
|
|
1914
|
+
return color("\u2588".repeat(filled)) + chalk6.dim("\u2591".repeat(empty));
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// src/commands/billing/subscription.ts
|
|
1918
|
+
import { Command as Command48 } from "commander";
|
|
1919
|
+
import chalk7 from "chalk";
|
|
1920
|
+
var billingSubscriptionCommand = new Command48("subscription").description("Show current subscription details").action(async () => {
|
|
1921
|
+
try {
|
|
1922
|
+
const client = new hookstreamClient();
|
|
1923
|
+
const data = await client.get("/billing/subscription");
|
|
1924
|
+
if (isJsonMode()) {
|
|
1925
|
+
printJson(data);
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
console.log();
|
|
1929
|
+
if (!data.subscription) {
|
|
1930
|
+
console.log(chalk7.dim(" No active subscription. You are on the Free plan."));
|
|
1931
|
+
console.log(chalk7.dim(" Upgrade at https://hookstream.io/settings?tab=billing"));
|
|
1932
|
+
} else {
|
|
1933
|
+
const sub = data.subscription;
|
|
1934
|
+
const statusColor2 = sub.status === "active" ? chalk7.green : sub.status === "past_due" ? chalk7.yellow : chalk7.red;
|
|
1935
|
+
printKeyValue([
|
|
1936
|
+
["Plan", chalk7.cyan(sub.plan.toUpperCase())],
|
|
1937
|
+
["Status", statusColor2(sub.status)],
|
|
1938
|
+
["Period Start", formatDate(sub.current_period_start)],
|
|
1939
|
+
["Period End", formatDate(sub.current_period_end)],
|
|
1940
|
+
["Cancel at Period End", sub.cancel_at_period_end ? chalk7.yellow("Yes") : "No"],
|
|
1941
|
+
["Canceled At", sub.canceled_at ? formatDate(sub.canceled_at) : "\u2014"]
|
|
1942
|
+
]);
|
|
1943
|
+
}
|
|
1944
|
+
console.log();
|
|
1945
|
+
} catch (err) {
|
|
1946
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
// src/commands/billing/index.ts
|
|
1952
|
+
var billingCommand = new Command49("billing").description("View billing, usage, and subscription info").addCommand(billingUsageCommand).addCommand(billingSubscriptionCommand);
|
|
1953
|
+
|
|
1954
|
+
// src/index.ts
|
|
1955
|
+
var program = new Command50();
|
|
1956
|
+
program.name("hookstream").description("hookstream CLI \u2014 manage webhooks from the terminal").version("0.1.0").option("--json", "Output as JSON").hook("preAction", (thisCommand) => {
|
|
1957
|
+
const opts = thisCommand.optsWithGlobals();
|
|
1958
|
+
if (opts.json) {
|
|
1959
|
+
setJsonMode(true);
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
program.addCommand(loginCommand);
|
|
1963
|
+
program.addCommand(logoutCommand);
|
|
1964
|
+
program.addCommand(whoamiCommand);
|
|
1965
|
+
program.addCommand(sourcesCommand);
|
|
1966
|
+
program.addCommand(destinationsCommand);
|
|
1967
|
+
program.addCommand(connectionsCommand);
|
|
1968
|
+
program.addCommand(eventsCommand);
|
|
1969
|
+
program.addCommand(deliveriesCommand);
|
|
1970
|
+
program.addCommand(metricsCommand);
|
|
1971
|
+
program.addCommand(testCommand);
|
|
1972
|
+
program.addCommand(listenCommand);
|
|
1973
|
+
program.addCommand(topicsCommand);
|
|
1974
|
+
program.addCommand(replayCommand);
|
|
1975
|
+
program.addCommand(billingCommand);
|
|
1976
|
+
program.parse();
|