@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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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.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",