@ulthon/ul-opencode-event 0.1.1 → 0.1.3
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 +52 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +322 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -135,6 +135,58 @@ You can configure multiple channels, and all enabled channels will receive notif
|
|
|
135
135
|
}
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
## CLI Tool
|
|
139
|
+
|
|
140
|
+
This package includes a CLI tool to test your notification channel configuration before running OpenCode.
|
|
141
|
+
|
|
142
|
+
### Test Command
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Test all channels (validates config, tests connection, sends test email)
|
|
146
|
+
npx @ulthon/ul-opencode-event test
|
|
147
|
+
|
|
148
|
+
# Test a specific channel by name
|
|
149
|
+
npx @ulthon/ul-opencode-event test --channel "My Email"
|
|
150
|
+
|
|
151
|
+
# Only verify connection without sending email
|
|
152
|
+
npx @ulthon/ul-opencode-event test --no-send
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### What the test command does:
|
|
156
|
+
|
|
157
|
+
1. **Configuration validation**: Checks if the config file exists and has valid format
|
|
158
|
+
2. **SMTP connection test**: Verifies that the SMTP server is reachable and credentials work
|
|
159
|
+
3. **Test email**: Sends a test email to configured recipients (unless `--no-send` is used)
|
|
160
|
+
|
|
161
|
+
### Test output example:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
[ul-opencode-event-cli] Starting channel test...
|
|
165
|
+
[ul-opencode-event-cli] Loaded project config: ./ul-opencode-event.json
|
|
166
|
+
[ul-opencode-event-cli] Found 1 channel(s) to test
|
|
167
|
+
|
|
168
|
+
[ul-opencode-event-cli] Testing channel: "My Email"
|
|
169
|
+
Type: smtp
|
|
170
|
+
Recipients: user@example.com
|
|
171
|
+
|
|
172
|
+
[1/3] Validating configuration...
|
|
173
|
+
[OK] Configuration is valid
|
|
174
|
+
|
|
175
|
+
[2/3] Testing SMTP connection to smtp.example.com:465...
|
|
176
|
+
[OK] Connection successful
|
|
177
|
+
|
|
178
|
+
[3/3] Sending test email...
|
|
179
|
+
[OK] Test email sent successfully
|
|
180
|
+
Message ID: <xxx>
|
|
181
|
+
|
|
182
|
+
[SUCCESS] Channel "My Email" passed all tests
|
|
183
|
+
Please check the inbox of: user@example.com
|
|
184
|
+
|
|
185
|
+
[ul-opencode-event-cli] Test Summary:
|
|
186
|
+
Passed: 1
|
|
187
|
+
Failed: 0
|
|
188
|
+
```
|
|
189
|
+
|
|
138
190
|
## Troubleshooting
|
|
139
191
|
|
|
140
192
|
### No notifications sent
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI tool for testing notification channel configuration
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @ulthon/ul-opencode-event test
|
|
7
|
+
* npx @ulthon/ul-opencode-event test --channel "My Channel"
|
|
8
|
+
* npx @ulthon/ul-opencode-event test --no-send # Only verify connection, don't send email
|
|
9
|
+
*/
|
|
10
|
+
import * as nodemailer from 'nodemailer';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
const CLI_PREFIX = '[ul-opencode-event-cli]';
|
|
15
|
+
/**
|
|
16
|
+
* Print usage information
|
|
17
|
+
*/
|
|
18
|
+
function printUsage() {
|
|
19
|
+
console.log(`
|
|
20
|
+
Usage: npx @ulthon/ul-opencode-event test [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--channel <name> Test only the channel with this name
|
|
24
|
+
--no-send Only verify connection, don't send test email
|
|
25
|
+
--help Show this help message
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
npx @ulthon/ul-opencode-event test
|
|
29
|
+
npx @ulthon/ul-opencode-event test --channel "Primary Email"
|
|
30
|
+
npx @ulthon/ul-opencode-event test --no-send
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse command line arguments
|
|
35
|
+
*/
|
|
36
|
+
function parseArgs() {
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
const options = {
|
|
39
|
+
noSend: false,
|
|
40
|
+
};
|
|
41
|
+
let command = null;
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
const arg = args[i];
|
|
44
|
+
if (arg === '--help' || arg === '-h') {
|
|
45
|
+
printUsage();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
else if (arg === '--channel') {
|
|
49
|
+
options.channelName = args[++i];
|
|
50
|
+
}
|
|
51
|
+
else if (arg === '--no-send') {
|
|
52
|
+
options.noSend = true;
|
|
53
|
+
}
|
|
54
|
+
else if (!arg.startsWith('-')) {
|
|
55
|
+
command = arg;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { command, options };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate channel configuration
|
|
62
|
+
*/
|
|
63
|
+
function validateChannel(channel) {
|
|
64
|
+
const errors = [];
|
|
65
|
+
// Check type
|
|
66
|
+
if (channel.type !== 'smtp') {
|
|
67
|
+
errors.push(`Unsupported channel type: "${channel.type}". Only "smtp" is supported.`);
|
|
68
|
+
return { valid: false, errors };
|
|
69
|
+
}
|
|
70
|
+
// Check enabled
|
|
71
|
+
if (channel.enabled === false) {
|
|
72
|
+
errors.push('Channel is disabled (enabled: false)');
|
|
73
|
+
return { valid: false, errors };
|
|
74
|
+
}
|
|
75
|
+
// Check recipients
|
|
76
|
+
if (!channel.recipients || channel.recipients.length === 0) {
|
|
77
|
+
errors.push('No recipients configured');
|
|
78
|
+
return { valid: false, errors };
|
|
79
|
+
}
|
|
80
|
+
// Check SMTP config
|
|
81
|
+
const config = channel.config;
|
|
82
|
+
if (!config.host) {
|
|
83
|
+
errors.push('SMTP host is missing');
|
|
84
|
+
}
|
|
85
|
+
if (!config.port) {
|
|
86
|
+
errors.push('SMTP port is missing');
|
|
87
|
+
}
|
|
88
|
+
if (!config.auth?.user) {
|
|
89
|
+
errors.push('SMTP auth.user is missing');
|
|
90
|
+
}
|
|
91
|
+
if (!config.auth?.pass) {
|
|
92
|
+
errors.push('SMTP auth.pass is missing');
|
|
93
|
+
}
|
|
94
|
+
return { valid: errors.length === 0, errors };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create SMTP transporter
|
|
98
|
+
*/
|
|
99
|
+
function createTransporter(channel) {
|
|
100
|
+
const config = channel.config;
|
|
101
|
+
return nodemailer.createTransport({
|
|
102
|
+
host: config.host,
|
|
103
|
+
port: config.port,
|
|
104
|
+
secure: config.secure ?? (config.port === 465),
|
|
105
|
+
auth: {
|
|
106
|
+
user: config.auth.user,
|
|
107
|
+
pass: config.auth.pass,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Test SMTP connection
|
|
113
|
+
*/
|
|
114
|
+
async function testConnection(channel) {
|
|
115
|
+
try {
|
|
116
|
+
const transporter = createTransporter(channel);
|
|
117
|
+
await transporter.verify();
|
|
118
|
+
return { success: true };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
return { success: false, error: message };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Send test email
|
|
127
|
+
*/
|
|
128
|
+
async function sendTestEmail(channel) {
|
|
129
|
+
try {
|
|
130
|
+
const config = channel.config;
|
|
131
|
+
const transporter = createTransporter(channel);
|
|
132
|
+
const fromName = channel.name || 'OpenCode Notification Test';
|
|
133
|
+
const timestamp = new Date().toISOString();
|
|
134
|
+
const result = await transporter.sendMail({
|
|
135
|
+
from: `"${fromName}" <${config.auth.user}>`,
|
|
136
|
+
to: channel.recipients.join(', '),
|
|
137
|
+
subject: `[Test] OpenCode Notification Channel Test - ${timestamp}`,
|
|
138
|
+
text: `This is a test email from OpenCode notification plugin.
|
|
139
|
+
|
|
140
|
+
Channel: ${channel.name || 'unnamed'}
|
|
141
|
+
Type: ${channel.type}
|
|
142
|
+
Time: ${timestamp}
|
|
143
|
+
|
|
144
|
+
If you received this email, your notification channel is configured correctly.`,
|
|
145
|
+
html: `
|
|
146
|
+
<h2>OpenCode Notification Channel Test</h2>
|
|
147
|
+
<p>This is a test email from OpenCode notification plugin.</p>
|
|
148
|
+
<hr>
|
|
149
|
+
<ul>
|
|
150
|
+
<li><strong>Channel:</strong> ${channel.name || 'unnamed'}</li>
|
|
151
|
+
<li><strong>Type:</strong> ${channel.type}</li>
|
|
152
|
+
<li><strong>Time:</strong> ${timestamp}</li>
|
|
153
|
+
</ul>
|
|
154
|
+
<hr>
|
|
155
|
+
<p style="color: green;">If you received this email, your notification channel is configured correctly.</p>
|
|
156
|
+
`,
|
|
157
|
+
});
|
|
158
|
+
return { success: true, messageId: result.messageId };
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
return { success: false, error: message };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Test a single channel
|
|
167
|
+
*/
|
|
168
|
+
async function testChannel(channel, options) {
|
|
169
|
+
const channelName = channel.name || 'unnamed';
|
|
170
|
+
console.log(`\n${CLI_PREFIX} Testing channel: "${channelName}"`);
|
|
171
|
+
console.log(` Type: ${channel.type}`);
|
|
172
|
+
console.log(` Recipients: ${channel.recipients?.join(', ') || 'none'}`);
|
|
173
|
+
// Step 1: Validate configuration
|
|
174
|
+
console.log(`\n [1/3] Validating configuration...`);
|
|
175
|
+
const validation = validateChannel(channel);
|
|
176
|
+
if (!validation.valid) {
|
|
177
|
+
console.log(` [FAIL] Configuration validation failed:`);
|
|
178
|
+
validation.errors.forEach(err => console.log(` - ${err}`));
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
console.log(` [OK] Configuration is valid`);
|
|
182
|
+
// Step 2: Test connection
|
|
183
|
+
const smtpConfig = channel.config;
|
|
184
|
+
console.log(`\n [2/3] Testing SMTP connection to ${smtpConfig.host}:${smtpConfig.port}...`);
|
|
185
|
+
const connectionResult = await testConnection(channel);
|
|
186
|
+
if (!connectionResult.success) {
|
|
187
|
+
console.log(` [FAIL] Connection failed: ${connectionResult.error}`);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
console.log(` [OK] Connection successful`);
|
|
191
|
+
// Step 3: Send test email (if not skipped)
|
|
192
|
+
if (options.noSend) {
|
|
193
|
+
console.log(`\n [3/3] Skipping test email (--no-send flag)`);
|
|
194
|
+
console.log(`\n [SUCCESS] Channel "${channelName}" passed all tests (connection only)`);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
console.log(`\n [3/3] Sending test email...`);
|
|
198
|
+
const sendResult = await sendTestEmail(channel);
|
|
199
|
+
if (!sendResult.success) {
|
|
200
|
+
console.log(` [FAIL] Failed to send test email: ${sendResult.error}`);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
console.log(` [OK] Test email sent successfully`);
|
|
204
|
+
console.log(` Message ID: ${sendResult.messageId}`);
|
|
205
|
+
console.log(`\n [SUCCESS] Channel "${channelName}" passed all tests`);
|
|
206
|
+
console.log(` Please check the inbox of: ${channel.recipients?.join(', ')}`);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Load configuration (simplified version that loads from file)
|
|
211
|
+
* This is similar to config.ts but returns all channels including disabled ones
|
|
212
|
+
*/
|
|
213
|
+
function loadConfigForTesting() {
|
|
214
|
+
// Config file paths
|
|
215
|
+
const globalPath = path.join(os.homedir(), '.config', 'opencode', 'ul-opencode-event.json');
|
|
216
|
+
const projectOpencodePath = path.join(process.cwd(), '.opencode', 'ul-opencode-event.json');
|
|
217
|
+
const projectLocalPath = path.join(process.cwd(), 'ul-opencode-event.json');
|
|
218
|
+
let globalConfig = null;
|
|
219
|
+
let projectConfig = null;
|
|
220
|
+
// Load global config
|
|
221
|
+
if (fs.existsSync(globalPath)) {
|
|
222
|
+
try {
|
|
223
|
+
const content = fs.readFileSync(globalPath, 'utf-8');
|
|
224
|
+
globalConfig = JSON.parse(content);
|
|
225
|
+
console.log(`${CLI_PREFIX} Loaded global config: ${globalPath}`);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.error(`${CLI_PREFIX} Failed to parse global config: ${globalPath}`, error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Load project config
|
|
232
|
+
const projectPath = fs.existsSync(projectOpencodePath) ? projectOpencodePath :
|
|
233
|
+
fs.existsSync(projectLocalPath) ? projectLocalPath : null;
|
|
234
|
+
if (projectPath) {
|
|
235
|
+
try {
|
|
236
|
+
const content = fs.readFileSync(projectPath, 'utf-8');
|
|
237
|
+
projectConfig = JSON.parse(content);
|
|
238
|
+
console.log(`${CLI_PREFIX} Loaded project config: ${projectPath}`);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
console.error(`${CLI_PREFIX} Failed to parse project config: ${projectPath}`, error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// If no configs found
|
|
245
|
+
if (!globalConfig && !projectConfig) {
|
|
246
|
+
console.error(`${CLI_PREFIX} No configuration file found.`);
|
|
247
|
+
console.error(`${CLI_PREFIX} Checked locations:`);
|
|
248
|
+
console.error(` - ${globalPath}`);
|
|
249
|
+
console.error(` - ${projectOpencodePath}`);
|
|
250
|
+
console.error(` - ${projectLocalPath}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
// Merge channels (project overrides global by name)
|
|
254
|
+
const globalChannels = globalConfig?.channels || [];
|
|
255
|
+
const projectChannels = projectConfig?.channels || [];
|
|
256
|
+
const mergedChannels = [...globalChannels];
|
|
257
|
+
for (const projectChannel of projectChannels) {
|
|
258
|
+
if (projectChannel.name) {
|
|
259
|
+
const existingIndex = mergedChannels.findIndex(c => c.name === projectChannel.name);
|
|
260
|
+
if (existingIndex >= 0) {
|
|
261
|
+
mergedChannels[existingIndex] = projectChannel;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
mergedChannels.push(projectChannel);
|
|
266
|
+
}
|
|
267
|
+
return { channels: mergedChannels };
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Main CLI entry point
|
|
271
|
+
*/
|
|
272
|
+
async function main() {
|
|
273
|
+
const { command, options } = parseArgs();
|
|
274
|
+
if (command !== 'test') {
|
|
275
|
+
console.error(`${CLI_PREFIX} Unknown command: "${command || '(none)'}"`);
|
|
276
|
+
printUsage();
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
console.log(`${CLI_PREFIX} Starting channel test...`);
|
|
280
|
+
// Load configuration
|
|
281
|
+
const config = loadConfigForTesting();
|
|
282
|
+
if (config.channels.length === 0) {
|
|
283
|
+
console.error(`${CLI_PREFIX} No channels configured.`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
// Filter channels by name if specified
|
|
287
|
+
let channelsToTest = config.channels;
|
|
288
|
+
if (options.channelName) {
|
|
289
|
+
channelsToTest = config.channels.filter(c => c.name === options.channelName);
|
|
290
|
+
if (channelsToTest.length === 0) {
|
|
291
|
+
console.error(`${CLI_PREFIX} No channel found with name: "${options.channelName}"`);
|
|
292
|
+
console.error(`${CLI_PREFIX} Available channels:`);
|
|
293
|
+
config.channels.forEach(c => console.error(` - ${c.name || 'unnamed'}`));
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
console.log(`${CLI_PREFIX} Found ${channelsToTest.length} channel(s) to test`);
|
|
298
|
+
// Test each channel
|
|
299
|
+
let successCount = 0;
|
|
300
|
+
let failCount = 0;
|
|
301
|
+
for (const channel of channelsToTest) {
|
|
302
|
+
const success = await testChannel(channel, options);
|
|
303
|
+
if (success) {
|
|
304
|
+
successCount++;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
failCount++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Summary
|
|
311
|
+
console.log(`\n${CLI_PREFIX} Test Summary:`);
|
|
312
|
+
console.log(` Passed: ${successCount}`);
|
|
313
|
+
console.log(` Failed: ${failCount}`);
|
|
314
|
+
if (failCount > 0) {
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Run CLI
|
|
319
|
+
main().catch(error => {
|
|
320
|
+
console.error(`${CLI_PREFIX} Unexpected error:`, error);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ulthon/ul-opencode-event",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "OpenCode notification plugin - sends email when session events occur",
|
|
5
5
|
"author": "augushong",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"ul-opencode-event": "dist/cli.js"
|
|
12
|
+
},
|
|
10
13
|
"exports": {
|
|
11
14
|
".": {
|
|
12
15
|
"import": "./dist/index.js",
|
|
@@ -26,7 +29,8 @@
|
|
|
26
29
|
"plugin",
|
|
27
30
|
"notification",
|
|
28
31
|
"email",
|
|
29
|
-
"smtp"
|
|
32
|
+
"smtp",
|
|
33
|
+
"cli"
|
|
30
34
|
],
|
|
31
35
|
"files": [
|
|
32
36
|
"dist",
|