coderail-watch 0.1.2 → 0.1.4

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
@@ -8,6 +8,12 @@ Usage:
8
8
  npx coderail-watch --session-id <session_id> --project-key <project_public_id> --base-url http://localhost:8000
9
9
  ```
10
10
 
11
+ Watch a log file instead of stdin:
12
+
13
+ ```bash
14
+ npx coderail-watch --session-id <session_id> --project-key <project_public_id> --base-url http://localhost:8000 --log-path /path/to/logfile
15
+ ```
16
+
11
17
  Publish (npm):
12
18
 
13
19
  ```bash
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
+ const fs = require("fs");
4
5
  const readline = require("readline");
5
- const WebSocket = require("ws");
6
6
 
7
7
  const DEFAULT_BASE_URL = "http://localhost:8000";
8
8
  const DEFAULT_RETRY_WAIT_MS = 2000;
9
+ const ALERT_PREFIX = "[[CODERAIL_ERROR]] ";
10
+ const ERROR_PATTERN = /(^|[\s:])(?:error|exception|traceback|panic|fatal)(?=[\s:])/i;
11
+
12
+ const isErrorLine = (line) => ERROR_PATTERN.test(line);
9
13
 
10
14
  const parseArgs = (argv) => {
11
15
  const args = {};
@@ -28,6 +32,19 @@ const log = (message) => {
28
32
  process.stderr.write(`[coderail-watch] ${message}\n`);
29
33
  };
30
34
 
35
+ let WebSocketImpl = globalThis.WebSocket;
36
+ if (!WebSocketImpl) {
37
+ try {
38
+ WebSocketImpl = require("ws");
39
+ } catch (error) {
40
+ log(
41
+ "Missing dependency 'ws'. Run `cd tools/coderail-watch && npm install` or use the published package via `npx coderail-watch@latest ...`.",
42
+ );
43
+ process.exit(1);
44
+ }
45
+ }
46
+ const WebSocket = WebSocketImpl;
47
+
31
48
  const buildWsUrl = (baseUrl, sessionId, projectKey, token) => {
32
49
  let normalized = baseUrl.trim();
33
50
  if (!normalized.startsWith("http://") && !normalized.startsWith("https://")) {
@@ -52,6 +69,7 @@ const projectKey = args["project-key"];
52
69
  const baseUrl = args["base-url"] || DEFAULT_BASE_URL;
53
70
  const token = args["token"];
54
71
  const retryWaitMs = Number(args["retry-wait"] || DEFAULT_RETRY_WAIT_MS);
72
+ const logPath = args["log-path"];
55
73
 
56
74
  if (!sessionId || !projectKey) {
57
75
  log("Missing required args: --session-id and --project-key");
@@ -62,27 +80,84 @@ const wsUrl = buildWsUrl(baseUrl, sessionId, projectKey, token);
62
80
  let socket = null;
63
81
  let isOpen = false;
64
82
  const queue = [];
83
+ let retryCount = 0;
84
+ let lastErrorMessage = "";
85
+
86
+ const attachHandler = (target, event, handler) => {
87
+ if (typeof target.on === "function") {
88
+ target.on(event, handler);
89
+ return;
90
+ }
91
+ if (typeof target.addEventListener === "function") {
92
+ target.addEventListener(event, handler);
93
+ }
94
+ };
95
+
96
+ const isSocketOpen = () => {
97
+ if (!socket) return false;
98
+ if (typeof WebSocket.OPEN === "number") {
99
+ return socket.readyState === WebSocket.OPEN;
100
+ }
101
+ return isOpen;
102
+ };
65
103
 
66
104
  const connect = () => {
67
- socket = new WebSocket(wsUrl);
105
+ retryCount += 1;
106
+ log(`connecting (#${retryCount})...`);
107
+ try {
108
+ socket = new WebSocket(wsUrl);
109
+ } catch (error) {
110
+ lastErrorMessage = String(error);
111
+ log(`connect failed: ${lastErrorMessage}`);
112
+ setTimeout(connect, retryWaitMs);
113
+ return;
114
+ }
68
115
 
69
- socket.on("open", () => {
116
+ attachHandler(socket, "open", () => {
70
117
  isOpen = true;
118
+ retryCount = 0;
71
119
  log(`connected: ${wsUrl}`);
72
120
  while (queue.length) {
73
121
  const payload = queue.shift();
74
- socket.send(payload);
122
+ if (!isSocketOpen()) {
123
+ queue.unshift(payload);
124
+ break;
125
+ }
126
+ try {
127
+ socket.send(payload);
128
+ } catch (error) {
129
+ queue.unshift(payload);
130
+ break;
131
+ }
75
132
  }
76
133
  });
77
134
 
78
- socket.on("close", () => {
135
+ attachHandler(socket, "close", (event) => {
79
136
  isOpen = false;
80
- log("disconnected; retrying...");
137
+ const code =
138
+ event && typeof event === "object" && "code" in event ? event.code : null;
139
+ const reason =
140
+ event && typeof event === "object" && "reason" in event
141
+ ? event.reason
142
+ : "";
143
+ const detail = [code ? `code=${code}` : null, reason || null]
144
+ .filter(Boolean)
145
+ .join(" ");
146
+ log(
147
+ `disconnected; retrying in ${retryWaitMs}ms${
148
+ detail ? ` (${detail})` : ""
149
+ }`,
150
+ );
81
151
  setTimeout(connect, retryWaitMs);
82
152
  });
83
153
 
84
- socket.on("error", (error) => {
85
- log(`error: ${error.message}`);
154
+ attachHandler(socket, "error", (error) => {
155
+ const message =
156
+ error && typeof error === "object" && "message" in error
157
+ ? error.message
158
+ : String(error);
159
+ lastErrorMessage = message;
160
+ log(`error: ${message}`);
86
161
  });
87
162
  };
88
163
 
@@ -93,21 +168,133 @@ const rl = readline.createInterface({
93
168
  crlfDelay: Infinity,
94
169
  });
95
170
 
96
- rl.on("line", (line) => {
171
+ const enqueueOrSend = (payload) => {
172
+ if (isSocketOpen()) {
173
+ try {
174
+ socket.send(payload);
175
+ return;
176
+ } catch (error) {
177
+ queue.push(payload);
178
+ return;
179
+ }
180
+ }
181
+ queue.push(payload);
182
+ };
183
+
184
+ const handleLine = (line) => {
97
185
  const payload = JSON.stringify({
98
186
  type: "log",
99
187
  content: line,
100
188
  timestamp: new Date().toISOString(),
101
189
  });
102
- if (isOpen && socket) {
103
- socket.send(payload);
104
- } else {
105
- queue.push(payload);
190
+ enqueueOrSend(payload);
191
+ if (isErrorLine(line)) {
192
+ const alertPayload = JSON.stringify({
193
+ type: "alert",
194
+ content: `${ALERT_PREFIX}${line}`,
195
+ timestamp: new Date().toISOString(),
196
+ });
197
+ enqueueOrSend(alertPayload);
106
198
  }
107
- });
199
+ };
200
+
201
+ const startFileTailer = (targetPath) => {
202
+ let fileOffset = 0;
203
+ let remainder = "";
204
+
205
+ const initializeOffset = () => {
206
+ try {
207
+ const stats = fs.statSync(targetPath);
208
+ fileOffset = stats.size;
209
+ return true;
210
+ } catch (error) {
211
+ log(`log-path not found: ${targetPath} (retrying...)`);
212
+ return false;
213
+ }
214
+ };
215
+
216
+ const flushChunk = (chunk) => {
217
+ const text = `${remainder}${chunk}`;
218
+ const lines = text.split(/\r?\n/);
219
+ remainder = lines.pop() ?? "";
220
+ lines.forEach((line) => {
221
+ if (line.length > 0) {
222
+ handleLine(line);
223
+ }
224
+ });
225
+ };
108
226
 
109
- rl.on("close", () => {
110
- if (socket) {
111
- socket.close();
227
+ const readNewData = () => {
228
+ fs.stat(targetPath, (error, stats) => {
229
+ if (error) {
230
+ log(`log-path read error: ${error.message || String(error)}`);
231
+ return;
232
+ }
233
+ if (stats.size < fileOffset) {
234
+ fileOffset = 0;
235
+ }
236
+ if (stats.size === fileOffset) {
237
+ return;
238
+ }
239
+ const stream = fs.createReadStream(targetPath, {
240
+ start: fileOffset,
241
+ end: stats.size - 1,
242
+ encoding: "utf8",
243
+ });
244
+ stream.on("data", flushChunk);
245
+ stream.on("end", () => {
246
+ fileOffset = stats.size;
247
+ });
248
+ stream.on("error", (streamError) => {
249
+ log(
250
+ `log-path stream error: ${
251
+ streamError.message || String(streamError)
252
+ }`,
253
+ );
254
+ });
255
+ });
256
+ };
257
+
258
+ const startWatcher = () => {
259
+ try {
260
+ fs.watch(targetPath, { persistent: true }, (eventType) => {
261
+ if (eventType === "change") {
262
+ readNewData();
263
+ }
264
+ });
265
+ } catch (error) {
266
+ log(`log-path watch error: ${error.message || String(error)}`);
267
+ }
268
+ };
269
+
270
+ if (!initializeOffset()) {
271
+ const retryTimer = setInterval(() => {
272
+ if (initializeOffset()) {
273
+ clearInterval(retryTimer);
274
+ startWatcher();
275
+ }
276
+ }, retryWaitMs);
277
+ return;
112
278
  }
113
- });
279
+
280
+ startWatcher();
281
+ };
282
+
283
+ if (logPath) {
284
+ startFileTailer(logPath);
285
+ } else {
286
+ const rl = readline.createInterface({
287
+ input: process.stdin,
288
+ crlfDelay: Infinity,
289
+ });
290
+
291
+ rl.on("line", (line) => {
292
+ handleLine(line);
293
+ });
294
+
295
+ rl.on("close", () => {
296
+ if (socket) {
297
+ socket.close();
298
+ }
299
+ });
300
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderail-watch",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Stream terminal output to CodeRail backend over WebSocket.",
5
5
  "bin": {
6
6
  "coderail-watch": "bin/coderail-watch.js"