@better-webhook/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Endalkachew Biruk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Better Webhook CLI
2
+
3
+ Simple CLI for listing, downloading, and executing predefined webhook JSON payloads stored in a local `.webhooks` directory.
4
+
5
+ ## Install (workspace)
6
+
7
+ From repo root after cloning:
8
+
9
+ ```bash
10
+ pnpm install
11
+ pnpm --filter @better-webhook/cli build
12
+ ```
13
+
14
+ Run via pnpm:
15
+
16
+ ```bash
17
+ pnpm --filter @better-webhook/cli exec better-webhook list
18
+ ```
19
+
20
+ Or in dev (watch) mode:
21
+
22
+ ```bash
23
+ pnpm --filter @better-webhook/cli dev run sample
24
+ ```
25
+
26
+ ## Install (published)
27
+
28
+ After publishing to npm you can use:
29
+
30
+ ```bash
31
+ npx @better-webhook/cli list
32
+ # or (after global install)
33
+ pnpm add -g @better-webhook/cli
34
+ better-webhook list
35
+ ```
36
+
37
+ The binary name is `better-webhook`.
38
+
39
+ ## Publishing (maintainers)
40
+
41
+ 1. Update version (pnpm):
42
+ ```bash
43
+ pnpm --filter @better-webhook/cli version patch # or minor / major
44
+ ```
45
+ 2. Build & publish (ensure you are logged in with `npm whoami`):
46
+ ```bash
47
+ pnpm --filter @better-webhook/cli run build
48
+ cd apps/webhook-cli
49
+ npm publish --access public
50
+ ```
51
+ (The `prepublishOnly` script also builds/validates automatically.)
52
+ 3. Test install:
53
+ ```bash
54
+ pnpm dlx @better-webhook/cli@latest --help
55
+ ```
56
+
57
+ ## Webhook File Schema
58
+
59
+ Every webhook JSON file MUST conform to this schema (validated with `zod`):
60
+
61
+ ```jsonc
62
+ {
63
+ "url": "https://example.com/endpoint", // required, valid URL
64
+ "method": "POST", // optional, defaults to POST (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
65
+ "headers": [
66
+ // optional, defaults to []
67
+ { "key": "X-Custom", "value": "abc" },
68
+ ],
69
+ "body": {
70
+ // optional (omit for methods w/out body)
71
+ "event": "user.created",
72
+ "data": { "id": "123" },
73
+ },
74
+ }
75
+ ```
76
+
77
+ Validation errors list all failing fields with context.
78
+
79
+ ## Directory Structure
80
+
81
+ ```
82
+ ./.webhooks/
83
+ user_created.json
84
+ order_paid.json
85
+ ```
86
+
87
+ ## Commands
88
+
89
+ ### List
90
+
91
+ ```
92
+ better-webhook list
93
+ ```
94
+
95
+ Lists all JSON filenames (without extension) in `.webhooks`.
96
+
97
+ ### Download Templates
98
+
99
+ ```
100
+ better-webhook download # lists available templates
101
+ better-webhook download stripe-invoice.payment_succeeded
102
+ better-webhook download --all
103
+ ```
104
+
105
+ ### Run
106
+
107
+ ```
108
+ better-webhook run user_created
109
+ better-webhook run path/to/file.json
110
+ ```
111
+
112
+ Overrides:
113
+
114
+ ```
115
+ --url https://override.test/hook
116
+ --method PUT
117
+ ```
118
+
119
+ ### Examples
120
+
121
+ Minimal:
122
+
123
+ ```json
124
+ { "url": "https://example.com/hook" }
125
+ ```
126
+
127
+ With headers + body:
128
+
129
+ ```json
130
+ {
131
+ "url": "https://example.com/hook",
132
+ "method": "POST",
133
+ "headers": [
134
+ { "key": "X-Env", "value": "staging" },
135
+ { "key": "Authorization", "value": "Bearer TOKEN" }
136
+ ],
137
+ "body": { "event": "deploy", "status": "ok" }
138
+ }
139
+ ```
140
+
141
+ ## Output
142
+
143
+ - Prints status code
144
+ - Prints response headers
145
+ - Pretty-prints JSON response or raw body
146
+
147
+ ## Error Handling
148
+
149
+ - Invalid JSON -> fails with parse message
150
+ - Schema violations -> detailed list of issues
151
+ - Network errors -> reported with non-zero exit code
152
+
153
+ ## Notes
154
+
155
+ - Content-Type automatically set to `application/json` if body present and not already specified.
156
+ - Headers defined later override earlier duplicates.
package/dist/index.cjs ADDED
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/index.ts
5
+ var import_commander = require("commander");
6
+ var import_fs2 = require("fs");
7
+ var import_path = require("path");
8
+
9
+ // src/loader.ts
10
+ var import_fs = require("fs");
11
+
12
+ // src/schema.ts
13
+ var import_zod = require("zod");
14
+ var httpMethodSchema = import_zod.z.enum([
15
+ "GET",
16
+ "POST",
17
+ "PUT",
18
+ "PATCH",
19
+ "DELETE",
20
+ "HEAD",
21
+ "OPTIONS"
22
+ ]);
23
+ var headerEntrySchema = import_zod.z.object({
24
+ key: import_zod.z.string().min(1, "Header key cannot be empty").regex(/^[A-Za-z0-9-]+$/, {
25
+ message: "Header key must contain only alphanumerics and -"
26
+ }),
27
+ value: import_zod.z.string().min(1, "Header value cannot be empty")
28
+ });
29
+ var webhookSchema = import_zod.z.object({
30
+ url: import_zod.z.string().url("Invalid URL"),
31
+ method: httpMethodSchema.default("POST"),
32
+ headers: import_zod.z.array(headerEntrySchema).default([]),
33
+ body: import_zod.z.any().optional()
34
+ // could be anything JSON-serializable
35
+ }).strict();
36
+ function validateWebhookJSON(raw, source) {
37
+ const parsed = webhookSchema.safeParse(raw);
38
+ if (!parsed.success) {
39
+ const issues = parsed.error.issues.map((i) => `- ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
40
+ throw new Error(`Invalid webhook definition in ${source}:
41
+ ${issues}`);
42
+ }
43
+ return parsed.data;
44
+ }
45
+
46
+ // src/loader.ts
47
+ function loadWebhookFile(path) {
48
+ let rawContent;
49
+ try {
50
+ rawContent = (0, import_fs.readFileSync)(path, "utf8");
51
+ } catch (e) {
52
+ throw new Error(`Failed to read file ${path}: ${e.message}`);
53
+ }
54
+ let json;
55
+ try {
56
+ json = JSON.parse(rawContent);
57
+ } catch (e) {
58
+ throw new Error(`Invalid JSON in file ${path}: ${e.message}`);
59
+ }
60
+ return validateWebhookJSON(json, path);
61
+ }
62
+
63
+ // src/http.ts
64
+ var import_undici = require("undici");
65
+ async function executeWebhook(def) {
66
+ const headerMap = {};
67
+ for (const h of def.headers) {
68
+ headerMap[h.key] = h.value;
69
+ }
70
+ if (!headerMap["content-type"] && def.body !== void 0) {
71
+ headerMap["content-type"] = "application/json";
72
+ }
73
+ const bodyPayload = def.body !== void 0 ? JSON.stringify(def.body) : void 0;
74
+ const { statusCode, headers, body } = await (0, import_undici.request)(def.url, {
75
+ method: def.method,
76
+ headers: headerMap,
77
+ body: bodyPayload
78
+ });
79
+ const text = await body.text();
80
+ let parsed;
81
+ if (text) {
82
+ try {
83
+ parsed = JSON.parse(text);
84
+ } catch {
85
+ }
86
+ }
87
+ const resultHeaders = {};
88
+ for (const [k, v] of Object.entries(headers)) {
89
+ resultHeaders[k] = v;
90
+ }
91
+ return {
92
+ status: statusCode,
93
+ headers: resultHeaders,
94
+ bodyText: text,
95
+ json: parsed
96
+ };
97
+ }
98
+
99
+ // src/index.ts
100
+ var import_undici2 = require("undici");
101
+ var program = new import_commander.Command();
102
+ program.name("better-webhook").description("CLI for listing, downloading and executing predefined webhooks").version("0.2.0");
103
+ function findWebhooksDir(cwd) {
104
+ return (0, import_path.resolve)(cwd, ".webhooks");
105
+ }
106
+ function listWebhookFiles(dir) {
107
+ try {
108
+ const entries = (0, import_fs2.readdirSync)(dir);
109
+ return entries.filter(
110
+ (e) => (0, import_fs2.statSync)((0, import_path.join)(dir, e)).isFile() && (0, import_path.extname)(e) === ".json"
111
+ );
112
+ } catch {
113
+ return [];
114
+ }
115
+ }
116
+ var TEMPLATE_REPO_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
117
+ var TEMPLATES = {
118
+ "stripe-invoice.payment_succeeded": "templates/stripe-invoice.payment_succeeded.json"
119
+ };
120
+ program.command("download [name]").description(
121
+ "Download official webhook template(s) into the .webhooks directory. If no name is provided, prints available templates."
122
+ ).option("-a, --all", "Download all available templates").option("-f, --force", "Overwrite existing files if they exist").action(
123
+ async (name, opts) => {
124
+ if (name && opts.all) {
125
+ console.error("Specify either a template name or --all, not both.");
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+ const cwd = process.cwd();
130
+ const dir = findWebhooksDir(cwd);
131
+ (0, import_fs2.mkdirSync)(dir, { recursive: true });
132
+ const toDownload = opts.all ? Object.keys(TEMPLATES) : name ? [name] : [];
133
+ if (!toDownload.length) {
134
+ console.log("Available templates:");
135
+ for (const key of Object.keys(TEMPLATES)) console.log(` - ${key}`);
136
+ console.log("Use: better-webhook download <name> OR --all");
137
+ return;
138
+ }
139
+ for (const templateName of toDownload) {
140
+ const rel = TEMPLATES[templateName];
141
+ if (!rel) {
142
+ console.error(
143
+ `Unknown template '${templateName}'. Run without arguments to list available templates.`
144
+ );
145
+ continue;
146
+ }
147
+ const rawUrl = `${TEMPLATE_REPO_BASE}/${rel}`;
148
+ try {
149
+ const { statusCode, body } = await (0, import_undici2.request)(rawUrl);
150
+ if (statusCode !== 200) {
151
+ console.error(
152
+ `Failed to fetch ${templateName} (HTTP ${statusCode}) from ${rawUrl}`
153
+ );
154
+ continue;
155
+ }
156
+ const text = await body.text();
157
+ let json;
158
+ try {
159
+ json = JSON.parse(text);
160
+ } catch (e) {
161
+ console.error(
162
+ `Invalid JSON in remote template ${templateName}: ${e.message}`
163
+ );
164
+ continue;
165
+ }
166
+ try {
167
+ validateWebhookJSON(json, rawUrl);
168
+ } catch (e) {
169
+ console.error(`Template failed schema validation: ${e.message}`);
170
+ continue;
171
+ }
172
+ const fileName = (0, import_path.basename)(rel);
173
+ const destPath = (0, import_path.join)(dir, fileName);
174
+ if ((0, import_fs2.existsSync)(destPath) && !opts.force) {
175
+ console.log(
176
+ `Skipping existing file ${fileName} (use --force to overwrite)`
177
+ );
178
+ continue;
179
+ }
180
+ (0, import_fs2.writeFileSync)(destPath, JSON.stringify(json, null, 2));
181
+ console.log(`Downloaded ${templateName} -> .webhooks/${fileName}`);
182
+ } catch (e) {
183
+ console.error(`Error downloading ${templateName}: ${e.message}`);
184
+ }
185
+ }
186
+ }
187
+ );
188
+ program.command("list").description("List available webhook JSON definitions in .webhooks directory").action(() => {
189
+ const cwd = process.cwd();
190
+ const dir = findWebhooksDir(cwd);
191
+ const files = listWebhookFiles(dir);
192
+ if (!files.length) {
193
+ console.log("No webhook definitions found in .webhooks");
194
+ return;
195
+ }
196
+ files.forEach((f) => console.log((0, import_path.basename)(f, ".json")));
197
+ });
198
+ program.command("run <nameOrPath>").description(
199
+ "Run a webhook by name (in .webhooks) or by providing a path to a JSON file"
200
+ ).option("-u, --url <url>", "Override destination URL").option("-m, --method <method>", "Override HTTP method").action(async (nameOrPath, options) => {
201
+ const cwd = process.cwd();
202
+ let filePath;
203
+ if (nameOrPath.endsWith(".json") && !nameOrPath.includes("/") && !nameOrPath.startsWith(".")) {
204
+ filePath = (0, import_path.join)(findWebhooksDir(cwd), nameOrPath);
205
+ } else {
206
+ const candidate = (0, import_path.join)(
207
+ findWebhooksDir(cwd),
208
+ nameOrPath + (nameOrPath.endsWith(".json") ? "" : ".json")
209
+ );
210
+ if (statExists(candidate)) {
211
+ filePath = candidate;
212
+ } else {
213
+ filePath = (0, import_path.resolve)(cwd, nameOrPath);
214
+ }
215
+ }
216
+ if (!statExists(filePath)) {
217
+ console.error(`Webhook file not found: ${filePath}`);
218
+ process.exitCode = 1;
219
+ return;
220
+ }
221
+ let def;
222
+ try {
223
+ def = loadWebhookFile(filePath);
224
+ } catch (err) {
225
+ console.error(err.message);
226
+ process.exitCode = 1;
227
+ return;
228
+ }
229
+ if (options.url) def = { ...def, url: options.url };
230
+ if (options.method)
231
+ def = { ...def, method: options.method.toUpperCase() };
232
+ try {
233
+ const result = await executeWebhook(def);
234
+ console.log("Status:", result.status);
235
+ console.log("Headers:");
236
+ for (const [k, v] of Object.entries(result.headers)) {
237
+ console.log(` ${k}: ${Array.isArray(v) ? v.join(", ") : v}`);
238
+ }
239
+ if (result.json !== void 0) {
240
+ console.log("Response JSON:");
241
+ console.log(JSON.stringify(result.json, null, 2));
242
+ } else {
243
+ console.log("Response Body:");
244
+ console.log(result.bodyText);
245
+ }
246
+ } catch (err) {
247
+ console.error("Request failed:", err.message);
248
+ process.exitCode = 1;
249
+ }
250
+ });
251
+ function statExists(p) {
252
+ try {
253
+ (0, import_fs2.statSync)(p);
254
+ return true;
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+ program.parseAsync(process.argv);
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import {
6
+ readdirSync,
7
+ statSync,
8
+ mkdirSync,
9
+ writeFileSync,
10
+ existsSync
11
+ } from "fs";
12
+ import { join, resolve, basename, extname } from "path";
13
+
14
+ // src/loader.ts
15
+ import { readFileSync } from "fs";
16
+
17
+ // src/schema.ts
18
+ import { z } from "zod";
19
+ var httpMethodSchema = z.enum([
20
+ "GET",
21
+ "POST",
22
+ "PUT",
23
+ "PATCH",
24
+ "DELETE",
25
+ "HEAD",
26
+ "OPTIONS"
27
+ ]);
28
+ var headerEntrySchema = z.object({
29
+ key: z.string().min(1, "Header key cannot be empty").regex(/^[A-Za-z0-9-]+$/, {
30
+ message: "Header key must contain only alphanumerics and -"
31
+ }),
32
+ value: z.string().min(1, "Header value cannot be empty")
33
+ });
34
+ var webhookSchema = z.object({
35
+ url: z.string().url("Invalid URL"),
36
+ method: httpMethodSchema.default("POST"),
37
+ headers: z.array(headerEntrySchema).default([]),
38
+ body: z.any().optional()
39
+ // could be anything JSON-serializable
40
+ }).strict();
41
+ function validateWebhookJSON(raw, source) {
42
+ const parsed = webhookSchema.safeParse(raw);
43
+ if (!parsed.success) {
44
+ const issues = parsed.error.issues.map((i) => `- ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
45
+ throw new Error(`Invalid webhook definition in ${source}:
46
+ ${issues}`);
47
+ }
48
+ return parsed.data;
49
+ }
50
+
51
+ // src/loader.ts
52
+ function loadWebhookFile(path) {
53
+ let rawContent;
54
+ try {
55
+ rawContent = readFileSync(path, "utf8");
56
+ } catch (e) {
57
+ throw new Error(`Failed to read file ${path}: ${e.message}`);
58
+ }
59
+ let json;
60
+ try {
61
+ json = JSON.parse(rawContent);
62
+ } catch (e) {
63
+ throw new Error(`Invalid JSON in file ${path}: ${e.message}`);
64
+ }
65
+ return validateWebhookJSON(json, path);
66
+ }
67
+
68
+ // src/http.ts
69
+ import { request } from "undici";
70
+ async function executeWebhook(def) {
71
+ const headerMap = {};
72
+ for (const h of def.headers) {
73
+ headerMap[h.key] = h.value;
74
+ }
75
+ if (!headerMap["content-type"] && def.body !== void 0) {
76
+ headerMap["content-type"] = "application/json";
77
+ }
78
+ const bodyPayload = def.body !== void 0 ? JSON.stringify(def.body) : void 0;
79
+ const { statusCode, headers, body } = await request(def.url, {
80
+ method: def.method,
81
+ headers: headerMap,
82
+ body: bodyPayload
83
+ });
84
+ const text = await body.text();
85
+ let parsed;
86
+ if (text) {
87
+ try {
88
+ parsed = JSON.parse(text);
89
+ } catch {
90
+ }
91
+ }
92
+ const resultHeaders = {};
93
+ for (const [k, v] of Object.entries(headers)) {
94
+ resultHeaders[k] = v;
95
+ }
96
+ return {
97
+ status: statusCode,
98
+ headers: resultHeaders,
99
+ bodyText: text,
100
+ json: parsed
101
+ };
102
+ }
103
+
104
+ // src/index.ts
105
+ import { request as request2 } from "undici";
106
+ var program = new Command();
107
+ program.name("better-webhook").description("CLI for listing, downloading and executing predefined webhooks").version("0.2.0");
108
+ function findWebhooksDir(cwd) {
109
+ return resolve(cwd, ".webhooks");
110
+ }
111
+ function listWebhookFiles(dir) {
112
+ try {
113
+ const entries = readdirSync(dir);
114
+ return entries.filter(
115
+ (e) => statSync(join(dir, e)).isFile() && extname(e) === ".json"
116
+ );
117
+ } catch {
118
+ return [];
119
+ }
120
+ }
121
+ var TEMPLATE_REPO_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
122
+ var TEMPLATES = {
123
+ "stripe-invoice.payment_succeeded": "templates/stripe-invoice.payment_succeeded.json"
124
+ };
125
+ program.command("download [name]").description(
126
+ "Download official webhook template(s) into the .webhooks directory. If no name is provided, prints available templates."
127
+ ).option("-a, --all", "Download all available templates").option("-f, --force", "Overwrite existing files if they exist").action(
128
+ async (name, opts) => {
129
+ if (name && opts.all) {
130
+ console.error("Specify either a template name or --all, not both.");
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+ const cwd = process.cwd();
135
+ const dir = findWebhooksDir(cwd);
136
+ mkdirSync(dir, { recursive: true });
137
+ const toDownload = opts.all ? Object.keys(TEMPLATES) : name ? [name] : [];
138
+ if (!toDownload.length) {
139
+ console.log("Available templates:");
140
+ for (const key of Object.keys(TEMPLATES)) console.log(` - ${key}`);
141
+ console.log("Use: better-webhook download <name> OR --all");
142
+ return;
143
+ }
144
+ for (const templateName of toDownload) {
145
+ const rel = TEMPLATES[templateName];
146
+ if (!rel) {
147
+ console.error(
148
+ `Unknown template '${templateName}'. Run without arguments to list available templates.`
149
+ );
150
+ continue;
151
+ }
152
+ const rawUrl = `${TEMPLATE_REPO_BASE}/${rel}`;
153
+ try {
154
+ const { statusCode, body } = await request2(rawUrl);
155
+ if (statusCode !== 200) {
156
+ console.error(
157
+ `Failed to fetch ${templateName} (HTTP ${statusCode}) from ${rawUrl}`
158
+ );
159
+ continue;
160
+ }
161
+ const text = await body.text();
162
+ let json;
163
+ try {
164
+ json = JSON.parse(text);
165
+ } catch (e) {
166
+ console.error(
167
+ `Invalid JSON in remote template ${templateName}: ${e.message}`
168
+ );
169
+ continue;
170
+ }
171
+ try {
172
+ validateWebhookJSON(json, rawUrl);
173
+ } catch (e) {
174
+ console.error(`Template failed schema validation: ${e.message}`);
175
+ continue;
176
+ }
177
+ const fileName = basename(rel);
178
+ const destPath = join(dir, fileName);
179
+ if (existsSync(destPath) && !opts.force) {
180
+ console.log(
181
+ `Skipping existing file ${fileName} (use --force to overwrite)`
182
+ );
183
+ continue;
184
+ }
185
+ writeFileSync(destPath, JSON.stringify(json, null, 2));
186
+ console.log(`Downloaded ${templateName} -> .webhooks/${fileName}`);
187
+ } catch (e) {
188
+ console.error(`Error downloading ${templateName}: ${e.message}`);
189
+ }
190
+ }
191
+ }
192
+ );
193
+ program.command("list").description("List available webhook JSON definitions in .webhooks directory").action(() => {
194
+ const cwd = process.cwd();
195
+ const dir = findWebhooksDir(cwd);
196
+ const files = listWebhookFiles(dir);
197
+ if (!files.length) {
198
+ console.log("No webhook definitions found in .webhooks");
199
+ return;
200
+ }
201
+ files.forEach((f) => console.log(basename(f, ".json")));
202
+ });
203
+ program.command("run <nameOrPath>").description(
204
+ "Run a webhook by name (in .webhooks) or by providing a path to a JSON file"
205
+ ).option("-u, --url <url>", "Override destination URL").option("-m, --method <method>", "Override HTTP method").action(async (nameOrPath, options) => {
206
+ const cwd = process.cwd();
207
+ let filePath;
208
+ if (nameOrPath.endsWith(".json") && !nameOrPath.includes("/") && !nameOrPath.startsWith(".")) {
209
+ filePath = join(findWebhooksDir(cwd), nameOrPath);
210
+ } else {
211
+ const candidate = join(
212
+ findWebhooksDir(cwd),
213
+ nameOrPath + (nameOrPath.endsWith(".json") ? "" : ".json")
214
+ );
215
+ if (statExists(candidate)) {
216
+ filePath = candidate;
217
+ } else {
218
+ filePath = resolve(cwd, nameOrPath);
219
+ }
220
+ }
221
+ if (!statExists(filePath)) {
222
+ console.error(`Webhook file not found: ${filePath}`);
223
+ process.exitCode = 1;
224
+ return;
225
+ }
226
+ let def;
227
+ try {
228
+ def = loadWebhookFile(filePath);
229
+ } catch (err) {
230
+ console.error(err.message);
231
+ process.exitCode = 1;
232
+ return;
233
+ }
234
+ if (options.url) def = { ...def, url: options.url };
235
+ if (options.method)
236
+ def = { ...def, method: options.method.toUpperCase() };
237
+ try {
238
+ const result = await executeWebhook(def);
239
+ console.log("Status:", result.status);
240
+ console.log("Headers:");
241
+ for (const [k, v] of Object.entries(result.headers)) {
242
+ console.log(` ${k}: ${Array.isArray(v) ? v.join(", ") : v}`);
243
+ }
244
+ if (result.json !== void 0) {
245
+ console.log("Response JSON:");
246
+ console.log(JSON.stringify(result.json, null, 2));
247
+ } else {
248
+ console.log("Response Body:");
249
+ console.log(result.bodyText);
250
+ }
251
+ } catch (err) {
252
+ console.error("Request failed:", err.message);
253
+ process.exitCode = 1;
254
+ }
255
+ });
256
+ function statExists(p) {
257
+ try {
258
+ statSync(p);
259
+ return true;
260
+ } catch {
261
+ return false;
262
+ }
263
+ }
264
+ program.parseAsync(process.argv);
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@better-webhook/cli",
3
+ "version": "0.2.0",
4
+ "description": "CLI for developing and replaying webhook payloads locally.",
5
+ "type": "module",
6
+ "bin": {
7
+ "better-webhook": "dist/index.js"
8
+ },
9
+ "main": "dist/index.cjs",
10
+ "module": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "keywords": [
24
+ "webhook",
25
+ "cli",
26
+ "developer-tools",
27
+ "testing",
28
+ "http"
29
+ ],
30
+ "license": "MIT",
31
+ "author": "Endalk <endalk200>",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/endalk200/better-webhook.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/endalk200/better-webhook/issues"
38
+ },
39
+ "homepage": "https://github.com/endalk200/better-webhook#readme",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "dependencies": {
47
+ "commander": "^14.0.1",
48
+ "undici": "^7.16.0",
49
+ "zod": "^4.1.8"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^24.3.1",
53
+ "tsup": "^8.5.0",
54
+ "tsx": "^4.19.2",
55
+ "typescript": "^5.6.3",
56
+ "@better-webhook/typescript-config": "0.0.0"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup",
60
+ "dev": "tsup --watch"
61
+ }
62
+ }