@claudemini/shit-cli 1.6.0 → 1.8.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/README.md +68 -0
- package/bin/shit.js +1 -0
- package/lib/log.js +15 -3
- package/lib/review.js +4 -0
- package/lib/summarize.js +37 -13
- package/lib/webhook.js +211 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,6 +124,7 @@ shit summarize <session-id> # Generate AI summary (requires API key)
|
|
|
124
124
|
| `doctor` | Diagnose and fix issues |
|
|
125
125
|
| `shadow` | List/inspect shadow branches |
|
|
126
126
|
| `clean` | Clean old sessions |
|
|
127
|
+
| `webhook` | Show/test webhook configuration |
|
|
127
128
|
|
|
128
129
|
## How It Works
|
|
129
130
|
|
|
@@ -248,12 +249,73 @@ if (history && history.length > 3) {
|
|
|
248
249
|
}
|
|
249
250
|
```
|
|
250
251
|
|
|
252
|
+
## Webhook
|
|
253
|
+
|
|
254
|
+
shit-cli can send webhook notifications to external systems (Slack, Lark, CI, custom platforms) when key events occur.
|
|
255
|
+
|
|
256
|
+
### Events
|
|
257
|
+
|
|
258
|
+
| Event | Trigger | Payload |
|
|
259
|
+
|-------|---------|---------|
|
|
260
|
+
| `session.ended` | Session ends (hook `session-end` / `stop`) | `summary.json` content |
|
|
261
|
+
| `review.completed` | `shit review` finishes | Review report |
|
|
262
|
+
|
|
263
|
+
### Configuration
|
|
264
|
+
|
|
265
|
+
Add a `webhooks` field to `.shit-logs/config.json`:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"webhooks": {
|
|
270
|
+
"url": "https://example.com/hook",
|
|
271
|
+
"events": ["session.ended", "review.completed"],
|
|
272
|
+
"secret": "hmac-secret-key",
|
|
273
|
+
"headers": { "X-Custom": "value" },
|
|
274
|
+
"timeout_ms": 5000,
|
|
275
|
+
"retry": 1
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Or use environment variables (higher priority than config.json):
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
export SHIT_WEBHOOK_URL=https://example.com/hook
|
|
284
|
+
export SHIT_WEBHOOK_SECRET=my-secret # HMAC-SHA256 signing
|
|
285
|
+
export SHIT_WEBHOOK_AUTH_TOKEN=bearer-token # Bearer auth (alternative to secret)
|
|
286
|
+
export SHIT_WEBHOOK_EVENTS=session.ended,review.completed
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Authentication
|
|
290
|
+
|
|
291
|
+
- **HMAC-SHA256** — Set `secret` or `SHIT_WEBHOOK_SECRET`. Adds `X-Signature-256: sha256=<hex>` header (GitHub-compatible format).
|
|
292
|
+
- **Bearer token** — Set `auth_token` or `SHIT_WEBHOOK_AUTH_TOKEN`. Adds `Authorization: Bearer <token>` header.
|
|
293
|
+
- If neither is set, requests are sent without authentication.
|
|
294
|
+
|
|
295
|
+
### Payload Format
|
|
296
|
+
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"event": "session.ended",
|
|
300
|
+
"timestamp": "2026-03-03T12:00:00.000Z",
|
|
301
|
+
"payload": { ... }
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Commands
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
shit webhook # Show current webhook configuration
|
|
309
|
+
shit webhook --test # Send a test ping to the configured URL
|
|
310
|
+
```
|
|
311
|
+
|
|
251
312
|
## AI Summary
|
|
252
313
|
|
|
253
314
|
Set one of these environment variables to enable AI-powered session summaries:
|
|
254
315
|
|
|
255
316
|
```bash
|
|
256
317
|
export OPENAI_API_KEY=sk-... # Uses gpt-4o-mini by default
|
|
318
|
+
export OPENAI_BASE_URL=https://api.openai.com/v1 # Optional: OpenAI-compatible base URL
|
|
257
319
|
export ANTHROPIC_API_KEY=sk-... # Uses claude-3-haiku by default
|
|
258
320
|
```
|
|
259
321
|
|
|
@@ -268,7 +330,13 @@ shit summarize <session-id>
|
|
|
268
330
|
|----------|-------------|
|
|
269
331
|
| `SHIT_LOG_DIR` | Custom log directory (default: `.shit-logs` in project root) |
|
|
270
332
|
| `OPENAI_API_KEY` | Enable AI summaries via OpenAI |
|
|
333
|
+
| `OPENAI_BASE_URL` | OpenAI-compatible base URL for summaries (default: `https://api.openai.com/v1`) |
|
|
334
|
+
| `OPENAI_ENDPOINT` | Full OpenAI-compatible endpoint (overrides `OPENAI_BASE_URL`) |
|
|
271
335
|
| `ANTHROPIC_API_KEY` | Enable AI summaries via Anthropic |
|
|
336
|
+
| `SHIT_WEBHOOK_URL` | Webhook endpoint URL |
|
|
337
|
+
| `SHIT_WEBHOOK_SECRET` | HMAC-SHA256 signing secret for webhooks |
|
|
338
|
+
| `SHIT_WEBHOOK_AUTH_TOKEN` | Bearer token for webhook authentication |
|
|
339
|
+
| `SHIT_WEBHOOK_EVENTS` | Comma-separated list of webhook events to subscribe to |
|
|
272
340
|
|
|
273
341
|
## Security
|
|
274
342
|
|
package/bin/shit.js
CHANGED
package/lib/log.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* Reads stdin, parses event, delegates to session/extract/report modules.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { appendFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { getProjectRoot, getLogDir } from './config.js';
|
|
11
11
|
import { loadState, saveState, processEvent, updateIndex } from './session.js';
|
|
12
12
|
import { extractIntent, extractChanges, classifySession } from './extract.js';
|
|
13
13
|
import { generateReports } from './report.js';
|
|
14
|
+
import { dispatchWebhook } from './webhook.js';
|
|
14
15
|
|
|
15
16
|
export default async function log(args) {
|
|
16
17
|
const hookType = args[0] || 'unknown';
|
|
@@ -50,8 +51,11 @@ export default async function log(args) {
|
|
|
50
51
|
const classification = classifySession(intent, changes);
|
|
51
52
|
generateReports(sessionDir, sessionId, state, intent, changes, classification);
|
|
52
53
|
|
|
53
|
-
// 4. On session end: checkpoint + update index
|
|
54
|
-
|
|
54
|
+
// 4. On session end: checkpoint + update index + webhook (idempotent — run once per session)
|
|
55
|
+
const isSessionEnd = ['session-end', 'SessionEnd', 'stop', 'session_end', 'end', 'onSessionEnd'].includes(hookType);
|
|
56
|
+
const endedSentinel = join(sessionDir, '.ended');
|
|
57
|
+
if (isSessionEnd && !existsSync(endedSentinel)) {
|
|
58
|
+
writeFileSync(endedSentinel, new Date().toISOString());
|
|
55
59
|
try {
|
|
56
60
|
const { commitCheckpoint } = await import('./checkpoint.js');
|
|
57
61
|
await commitCheckpoint(projectRoot, sessionDir, sessionId);
|
|
@@ -60,6 +64,14 @@ export default async function log(args) {
|
|
|
60
64
|
try {
|
|
61
65
|
updateIndex(logDir, sessionId, intent, classification, changes, state);
|
|
62
66
|
} catch { /* index is best-effort */ }
|
|
67
|
+
|
|
68
|
+
// Dispatch webhook for session end — must await before exit
|
|
69
|
+
try {
|
|
70
|
+
const { readFileSync } = await import('fs');
|
|
71
|
+
const summaryPath = join(sessionDir, 'summary.json');
|
|
72
|
+
const summary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
|
|
73
|
+
await dispatchWebhook(projectRoot, 'session.ended', summary);
|
|
74
|
+
} catch { /* webhook is best-effort */ }
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
process.exit(0);
|
package/lib/review.js
CHANGED
|
@@ -12,6 +12,7 @@ import { join } from 'path';
|
|
|
12
12
|
import { createHash } from 'crypto';
|
|
13
13
|
import { getLogDir, getProjectRoot, SESSION_ID_REGEX } from './config.js';
|
|
14
14
|
import { redactSecrets } from './redact.js';
|
|
15
|
+
import { dispatchWebhook } from './webhook.js';
|
|
15
16
|
|
|
16
17
|
const SEVERITY_SCORE = {
|
|
17
18
|
info: 1,
|
|
@@ -708,6 +709,9 @@ export default async function review(args) {
|
|
|
708
709
|
|
|
709
710
|
const report = buildReport(selectedSessions, options);
|
|
710
711
|
|
|
712
|
+
// Dispatch webhook for review completion
|
|
713
|
+
dispatchWebhook(projectRoot, 'review.completed', report).catch(() => {});
|
|
714
|
+
|
|
711
715
|
if (options.format === 'json') {
|
|
712
716
|
console.log(JSON.stringify(report, null, 2));
|
|
713
717
|
} else if (options.format === 'markdown') {
|
package/lib/summarize.js
CHANGED
|
@@ -16,24 +16,15 @@ const DEFAULT_CONFIG = {
|
|
|
16
16
|
model: 'gpt-4o-mini',
|
|
17
17
|
max_tokens: 1000,
|
|
18
18
|
temperature: 0.7,
|
|
19
|
+
openai_base_url: 'https://api.openai.com/v1',
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Get API configuration from environment or config file
|
|
23
24
|
*/
|
|
24
25
|
function getApiConfig(projectRoot) {
|
|
25
|
-
// Check for environment variables first
|
|
26
26
|
const config = { ...DEFAULT_CONFIG };
|
|
27
27
|
|
|
28
|
-
// OpenAI
|
|
29
|
-
if (process.env.OPENAI_API_KEY) {
|
|
30
|
-
config.provider = 'openai';
|
|
31
|
-
config.api_key = process.env.OPENAI_API_KEY;
|
|
32
|
-
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
33
|
-
config.provider = 'anthropic';
|
|
34
|
-
config.api_key = process.env.ANTHROPIC_API_KEY;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
28
|
// Check for project config
|
|
38
29
|
const configFile = join(projectRoot, '.shit-logs', 'config.json');
|
|
39
30
|
if (existsSync(configFile)) {
|
|
@@ -45,6 +36,22 @@ function getApiConfig(projectRoot) {
|
|
|
45
36
|
}
|
|
46
37
|
}
|
|
47
38
|
|
|
39
|
+
// Environment variables override file config
|
|
40
|
+
if (process.env.OPENAI_API_KEY) {
|
|
41
|
+
config.provider = 'openai';
|
|
42
|
+
config.api_key = process.env.OPENAI_API_KEY;
|
|
43
|
+
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
44
|
+
config.provider = 'anthropic';
|
|
45
|
+
config.api_key = process.env.ANTHROPIC_API_KEY;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (process.env.OPENAI_BASE_URL) {
|
|
49
|
+
config.openai_base_url = process.env.OPENAI_BASE_URL;
|
|
50
|
+
}
|
|
51
|
+
if (process.env.OPENAI_ENDPOINT) {
|
|
52
|
+
config.openai_endpoint = process.env.OPENAI_ENDPOINT;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
return config;
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -159,8 +166,21 @@ function buildSummarizePrompt(context) {
|
|
|
159
166
|
/**
|
|
160
167
|
* Call OpenAI API
|
|
161
168
|
*/
|
|
162
|
-
|
|
163
|
-
const
|
|
169
|
+
function resolveOpenAIEndpoint(config) {
|
|
170
|
+
const explicitEndpoint = (config.openai_endpoint || '').trim();
|
|
171
|
+
if (explicitEndpoint) {
|
|
172
|
+
return explicitEndpoint;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const baseUrl = String(config.openai_base_url || DEFAULT_CONFIG.openai_base_url).trim().replace(/\/+$/, '');
|
|
176
|
+
if (baseUrl.endsWith('/chat/completions')) {
|
|
177
|
+
return baseUrl;
|
|
178
|
+
}
|
|
179
|
+
return `${baseUrl}/chat/completions`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function callOpenAI(apiKey, endpoint, model, prompt, maxTokens, temperature) {
|
|
183
|
+
const response = await fetch(endpoint, {
|
|
164
184
|
method: 'POST',
|
|
165
185
|
headers: {
|
|
166
186
|
'Content-Type': 'application/json',
|
|
@@ -247,8 +267,10 @@ export async function summarizeSession(projectRoot, sessionId, sessionDir) {
|
|
|
247
267
|
config.temperature
|
|
248
268
|
);
|
|
249
269
|
} else {
|
|
270
|
+
const openaiEndpoint = resolveOpenAIEndpoint(config);
|
|
250
271
|
summary = await callOpenAI(
|
|
251
272
|
config.api_key,
|
|
273
|
+
openaiEndpoint,
|
|
252
274
|
config.model || 'gpt-4o-mini',
|
|
253
275
|
prompt,
|
|
254
276
|
config.max_tokens,
|
|
@@ -301,9 +323,11 @@ export default async function summarize(args) {
|
|
|
301
323
|
console.log('Usage: shit summarize <session-id>');
|
|
302
324
|
console.log('\nEnvironment variables:');
|
|
303
325
|
console.log(' OPENAI_API_KEY # Use OpenAI for summarization');
|
|
326
|
+
console.log(' OPENAI_BASE_URL # OpenAI-compatible base URL (e.g. https://api.openai.com/v1)');
|
|
327
|
+
console.log(' OPENAI_ENDPOINT # Full OpenAI-compatible endpoint (overrides OPENAI_BASE_URL)');
|
|
304
328
|
console.log(' ANTHROPIC_API_KEY # Use Anthropic for summarization');
|
|
305
329
|
console.log('\nConfiguration (.shit-logs/config.json):');
|
|
306
|
-
console.log(` {"provider": "openai", "model": "gpt-4o-mini"}`);
|
|
330
|
+
console.log(` {"provider": "openai", "model": "gpt-4o-mini", "openai_base_url": "https://api.openai.com/v1"}`);
|
|
307
331
|
process.exit(1);
|
|
308
332
|
}
|
|
309
333
|
|
package/lib/webhook.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook support for shit-cli.
|
|
3
|
+
* Fire-and-forget notifications for session.ended and review.completed events.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { createHmac } from 'crypto';
|
|
9
|
+
import { request as httpsRequest } from 'https';
|
|
10
|
+
import { request as httpRequest } from 'http';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load webhook configuration from .shit-logs/config.json + environment variables.
|
|
14
|
+
* Env vars take precedence over config.json values.
|
|
15
|
+
*/
|
|
16
|
+
export function loadWebhookConfig(projectRoot) {
|
|
17
|
+
let fileConfig = {};
|
|
18
|
+
const configPath = join(projectRoot, '.shit-logs', 'config.json');
|
|
19
|
+
if (existsSync(configPath)) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
22
|
+
fileConfig = raw.webhooks || {};
|
|
23
|
+
} catch { /* ignore malformed config */ }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const url = process.env.SHIT_WEBHOOK_URL || fileConfig.url;
|
|
27
|
+
if (!url) return null;
|
|
28
|
+
|
|
29
|
+
const envEvents = process.env.SHIT_WEBHOOK_EVENTS;
|
|
30
|
+
const events = envEvents
|
|
31
|
+
? envEvents.split(',').map(e => e.trim()).filter(Boolean)
|
|
32
|
+
: (Array.isArray(fileConfig.events) ? fileConfig.events : []);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
url,
|
|
36
|
+
events,
|
|
37
|
+
secret: process.env.SHIT_WEBHOOK_SECRET || fileConfig.secret || '',
|
|
38
|
+
auth_token: process.env.SHIT_WEBHOOK_AUTH_TOKEN || fileConfig.auth_token || '',
|
|
39
|
+
headers: fileConfig.headers || {},
|
|
40
|
+
timeout_ms: fileConfig.timeout_ms || 5000,
|
|
41
|
+
retry: typeof fileConfig.retry === 'number' ? fileConfig.retry : 1,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compute HMAC-SHA256 signature in GitHub-compatible format.
|
|
47
|
+
*/
|
|
48
|
+
function computeSignature(secret, body) {
|
|
49
|
+
const hmac = createHmac('sha256', secret);
|
|
50
|
+
hmac.update(body);
|
|
51
|
+
return `sha256=${hmac.digest('hex')}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Send a single HTTP POST request. Returns a promise that resolves/rejects.
|
|
56
|
+
*/
|
|
57
|
+
function httpPost(url, headers, body, timeoutMs) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const parsed = new URL(url);
|
|
60
|
+
const isHttps = parsed.protocol === 'https:';
|
|
61
|
+
const reqFn = isHttps ? httpsRequest : httpRequest;
|
|
62
|
+
|
|
63
|
+
const options = {
|
|
64
|
+
hostname: parsed.hostname,
|
|
65
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
66
|
+
path: parsed.pathname + parsed.search,
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'Content-Length': Buffer.byteLength(body),
|
|
71
|
+
...headers,
|
|
72
|
+
},
|
|
73
|
+
timeout: timeoutMs,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const req = reqFn(options, (res) => {
|
|
77
|
+
let data = '';
|
|
78
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
79
|
+
res.on('end', () => {
|
|
80
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
81
|
+
resolve({ status: res.statusCode, body: data });
|
|
82
|
+
} else {
|
|
83
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
req.on('timeout', () => {
|
|
89
|
+
req.destroy();
|
|
90
|
+
reject(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
91
|
+
});
|
|
92
|
+
req.on('error', reject);
|
|
93
|
+
req.write(body);
|
|
94
|
+
req.end();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Send webhook for a single config entry. Supports retry.
|
|
100
|
+
*/
|
|
101
|
+
export async function sendWebhook(config, event, payload) {
|
|
102
|
+
const body = JSON.stringify({
|
|
103
|
+
event,
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
payload,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const headers = { ...config.headers };
|
|
109
|
+
|
|
110
|
+
// Auth: HMAC signature or Bearer token (mutually exclusive, HMAC preferred)
|
|
111
|
+
if (config.secret) {
|
|
112
|
+
headers['X-Signature-256'] = computeSignature(config.secret, body);
|
|
113
|
+
} else if (config.auth_token) {
|
|
114
|
+
headers['Authorization'] = `Bearer ${config.auth_token}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const maxAttempts = (config.retry || 0) + 1;
|
|
118
|
+
let lastError;
|
|
119
|
+
|
|
120
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
121
|
+
try {
|
|
122
|
+
await httpPost(config.url, headers, body, config.timeout_ms || 5000);
|
|
123
|
+
return; // success
|
|
124
|
+
} catch (err) {
|
|
125
|
+
lastError = err;
|
|
126
|
+
if (attempt < maxAttempts) {
|
|
127
|
+
// Brief delay before retry
|
|
128
|
+
await new Promise(r => setTimeout(r, 500 * attempt));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw lastError;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Main entry point: load config, filter by event, dispatch webhook.
|
|
138
|
+
* Non-blocking — logs warnings to stderr, never throws.
|
|
139
|
+
*/
|
|
140
|
+
export async function dispatchWebhook(projectRoot, event, payload) {
|
|
141
|
+
try {
|
|
142
|
+
const config = loadWebhookConfig(projectRoot);
|
|
143
|
+
if (!config) return;
|
|
144
|
+
|
|
145
|
+
// If events list is configured, only fire for matching events
|
|
146
|
+
if (config.events.length > 0 && !config.events.includes(event)) return;
|
|
147
|
+
|
|
148
|
+
await sendWebhook(config, event, payload);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
process.stderr.write(`[shit-cli] webhook warning: ${err.message}\n`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* CLI command: shit webhook
|
|
156
|
+
* Shows current webhook configuration status.
|
|
157
|
+
*/
|
|
158
|
+
export default async function webhook(args) {
|
|
159
|
+
const { getProjectRoot } = await import('./config.js');
|
|
160
|
+
const projectRoot = getProjectRoot();
|
|
161
|
+
const config = loadWebhookConfig(projectRoot);
|
|
162
|
+
|
|
163
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
164
|
+
console.log('Usage: shit webhook [options]');
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log('Show current webhook configuration.');
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log('Options:');
|
|
169
|
+
console.log(' --test Send a test webhook ping');
|
|
170
|
+
console.log(' --help Show this help');
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log('Configuration:');
|
|
173
|
+
console.log(' Set webhooks in .shit-logs/config.json or via environment variables:');
|
|
174
|
+
console.log(' SHIT_WEBHOOK_URL Webhook endpoint URL');
|
|
175
|
+
console.log(' SHIT_WEBHOOK_SECRET HMAC-SHA256 signing secret');
|
|
176
|
+
console.log(' SHIT_WEBHOOK_AUTH_TOKEN Bearer token');
|
|
177
|
+
console.log(' SHIT_WEBHOOK_EVENTS Comma-separated event list');
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!config) {
|
|
182
|
+
console.log('No webhook configured.');
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log('Set SHIT_WEBHOOK_URL or add "webhooks" to .shit-logs/config.json');
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('Webhook configuration:');
|
|
189
|
+
console.log(` URL: ${config.url}`);
|
|
190
|
+
console.log(` Events: ${config.events.length > 0 ? config.events.join(', ') : '(all)'}`);
|
|
191
|
+
console.log(` Auth: ${config.secret ? 'HMAC-SHA256' : config.auth_token ? 'Bearer token' : 'none'}`);
|
|
192
|
+
console.log(` Timeout: ${config.timeout_ms}ms`);
|
|
193
|
+
console.log(` Retry: ${config.retry}`);
|
|
194
|
+
if (Object.keys(config.headers).length > 0) {
|
|
195
|
+
console.log(` Headers: ${Object.keys(config.headers).join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (args.includes('--test')) {
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log('Sending test ping...');
|
|
201
|
+
try {
|
|
202
|
+
await sendWebhook(config, 'ping', { message: 'shit-cli webhook test' });
|
|
203
|
+
console.log('OK — webhook delivered successfully.');
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error(`Failed: ${err.message}`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return 0;
|
|
211
|
+
}
|