coderail-watch 0.1.8 → 0.1.11

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.
Files changed (2) hide show
  1. package/bin/coderail-watch.js +674 -78
  2. package/package.json +1 -1
@@ -12,6 +12,8 @@ const ENV_BASE_URLS = {
12
12
  production: "https://api.coderail.local",
13
13
  };
14
14
  const DEFAULT_RETRY_WAIT_MS = 2000;
15
+ const DEFAULT_LOG_BATCH_MS = 300;
16
+ const DEFAULT_LOG_BATCH_MAX_LINES = 50;
15
17
  const ALERT_PREFIX = "[[CODERAIL_ERROR]] ";
16
18
  const ERROR_PATTERN = /(^|[\s:])(?:error|exception|traceback|panic|fatal)(?=[\s:])/i;
17
19
  const DEFAULT_PROJECT_TREE_MAX_FILES = 1000;
@@ -55,7 +57,21 @@ const parseArgs = (argv) => {
55
57
  return args;
56
58
  };
57
59
 
58
- const log = (message) => {
60
+ const LOG_LEVELS = {
61
+ error: 0,
62
+ warn: 1,
63
+ info: 2,
64
+ debug: 3,
65
+ };
66
+ let currentLogLevel = LOG_LEVELS.warn;
67
+
68
+ const shouldLog = (level) => {
69
+ const resolved = LOG_LEVELS[level] ?? LOG_LEVELS.info;
70
+ return currentLogLevel >= resolved;
71
+ };
72
+
73
+ const log = (message, level = "info") => {
74
+ if (!shouldLog(level)) return;
59
75
  process.stderr.write(`[coderail-watch] ${message}\n`);
60
76
  };
61
77
 
@@ -66,6 +82,7 @@ if (!WebSocketImpl) {
66
82
  } catch (error) {
67
83
  log(
68
84
  "Missing dependency 'ws'. Run `cd tools/coderail-watch && npm install` or use the published package via `npx coderail-watch@latest ...`.",
85
+ "error",
69
86
  );
70
87
  process.exit(1);
71
88
  }
@@ -85,7 +102,7 @@ const normalizeBaseHttpUrl = (baseUrl) => {
85
102
  return normalized.replace(/\/$/, "");
86
103
  };
87
104
 
88
- const buildWsUrl = (baseUrl, sessionId, projectKey, token) => {
105
+ const buildWsUrl = (baseUrl, sessionId, projectKey, token, flowId) => {
89
106
  const url = new URL(normalizeHttpBaseUrl(baseUrl));
90
107
  const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
91
108
  url.protocol = wsProtocol;
@@ -96,6 +113,9 @@ const buildWsUrl = (baseUrl, sessionId, projectKey, token) => {
96
113
  if (token) {
97
114
  url.searchParams.set("token", token);
98
115
  }
116
+ if (flowId) {
117
+ url.searchParams.set("flow_id", flowId);
118
+ }
99
119
  return url.toString();
100
120
  };
101
121
 
@@ -148,7 +168,7 @@ const collectSnapshotPaths = (rootDir, excludes) => {
148
168
  try {
149
169
  entries = fs.readdirSync(current, { withFileTypes: true });
150
170
  } catch (error) {
151
- log(`snapshot read failed: ${current} (${error})`);
171
+ log(`snapshot read failed: ${current} (${error})`, "warn");
152
172
  continue;
153
173
  }
154
174
  for (const entry of entries) {
@@ -255,7 +275,7 @@ let baseUrl = "";
255
275
  try {
256
276
  baseUrl = resolveBaseUrl(args);
257
277
  } catch (error) {
258
- log(error.message || String(error));
278
+ log(error.message || String(error), "error");
259
279
  process.exit(1);
260
280
  }
261
281
  const token = args["token"];
@@ -264,6 +284,16 @@ const retryWaitMs = normalizeIntArg(
264
284
  DEFAULT_RETRY_WAIT_MS,
265
285
  0,
266
286
  );
287
+ const logBatchMs = normalizeIntArg(
288
+ args["log-batch-ms"],
289
+ DEFAULT_LOG_BATCH_MS,
290
+ 0,
291
+ );
292
+ const logBatchMaxLines = normalizeIntArg(
293
+ args["log-batch-lines"],
294
+ DEFAULT_LOG_BATCH_MAX_LINES,
295
+ 1,
296
+ );
267
297
  const logPath = args["log-path"];
268
298
  const projectRoot = path.resolve(args["project-root"] || process.cwd());
269
299
  const sendProjectTree = normalizeBoolArg(args["project-tree"], true);
@@ -280,7 +310,8 @@ const projectTreeMaxBytes = normalizeIntArg(
280
310
  DEFAULT_PROJECT_TREE_MAX_BYTES,
281
311
  1,
282
312
  );
283
- const enableApiCheck = normalizeBoolArg(args["api-llm-check"], true);
313
+ const runMode = args["mode"] || "default";
314
+ const enableApiCheck = true;
284
315
  const enableScreenCheck = normalizeBoolArg(args["screen-check"], true);
285
316
  const dotenvPath = args["env-file"] || path.join(projectRoot, ".env");
286
317
  const loadEnvFile = (filePath) => {
@@ -318,20 +349,57 @@ const openaiModel =
318
349
  process.env.OPENAI_MODEL ||
319
350
  envFromFile.OPENAI_MODEL ||
320
351
  DEFAULT_OPENAI_MODEL;
321
- const apiEndpointUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/projects/${encodeURIComponent(
322
- projectKey,
323
- )}/api_endpoint_list`;
352
+ const isNumericProjectKey = /^[0-9]+$/.test(projectKey);
353
+ const apiEndpointUrl = isNumericProjectKey
354
+ ? `${baseUrl.replace(/\/$/, "")}/api/v1/projects/${encodeURIComponent(
355
+ projectKey,
356
+ )}/api_endpoint_list`
357
+ : `${baseUrl.replace(/\/$/, "")}/api/v1/projects/by_public/${encodeURIComponent(
358
+ projectKey,
359
+ )}/api_endpoint_list`;
360
+ const screenListUrl = isNumericProjectKey
361
+ ? `${baseUrl.replace(/\/$/, "")}/api/v1/projects/${encodeURIComponent(
362
+ projectKey,
363
+ )}/screen_list`
364
+ : `${baseUrl.replace(/\/$/, "")}/api/v1/projects/by_public/${encodeURIComponent(
365
+ projectKey,
366
+ )}/screen_list`;
324
367
  let apiEndpointCandidatesCached = [];
368
+ let screenCandidatesCached = [];
369
+ const toScreenName = (item) =>
370
+ typeof item === "object" && item !== null ? item.name || JSON.stringify(item) : String(item);
371
+ let apiEndpointFetchStatus = "unknown";
372
+ let apiEndpointFetchDetail = "";
373
+ let screenListFetchStatus = "unknown";
374
+ let screenListFetchDetail = "";
325
375
 
326
376
  if (!sessionId || !projectKey) {
327
- log("Missing required args: --session-id and --project-key");
377
+ log("Missing required args: --session-id and --project-key", "error");
328
378
  process.exit(1);
329
379
  }
330
380
 
331
- const wsUrl = buildWsUrl(baseUrl, sessionId, projectKey, token);
381
+ const requestedLogLevel = String(
382
+ args["log-level"] || args["loglevel"] || "",
383
+ ).toLowerCase();
384
+ if (requestedLogLevel in LOG_LEVELS) {
385
+ currentLogLevel = LOG_LEVELS[requestedLogLevel];
386
+ }
387
+ if (args["quiet"] === "true") {
388
+ currentLogLevel = -1;
389
+ }
390
+ // --mode security はデフォルトで info レベル以上を表示
391
+ if (runMode === "security" && !requestedLogLevel && args["quiet"] !== "true") {
392
+ currentLogLevel = LOG_LEVELS.info;
393
+ }
394
+
395
+ const MODE_FLOW_IDS = { security: "security_check" };
396
+ const resolvedFlowId = args["flow-id"] || MODE_FLOW_IDS[runMode] || null;
397
+ const wsUrl = buildWsUrl(baseUrl, sessionId, projectKey, token, resolvedFlowId);
332
398
  let socket = null;
333
399
  let isOpen = false;
334
400
  const queue = [];
401
+ const logBuffer = [];
402
+ let logFlushTimer = null;
335
403
  let retryCount = 0;
336
404
  let lastErrorMessage = "";
337
405
  let apiCheckInFlight = false;
@@ -354,22 +422,67 @@ const isSocketOpen = () => {
354
422
  return isOpen;
355
423
  };
356
424
 
425
+ const CONNECT_TIMEOUT_MS = 10000;
426
+
357
427
  const connect = () => {
358
428
  retryCount += 1;
359
- log(`connecting (#${retryCount})...`);
429
+ const attempt = retryCount;
430
+ log(`connecting to ${wsUrl} (#${attempt})...`, "warn");
360
431
  try {
361
- socket = new WebSocket(wsUrl);
432
+ const wsOptions = wsUrl.startsWith("wss://")
433
+ ? { rejectUnauthorized: false }
434
+ : undefined;
435
+ socket = new WebSocket(wsUrl, wsOptions);
362
436
  } catch (error) {
363
437
  lastErrorMessage = String(error);
364
- log(`connect failed: ${lastErrorMessage}`);
438
+ log(`connect failed: ${lastErrorMessage}`, "error");
365
439
  setTimeout(connect, retryWaitMs);
366
440
  return;
367
441
  }
368
442
 
443
+ // 接続タイムアウト検知
444
+ const connectTimer = setTimeout(() => {
445
+ if (socket && !isOpen) {
446
+ log(`connection timeout (${CONNECT_TIMEOUT_MS}ms). 接続先を確認してください: ${wsUrl}`, "error");
447
+ try { socket.close(); } catch (_e) { /* ignore */ }
448
+ }
449
+ }, CONNECT_TIMEOUT_MS);
450
+
369
451
  attachHandler(socket, "open", () => {
452
+ clearTimeout(connectTimer);
370
453
  isOpen = true;
371
454
  retryCount = 0;
372
- log(`connected: ${wsUrl}`);
455
+ log(`connected: ${wsUrl}`, "warn");
456
+
457
+ // --mode security: run security check only, then exit
458
+ if (runMode === "security") {
459
+ void (async () => {
460
+ try {
461
+ const result = await triggerSecurityCheck("mode:security");
462
+ flushLogBuffer();
463
+ // Send done summary as code-analysis done for backend pipeline
464
+ sendCodeAnalysisLine(`done ${JSON.stringify({
465
+ api_missing_count: 0, api_missing_items: [],
466
+ api_implemented_count: 0, api_implemented_items: [],
467
+ screen_missing_count: 0, screen_missing_items: [],
468
+ screen_implemented_count: 0, screen_implemented_items: [],
469
+ ...result,
470
+ })}`);
471
+ flushLogBuffer();
472
+ // Give time for send to complete
473
+ setTimeout(() => {
474
+ log("security mode complete, exiting.", "warn");
475
+ if (socket) socket.close();
476
+ process.exit(0);
477
+ }, 500);
478
+ } catch (error) {
479
+ log(`security mode error: ${error}`, "error");
480
+ process.exit(1);
481
+ }
482
+ })();
483
+ return;
484
+ }
485
+
373
486
  if (sendProjectTree) {
374
487
  emitProjectTree();
375
488
  }
@@ -384,6 +497,14 @@ const connect = () => {
384
497
  }
385
498
  })();
386
499
  }
500
+ if (enableScreenCheck) {
501
+ void (async () => {
502
+ if (!screenCandidatesCached.length) {
503
+ await fetchScreenList();
504
+ }
505
+ logScreenCheckStatus();
506
+ })();
507
+ }
387
508
  while (queue.length) {
388
509
  const payload = queue.shift();
389
510
  if (!isSocketOpen()) {
@@ -418,10 +539,18 @@ const connect = () => {
418
539
  }
419
540
  if (!payload || typeof payload !== "object") return;
420
541
  if (payload.type !== "control") return;
542
+ log(`[control] received action=${payload.action}`, "info");
421
543
  if (payload.action === "api_check") {
544
+ log("[control] triggering api_check", "info");
422
545
  void triggerApiCheck("control");
423
546
  return;
424
547
  }
548
+ if (payload.action === "code_analysis") {
549
+ const role = payload.role || null;
550
+ log(`[control] triggering code_analysis role=${role}`, "info");
551
+ void triggerCodeAnalysis("control", role);
552
+ return;
553
+ }
425
554
  if (payload.action === "screen_check") {
426
555
  if (enableScreenCheck) {
427
556
  void triggerScreenCheck("control");
@@ -429,9 +558,14 @@ const connect = () => {
429
558
  sendScreenCheckLine("screen check disabled; skipping.");
430
559
  }
431
560
  }
561
+ if (payload.action === "security_check") {
562
+ log("[control] triggering security_check", "info");
563
+ void triggerSecurityCheck("control");
564
+ }
432
565
  });
433
566
 
434
567
  attachHandler(socket, "close", (event) => {
568
+ clearTimeout(connectTimer);
435
569
  isOpen = false;
436
570
  const code =
437
571
  event && typeof event === "object" && "code" in event ? event.code : null;
@@ -439,13 +573,23 @@ const connect = () => {
439
573
  event && typeof event === "object" && "reason" in event
440
574
  ? event.reason
441
575
  : "";
442
- const detail = [code ? `code=${code}` : null, reason || null]
576
+ const reasonStr = reason ? String(reason) : "";
577
+ const detail = [code ? `code=${code}` : null, reasonStr || null]
443
578
  .filter(Boolean)
444
579
  .join(" ");
580
+ if (code === 1008) {
581
+ const hint = reasonStr === "project_key_mismatch"
582
+ ? "同じ session_id で別の project_key が既に登録されています。バックエンドサーバーを再起動してください。"
583
+ : reasonStr
584
+ ? `サーバーが接続を拒否しました: ${reasonStr}`
585
+ : "サーバーが接続を拒否しました (code=1008)。session_id / project_key を確認してください。";
586
+ log(`error: ${hint}`, "error");
587
+ }
445
588
  log(
446
589
  `disconnected; retrying in ${retryWaitMs}ms${
447
590
  detail ? ` (${detail})` : ""
448
591
  }`,
592
+ "warn",
449
593
  );
450
594
  setTimeout(connect, retryWaitMs);
451
595
  });
@@ -456,7 +600,7 @@ const connect = () => {
456
600
  ? error.message
457
601
  : String(error);
458
602
  lastErrorMessage = message;
459
- log(`error: ${message}`);
603
+ log(`error: ${message}`, "warn");
460
604
  });
461
605
  };
462
606
 
@@ -480,45 +624,96 @@ const enqueueOrSend = (payload) => {
480
624
  queue.push(payload);
481
625
  };
482
626
 
483
- const sendLogLine = (line) => {
627
+ const flushLogBuffer = () => {
628
+ if (logFlushTimer) {
629
+ clearTimeout(logFlushTimer);
630
+ logFlushTimer = null;
631
+ }
632
+ if (!logBuffer.length) return;
633
+ const lines = logBuffer.splice(0, logBuffer.length);
484
634
  const payload = JSON.stringify({
485
635
  type: "log",
486
- content: line,
636
+ content: lines.join("\n"),
487
637
  timestamp: new Date().toISOString(),
488
638
  });
489
639
  enqueueOrSend(payload);
490
640
  };
491
641
 
642
+ const enqueueLogLine = (line) => {
643
+ if (!line) return;
644
+ logBuffer.push(line);
645
+ if (logBatchMs === 0 || logBuffer.length >= logBatchMaxLines) {
646
+ flushLogBuffer();
647
+ return;
648
+ }
649
+ if (!logFlushTimer) {
650
+ logFlushTimer = setTimeout(flushLogBuffer, logBatchMs);
651
+ }
652
+ };
653
+
654
+ const sendLogLine = (line) => {
655
+ enqueueLogLine(line);
656
+ };
657
+
492
658
  const sendApiCheckLine = (line) => {
493
659
  const tagged = `[api-check] ${line}`;
494
- log(tagged);
660
+ log(tagged, "debug");
661
+ sendLogLine(tagged);
662
+ };
663
+
664
+ const sendCodeAnalysisLine = (line) => {
665
+ const tagged = `[code-analysis] ${line}`;
666
+ log(tagged, "info");
495
667
  sendLogLine(tagged);
496
668
  };
497
669
 
498
670
  const sendScreenCheckLine = (line) => {
499
671
  const tagged = `[screen-check] ${line}`;
500
- log(tagged);
672
+ log(tagged, "debug");
673
+ sendLogLine(tagged);
674
+ };
675
+
676
+ const sendSecurityCheckLine = (line) => {
677
+ const tagged = `[security-check] ${line}`;
678
+ log(tagged, "info");
501
679
  sendLogLine(tagged);
502
680
  };
503
681
 
504
682
  const logApiCheckStatus = () => {
505
683
  const maskedKey = openaiApiKey ? `${openaiApiKey.slice(0, 6)}...` : "";
684
+ const statusLabel =
685
+ apiEndpointFetchStatus === "error"
686
+ ? `NG(fetch_failed${apiEndpointFetchDetail ? `: ${apiEndpointFetchDetail}` : ""})`
687
+ : apiEndpointFetchStatus === "ok_empty"
688
+ ? "NG(empty)"
689
+ : apiEndpointCandidatesCached.length
690
+ ? "OK"
691
+ : "NG(unknown)";
506
692
  sendApiCheckLine(
507
- `必須チェック: api_endpoint_list=${
508
- apiEndpointCandidatesCached.length ? "OK" : "NG"
509
- } openai_api_key=${openaiApiKey ? "OK" : "NG"} ${
693
+ `必須チェック: api_endpoint_list=${statusLabel} openai_api_key=${
694
+ openaiApiKey ? "OK" : "NG"
695
+ } ${
510
696
  maskedKey ? `(${maskedKey})` : ""
511
697
  } openai_model=${openaiModel ? "OK" : "NG"}`,
512
698
  );
513
699
  };
514
700
 
701
+ const logScreenCheckStatus = () => {
702
+ const statusLabel =
703
+ screenListFetchStatus === "error"
704
+ ? `NG(fetch_failed${screenListFetchDetail ? `: ${screenListFetchDetail}` : ""})`
705
+ : screenListFetchStatus === "ok_empty"
706
+ ? "NG(empty)"
707
+ : screenCandidatesCached.length
708
+ ? "OK"
709
+ : "NG(unknown)";
710
+ sendScreenCheckLine(
711
+ `必須チェック: screen_list=${statusLabel}`,
712
+ );
713
+ };
714
+
515
715
  const handleLine = (line) => {
516
- const payload = JSON.stringify({
517
- type: "log",
518
- content: line,
519
- timestamp: new Date().toISOString(),
520
- });
521
- enqueueOrSend(payload);
716
+ sendLogLine(line);
522
717
  if (isErrorLine(line)) {
523
718
  const alertPayload = JSON.stringify({
524
719
  type: "alert",
@@ -538,11 +733,21 @@ const emitProjectTree = () => {
538
733
  projectTreeMaxBytes,
539
734
  );
540
735
  lines.forEach((line) => {
541
- log(line);
736
+ log(line, "debug");
542
737
  sendLogLine(line);
543
738
  });
544
739
  };
545
740
 
741
+ process.on("SIGINT", () => {
742
+ flushLogBuffer();
743
+ process.exit(0);
744
+ });
745
+
746
+ process.on("SIGTERM", () => {
747
+ flushLogBuffer();
748
+ process.exit(0);
749
+ });
750
+
546
751
  const buildApiCheckPrompt = (endpoints, treeLines) => {
547
752
  return [
548
753
  "You are a senior backend engineer. Determine which API endpoints are NOT implemented in the codebase.",
@@ -569,19 +774,25 @@ const collectTreeForApiCheck = () => {
569
774
  };
570
775
 
571
776
  const runApiCheck = async () => {
572
- if (!enableApiCheck) return;
573
- if (!apiEndpointCandidatesCached.length) return;
777
+ const allMissingResult = () => {
778
+ const items = apiEndpointCandidatesCached.map(String);
779
+ return { api_missing_count: items.length, api_missing_items: items, api_implemented_count: 0, api_implemented_items: [] };
780
+ };
781
+ if (!enableApiCheck) return allMissingResult();
782
+ if (!apiEndpointCandidatesCached.length) {
783
+ return { api_missing_count: 0, api_missing_items: [], api_implemented_count: 0, api_implemented_items: [] };
784
+ }
574
785
  if (!openaiApiKey) {
575
- sendApiCheckLine("OPENAI_API_KEY is not set; skipping API check.");
576
- return;
786
+ sendApiCheckLine("OPENAI_API_KEY is not set; すべて未実装として扱います。");
787
+ return allMissingResult();
577
788
  }
578
789
  if (!openaiModel) {
579
- sendApiCheckLine("OPENAI_MODEL is not set; skipping API check.");
580
- return;
790
+ sendApiCheckLine("OPENAI_MODEL is not set; すべて未実装として扱います。");
791
+ return allMissingResult();
581
792
  }
582
793
  if (typeof fetch !== "function") {
583
- sendApiCheckLine("fetch is not available in this runtime; skipping API check.");
584
- return;
794
+ sendApiCheckLine("fetch is not available in this runtime; すべて未実装として扱います。");
795
+ return allMissingResult();
585
796
  }
586
797
  const treeLines = collectTreeForApiCheck();
587
798
  const prompt = buildApiCheckPrompt(apiEndpointCandidatesCached, treeLines);
@@ -609,7 +820,7 @@ const runApiCheck = async () => {
609
820
  });
610
821
  } catch (error) {
611
822
  sendApiCheckLine(`OpenAI request failed: ${error}`);
612
- return;
823
+ return allMissingResult();
613
824
  }
614
825
  if (!response.ok) {
615
826
  const text = await response.text();
@@ -617,19 +828,19 @@ const runApiCheck = async () => {
617
828
  `OpenAI request failed: ${response.status} ${response.statusText}`,
618
829
  );
619
830
  sendApiCheckLine(text.slice(0, 1000));
620
- return;
831
+ return allMissingResult();
621
832
  }
622
833
  let data = null;
623
834
  try {
624
835
  data = await response.json();
625
836
  } catch (error) {
626
837
  sendApiCheckLine(`OpenAI response parse failed: ${error}`);
627
- return;
838
+ return allMissingResult();
628
839
  }
629
840
  const content = data?.choices?.[0]?.message?.content?.trim();
630
841
  if (!content) {
631
842
  sendApiCheckLine("OpenAI response was empty.");
632
- return;
843
+ return allMissingResult();
633
844
  }
634
845
  try {
635
846
  const parsed = JSON.parse(content);
@@ -641,38 +852,68 @@ const runApiCheck = async () => {
641
852
  sendApiCheckLine(
642
853
  `missing=${missing.length} unknown=${unknown.length} implemented=${implemented.length}`,
643
854
  );
855
+ const missingItems = [];
644
856
  missing.forEach((item) => {
645
857
  if (item && typeof item === "object") {
858
+ const label = item.endpoint || "";
859
+ missingItems.push(label);
646
860
  sendApiCheckLine(
647
- `missing: ${item.endpoint || ""} (${item.reason || "no reason"})`,
861
+ `missing: ${label} (${item.reason || "no reason"})`,
648
862
  );
649
863
  } else {
864
+ missingItems.push(String(item));
650
865
  sendApiCheckLine(`missing: ${String(item)}`);
651
866
  }
652
867
  });
868
+ const implementedItems = [];
653
869
  unknown.forEach((item) => sendApiCheckLine(`unknown: ${String(item)}`));
654
- implemented.forEach((item) =>
655
- sendApiCheckLine(`implemented: ${String(item)}`),
656
- );
870
+ implemented.forEach((item) => {
871
+ implementedItems.push(String(item));
872
+ sendApiCheckLine(`implemented: ${String(item)}`);
873
+ });
874
+ return {
875
+ api_missing_count: missing.length,
876
+ api_missing_items: missingItems,
877
+ api_implemented_count: implemented.length,
878
+ api_implemented_items: implementedItems,
879
+ };
657
880
  } catch (error) {
658
881
  sendApiCheckLine("OpenAI output was not valid JSON. Raw output:");
659
882
  sendApiCheckLine(content.slice(0, 2000));
883
+ return allMissingResult();
660
884
  }
661
885
  };
662
886
 
663
887
  const screenCheckUrl = `${normalizeBaseHttpUrl(baseUrl)}/api/v1/project_snapshot/check`;
664
888
 
665
889
  const runScreenCheck = async () => {
890
+ const allMissingResult = () => {
891
+ const items = screenCandidatesCached.map(toScreenName);
892
+ return { screen_missing_count: items.length, screen_missing_items: items, screen_implemented_count: 0, screen_implemented_items: [] };
893
+ };
666
894
  if (!projectKey) {
667
- sendScreenCheckLine("project_key is missing; skipping screen check.");
668
- return;
895
+ sendScreenCheckLine("project_key is missing; すべて未実装として扱います。");
896
+ return allMissingResult();
669
897
  }
670
898
  if (typeof fetch !== "function") {
671
- sendScreenCheckLine("fetch is not available in this runtime; skipping screen check.");
672
- return;
899
+ sendScreenCheckLine("fetch is not available in this runtime; すべて未実装として扱います。");
900
+ return allMissingResult();
673
901
  }
674
- const paths = collectSnapshotPaths(projectRoot, DEFAULT_SNAPSHOT_EXCLUDES);
902
+ const paths = collectSnapshotPaths(projectRoot, DEFAULT_SNAPSHOT_EXCLUDES).filter(
903
+ (item) => item.startsWith("frontend/"),
904
+ );
675
905
  const limitedPaths = paths.slice(0, projectTreeMaxFiles);
906
+ if (!limitedPaths.length) {
907
+ sendScreenCheckLine("frontend/ 配下のファイルが見つからないため、すべて未実装として扱います。");
908
+ const allMissing = screenCandidatesCached.map(toScreenName);
909
+ allMissing.forEach((item) => sendScreenCheckLine(`missing: ${item}`));
910
+ return {
911
+ screen_missing_count: allMissing.length,
912
+ screen_missing_items: allMissing,
913
+ screen_implemented_count: 0,
914
+ screen_implemented_items: [],
915
+ };
916
+ }
676
917
  const payload = {
677
918
  projectPublicId: projectKey,
678
919
  paths: limitedPaths,
@@ -686,7 +927,7 @@ const runScreenCheck = async () => {
686
927
  });
687
928
  } catch (error) {
688
929
  sendScreenCheckLine(`screen check request failed: ${error}`);
689
- return;
930
+ return allMissingResult();
690
931
  }
691
932
  if (!response.ok) {
692
933
  const text = await response.text();
@@ -694,19 +935,24 @@ const runScreenCheck = async () => {
694
935
  `screen check failed: ${response.status} ${response.statusText}`,
695
936
  );
696
937
  sendScreenCheckLine(text.slice(0, 1000));
697
- return;
938
+ return allMissingResult();
698
939
  }
699
940
  let data = null;
700
941
  try {
701
942
  data = await response.json();
702
943
  } catch (error) {
703
944
  sendScreenCheckLine(`screen check response parse failed: ${error}`);
704
- return;
945
+ return allMissingResult();
705
946
  }
706
- const expected = Array.isArray(data?.expectedScreens) ? data.expectedScreens : [];
707
- const missing = Array.isArray(data?.check?.missingScreens)
947
+ const expected = screenCandidatesCached.length
948
+ ? screenCandidatesCached.map(toScreenName)
949
+ : (Array.isArray(data?.expectedScreens) ? data.expectedScreens : []);
950
+ const rawMissing = Array.isArray(data?.check?.missingScreens)
708
951
  ? data.check.missingScreens
709
952
  : [];
953
+ // expected をキャッシュに揃えたので、missing も expected に含まれるもののみに絞る
954
+ const expectedSet = new Set(expected.map(String));
955
+ const missing = rawMissing.filter((item) => expectedSet.has(String(item)));
710
956
  const implemented = expected.filter((item) => !missing.includes(item));
711
957
  sendScreenCheckLine(
712
958
  `missing_screens=${missing.length} expected=${expected.length} implemented=${implemented.length}`,
@@ -718,10 +964,17 @@ const runScreenCheck = async () => {
718
964
  if (data?.check?.notes) {
719
965
  sendScreenCheckLine(`note: ${String(data.check.notes)}`);
720
966
  }
967
+ return {
968
+ screen_missing_count: missing.length,
969
+ screen_missing_items: missing.map(String),
970
+ screen_implemented_count: implemented.length,
971
+ screen_implemented_items: implemented.map(String),
972
+ };
721
973
  };
722
974
 
723
975
  const triggerApiCheck = async (reason) => {
724
- if (apiCheckInFlight) return;
976
+ const emptyResult = { api_missing_count: 0, api_missing_items: [], api_implemented_count: 0, api_implemented_items: [] };
977
+ if (apiCheckInFlight) return emptyResult;
725
978
  apiCheckInFlight = true;
726
979
  try {
727
980
  sendApiCheckLine(`api-check start (${reason})`);
@@ -731,27 +984,292 @@ const triggerApiCheck = async (reason) => {
731
984
  logApiCheckStatus();
732
985
  if (!apiEndpointCandidatesCached.length) {
733
986
  sendApiCheckLine("api_endpoint_list is empty; skipping api-check.");
734
- return;
987
+ return emptyResult;
735
988
  }
736
- await runApiCheck();
989
+ return await runApiCheck();
737
990
  } finally {
738
991
  apiCheckInFlight = false;
739
992
  }
740
993
  };
741
994
 
742
995
  const triggerScreenCheck = async (reason) => {
996
+ const emptyResult = { screen_missing_count: 0, screen_missing_items: [], screen_implemented_count: 0, screen_implemented_items: [] };
743
997
  try {
744
998
  sendScreenCheckLine(`screen-check start (${reason})`);
745
- await runScreenCheck();
999
+ if (!screenCandidatesCached.length) {
1000
+ await fetchScreenList();
1001
+ }
1002
+ logScreenCheckStatus();
1003
+ if (!screenCandidatesCached.length) {
1004
+ sendScreenCheckLine("screen_list is empty; skipping screen-check.");
1005
+ return emptyResult;
1006
+ }
1007
+ return await runScreenCheck();
746
1008
  } catch (error) {
747
1009
  sendScreenCheckLine(`screen-check failed: ${error}`);
1010
+ return emptyResult;
748
1011
  }
749
1012
  };
750
1013
 
1014
+ // ---------------------------------------------------------------------------
1015
+ // Security Check (static regex-based)
1016
+ // ---------------------------------------------------------------------------
1017
+
1018
+ const SECURITY_RULES = [
1019
+ {
1020
+ id: "raw_sql",
1021
+ label: "SQLインジェクション",
1022
+ pattern: /(?:execute\s*\(\s*f["']|\.raw\s*\(|cursor\.execute\s*\(\s*f["'])/,
1023
+ extensions: new Set([".py", ".js", ".ts"]),
1024
+ },
1025
+ {
1026
+ id: "xss",
1027
+ label: "XSS",
1028
+ pattern: /(?:dangerouslySetInnerHTML|v-html)/,
1029
+ extensions: new Set([".js", ".jsx", ".ts", ".tsx", ".vue"]),
1030
+ },
1031
+ {
1032
+ id: "hardcoded_secrets",
1033
+ label: "秘密情報ハードコード",
1034
+ pattern: /(?:api_key|apikey|secret_key|password|aws_secret)\s*=\s*["'][^"']{4,}["']/i,
1035
+ extensions: new Set([".py", ".js", ".ts", ".env", ".yaml", ".yml", ".json"]),
1036
+ },
1037
+ {
1038
+ id: "stack_trace_leak",
1039
+ label: "スタックトレース漏洩",
1040
+ pattern: /(?:traceback\.format_exc|import\s+traceback)/,
1041
+ extensions: new Set([".py"]),
1042
+ },
1043
+ {
1044
+ id: "log_secrets",
1045
+ label: "ログ秘密情報",
1046
+ pattern: /(?:logger\.\w+.*(?:password|token|secret)|print\s*\(.*(?:password|token|secret))/i,
1047
+ extensions: new Set([".py", ".js", ".ts"]),
1048
+ },
1049
+ {
1050
+ id: "cors_wildcard",
1051
+ label: "CORSワイルドカード",
1052
+ pattern: /allow_origins\s*=\s*\[\s*["']\*["']\s*\]/,
1053
+ extensions: new Set([".py"]),
1054
+ },
1055
+ ];
1056
+
1057
+ const SECURITY_EXTENSIONS = new Set([".py", ".js", ".jsx", ".ts", ".tsx", ".vue", ".yaml", ".yml", ".json"]);
1058
+
1059
+ const collectSecurityTargetPaths = (rootDir, excludes) => {
1060
+ const results = [];
1061
+ const queue = [rootDir];
1062
+ while (queue.length) {
1063
+ const current = queue.pop();
1064
+ if (!current) continue;
1065
+ let entries = [];
1066
+ try {
1067
+ entries = fs.readdirSync(current, { withFileTypes: true });
1068
+ } catch (error) {
1069
+ continue;
1070
+ }
1071
+ for (const entry of entries) {
1072
+ const fullPath = path.join(current, entry.name);
1073
+ const relPath = path.relative(rootDir, fullPath).replace(/\\/g, "/");
1074
+ if (isExcludedPath(relPath, entry.name, excludes)) continue;
1075
+ if (entry.isSymbolicLink && entry.isSymbolicLink()) continue;
1076
+ if (entry.isDirectory()) {
1077
+ queue.push(fullPath);
1078
+ continue;
1079
+ }
1080
+ if (entry.isFile()) {
1081
+ const ext = path.extname(entry.name).toLowerCase();
1082
+ if (SECURITY_EXTENSIONS.has(ext)) {
1083
+ results.push({ relPath, fullPath, ext });
1084
+ }
1085
+ }
1086
+ }
1087
+ }
1088
+ return results;
1089
+ };
1090
+
1091
+ const SECURITY_CHECK_STEP_DELAY_MS = 400;
1092
+ const SECURITY_DETAIL_MAX_MATCHES = 5;
1093
+
1094
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1095
+
1096
+ const collectRuleMatches = (rule, files) => {
1097
+ const matches = [];
1098
+ for (const file of files) {
1099
+ if (!rule.extensions.has(file.ext)) continue;
1100
+ let content = "";
1101
+ try {
1102
+ const buffer = fs.readFileSync(file.fullPath);
1103
+ if (isBinaryBuffer(buffer)) continue;
1104
+ content = buffer.toString("utf8");
1105
+ } catch (error) {
1106
+ continue;
1107
+ }
1108
+ const lines = content.split("\n");
1109
+ const re = new RegExp(rule.pattern.source, rule.pattern.flags);
1110
+ for (let i = 0; i < lines.length; i++) {
1111
+ if (!re.test(lines[i])) continue;
1112
+ const contextStart = Math.max(0, i - 3);
1113
+ const contextEnd = Math.min(lines.length, i + 4);
1114
+ matches.push({
1115
+ file: file.relPath,
1116
+ line: i + 1,
1117
+ code: lines[i].trim(),
1118
+ context: lines.slice(contextStart, contextEnd).join("\n"),
1119
+ });
1120
+ if (matches.length >= SECURITY_DETAIL_MAX_MATCHES) return matches;
1121
+ }
1122
+ }
1123
+ return matches;
1124
+ };
1125
+
1126
+ const runSecurityCheck = async () => {
1127
+ const files = collectSecurityTargetPaths(projectRoot, DEFAULT_SNAPSHOT_EXCLUDES);
1128
+ const issueMap = {}; // id -> count
1129
+ const passedIds = [];
1130
+ const issueDetails = []; // { id, label, count }
1131
+ const total = SECURITY_RULES.length + 1; // +1 for .env check
1132
+
1133
+ // Check each rule
1134
+ let ruleIndex = 0;
1135
+ for (const rule of SECURITY_RULES) {
1136
+ ruleIndex += 1;
1137
+ // Collect detailed match info
1138
+ const ruleMatches = collectRuleMatches(rule, files);
1139
+ const hitCount = ruleMatches.length;
1140
+ if (hitCount > 0) {
1141
+ issueMap[rule.id] = hitCount;
1142
+ issueDetails.push({ id: rule.id, label: rule.label, count: hitCount });
1143
+ sendSecurityCheckLine(`progress ${ruleIndex}/${total} ${rule.id} issue ${hitCount}`);
1144
+ // Send detail line with match locations
1145
+ sendSecurityCheckLine(`detail ${JSON.stringify({ rule_id: rule.id, label: rule.label, matches: ruleMatches })}`);
1146
+ } else {
1147
+ passedIds.push(rule.id);
1148
+ sendSecurityCheckLine(`progress ${ruleIndex}/${total} ${rule.id} passed`);
1149
+ }
1150
+ // 1項目ずつ送信してバルーン更新を可視化
1151
+ flushLogBuffer();
1152
+ await sleep(SECURITY_CHECK_STEP_DELAY_MS);
1153
+ }
1154
+
1155
+ // Check .env in .gitignore
1156
+ let envGitignored = false;
1157
+ try {
1158
+ const gitignore = fs.readFileSync(path.join(projectRoot, ".gitignore"), "utf8");
1159
+ envGitignored = /^\.env$/m.test(gitignore);
1160
+ } catch (error) {
1161
+ // no .gitignore
1162
+ }
1163
+ if (!envGitignored) {
1164
+ issueDetails.push({ id: "env_not_gitignored", label: ".env未gitignore", count: 1 });
1165
+ issueMap["env_not_gitignored"] = 1;
1166
+ sendSecurityCheckLine(`progress ${total}/${total} env_not_gitignored issue 1`);
1167
+ } else {
1168
+ passedIds.push("env_not_gitignored");
1169
+ sendSecurityCheckLine(`progress ${total}/${total} env_not_gitignored passed`);
1170
+ }
1171
+ flushLogBuffer();
1172
+
1173
+ return {
1174
+ security_issue_count: issueDetails.length,
1175
+ security_issue_items: issueDetails.map((d) => `${d.id} (${d.count}箇所)`),
1176
+ security_passed_count: passedIds.length,
1177
+ security_passed_items: passedIds,
1178
+ };
1179
+ };
1180
+
1181
+ const triggerSecurityCheck = async (reason) => {
1182
+ sendSecurityCheckLine(`security-check start (${reason})`);
1183
+ flushLogBuffer();
1184
+ const result = await runSecurityCheck();
1185
+
1186
+ // Emit detail lines
1187
+ for (const item of result.security_issue_items) {
1188
+ sendSecurityCheckLine(`issue: ${item}`);
1189
+ }
1190
+ for (const id of result.security_passed_items) {
1191
+ sendSecurityCheckLine(`passed: ${id}`);
1192
+ }
1193
+ sendSecurityCheckLine(
1194
+ `issues=${result.security_issue_count} passed=${result.security_passed_count}`,
1195
+ );
1196
+ return result;
1197
+ };
1198
+
1199
+ const triggerCodeAnalysis = async (reason, role = null) => {
1200
+ const runApi = !role || role === "api";
1201
+ const runScreen = !role || role === "screen";
1202
+
1203
+ const steps = [];
1204
+ if (runApi) {
1205
+ const apiCount = apiEndpointCandidatesCached.length;
1206
+ const apiSuffix = apiCount ? ` (対象: ${apiCount}件)` : "";
1207
+ steps.push({ label: `API実装チェック中...${apiSuffix}`, fn: () => triggerApiCheck(`code-analysis:${reason}`) });
1208
+ }
1209
+ if (runScreen && enableScreenCheck) {
1210
+ const screenCount = screenCandidatesCached.length;
1211
+ const screenSuffix = screenCount ? ` (対象: ${screenCount}件)` : "";
1212
+ steps.push({ label: `画面実装チェック中...${screenSuffix}`, fn: () => triggerScreenCheck(`code-analysis:${reason}`) });
1213
+ }
1214
+ // Security check is always included
1215
+ steps.push({ label: "セキュリティチェック中...", fn: async () => triggerSecurityCheck(`code-analysis:${reason}`) });
1216
+
1217
+ const totalSteps = steps.length;
1218
+ let currentStep = 0;
1219
+
1220
+ sendCodeAnalysisLine(`code-analysis start (${reason})`);
1221
+
1222
+ // 各ステップの結果を収集
1223
+ const summary = {
1224
+ api_missing_count: 0, api_missing_items: [], api_implemented_count: 0, api_implemented_items: [],
1225
+ screen_missing_count: 0, screen_missing_items: [], screen_implemented_count: 0, screen_implemented_items: [],
1226
+ security_issue_count: 0, security_issue_items: [], security_passed_count: 0, security_passed_items: [],
1227
+ };
1228
+
1229
+ for (const step of steps) {
1230
+ currentStep += 1;
1231
+ sendCodeAnalysisLine(`[${currentStep}/${totalSteps}] ${step.label}`);
1232
+ const result = await step.fn();
1233
+ if (result) {
1234
+ Object.assign(summary, result);
1235
+ }
1236
+ }
1237
+
1238
+ const apiExpected = apiEndpointCandidatesCached.map(String);
1239
+ const screenExpected = screenCandidatesCached.map(toScreenName);
1240
+ const apiResultEmpty =
1241
+ summary.api_missing_count === 0 &&
1242
+ summary.api_implemented_count === 0 &&
1243
+ !summary.api_missing_items.length &&
1244
+ !summary.api_implemented_items.length;
1245
+ const screenResultEmpty =
1246
+ summary.screen_missing_count === 0 &&
1247
+ summary.screen_implemented_count === 0 &&
1248
+ !summary.screen_missing_items.length &&
1249
+ !summary.screen_implemented_items.length;
1250
+
1251
+ if (runApi && apiExpected.length && apiResultEmpty) {
1252
+ summary.api_missing_count = apiExpected.length;
1253
+ summary.api_missing_items = apiExpected;
1254
+ summary.api_check_inferred = true;
1255
+ }
1256
+ if (runScreen && enableScreenCheck && screenExpected.length && screenResultEmpty) {
1257
+ summary.screen_missing_count = screenExpected.length;
1258
+ summary.screen_missing_items = screenExpected;
1259
+ summary.screen_check_inferred = true;
1260
+ }
1261
+
1262
+ // done 行にサマリー JSON を埋め込む(バックエンドが確実にパースできるように)
1263
+ flushLogBuffer();
1264
+ sendCodeAnalysisLine(`done ${JSON.stringify(summary)}`);
1265
+ };
1266
+
751
1267
  const fetchApiEndpointList = async () => {
752
1268
  if (!apiEndpointUrl) return [];
753
1269
  if (typeof fetch !== "function") {
754
1270
  sendApiCheckLine("fetch is not available in this runtime; skipping fetch.");
1271
+ apiEndpointFetchStatus = "error";
1272
+ apiEndpointFetchDetail = "fetch_unavailable";
755
1273
  return [];
756
1274
  }
757
1275
  try {
@@ -770,6 +1288,8 @@ const fetchApiEndpointList = async () => {
770
1288
  sendApiCheckLine(
771
1289
  `api_endpoint_list取得失敗: ${response.status} ${response.statusText} (${apiEndpointUrl})${detailSnippet}`,
772
1290
  );
1291
+ apiEndpointFetchStatus = "error";
1292
+ apiEndpointFetchDetail = `${response.status}`;
773
1293
  return [];
774
1294
  }
775
1295
  const data = await response.json();
@@ -777,17 +1297,90 @@ const fetchApiEndpointList = async () => {
777
1297
  apiEndpointCandidatesCached = extractApiEndpointItems(
778
1298
  data.api_endpoint_list,
779
1299
  );
1300
+ apiEndpointFetchStatus = apiEndpointCandidatesCached.length
1301
+ ? "ok"
1302
+ : "ok_empty";
1303
+ apiEndpointFetchDetail = "";
780
1304
  return apiEndpointCandidatesCached;
781
1305
  }
782
1306
  if (Array.isArray(data?.endpoints)) {
783
1307
  apiEndpointCandidatesCached = extractApiEndpointItems(
784
1308
  JSON.stringify({ endpoints: data.endpoints }),
785
1309
  );
1310
+ apiEndpointFetchStatus = apiEndpointCandidatesCached.length
1311
+ ? "ok"
1312
+ : "ok_empty";
1313
+ apiEndpointFetchDetail = "";
786
1314
  return apiEndpointCandidatesCached;
787
1315
  }
1316
+ apiEndpointFetchStatus = "ok_empty";
1317
+ apiEndpointFetchDetail = "";
788
1318
  return [];
789
1319
  } catch (error) {
790
1320
  sendApiCheckLine(`api_endpoint_list fetch error: ${error}`);
1321
+ apiEndpointFetchStatus = "error";
1322
+ apiEndpointFetchDetail = "exception";
1323
+ return [];
1324
+ }
1325
+ };
1326
+
1327
+ const fetchScreenList = async () => {
1328
+ if (!screenListUrl) return [];
1329
+ if (typeof fetch !== "function") {
1330
+ sendScreenCheckLine("fetch is not available in this runtime; skipping fetch.");
1331
+ screenListFetchStatus = "error";
1332
+ screenListFetchDetail = "fetch_unavailable";
1333
+ return [];
1334
+ }
1335
+ try {
1336
+ const response = await fetch(screenListUrl, {
1337
+ method: "GET",
1338
+ headers: { "Content-Type": "application/json" },
1339
+ });
1340
+ if (!response.ok) {
1341
+ let detail = "";
1342
+ try {
1343
+ detail = await response.text();
1344
+ } catch (error) {
1345
+ detail = "";
1346
+ }
1347
+ const detailSnippet = detail ? ` detail=${detail.slice(0, 200)}` : "";
1348
+ sendScreenCheckLine(
1349
+ `screen_list取得失敗: ${response.status} ${response.statusText} (${screenListUrl})${detailSnippet}`,
1350
+ );
1351
+ screenListFetchStatus = "error";
1352
+ screenListFetchDetail = `${response.status}`;
1353
+ return [];
1354
+ }
1355
+ const data = await response.json();
1356
+ if (data?.screen_list) {
1357
+ const raw = Array.isArray(data?.screens) ? data.screens : [];
1358
+ screenCandidatesCached = raw.map((item) =>
1359
+ typeof item === "object" && item !== null ? item.name || String(item) : String(item)
1360
+ );
1361
+ screenListFetchStatus = screenCandidatesCached.length
1362
+ ? "ok"
1363
+ : "ok_empty";
1364
+ screenListFetchDetail = "";
1365
+ return screenCandidatesCached;
1366
+ }
1367
+ if (Array.isArray(data?.screens)) {
1368
+ screenCandidatesCached = data.screens.map((item) =>
1369
+ typeof item === "object" && item !== null ? item.name || String(item) : String(item)
1370
+ );
1371
+ screenListFetchStatus = screenCandidatesCached.length
1372
+ ? "ok"
1373
+ : "ok_empty";
1374
+ screenListFetchDetail = "";
1375
+ return screenCandidatesCached;
1376
+ }
1377
+ screenListFetchStatus = "ok_empty";
1378
+ screenListFetchDetail = "";
1379
+ return [];
1380
+ } catch (error) {
1381
+ sendScreenCheckLine(`screen_list fetch error: ${error}`);
1382
+ screenListFetchStatus = "error";
1383
+ screenListFetchDetail = "exception";
791
1384
  return [];
792
1385
  }
793
1386
  };
@@ -802,7 +1395,7 @@ const startFileTailer = (targetPath) => {
802
1395
  fileOffset = stats.size;
803
1396
  return true;
804
1397
  } catch (error) {
805
- log(`log-path not found: ${targetPath} (retrying...)`);
1398
+ log(`log-path not found: ${targetPath} (retrying...)`, "warn");
806
1399
  return false;
807
1400
  }
808
1401
  };
@@ -821,7 +1414,7 @@ const startFileTailer = (targetPath) => {
821
1414
  const readNewData = () => {
822
1415
  fs.stat(targetPath, (error, stats) => {
823
1416
  if (error) {
824
- log(`log-path read error: ${error.message || String(error)}`);
1417
+ log(`log-path read error: ${error.message || String(error)}`, "warn");
825
1418
  return;
826
1419
  }
827
1420
  if (stats.size < fileOffset) {
@@ -844,6 +1437,7 @@ const startFileTailer = (targetPath) => {
844
1437
  `log-path stream error: ${
845
1438
  streamError.message || String(streamError)
846
1439
  }`,
1440
+ "warn",
847
1441
  );
848
1442
  });
849
1443
  });
@@ -857,7 +1451,7 @@ const startFileTailer = (targetPath) => {
857
1451
  }
858
1452
  });
859
1453
  } catch (error) {
860
- log(`log-path watch error: ${error.message || String(error)}`);
1454
+ log(`log-path watch error: ${error.message || String(error)}`, "warn");
861
1455
  }
862
1456
  };
863
1457
 
@@ -874,21 +1468,23 @@ const startFileTailer = (targetPath) => {
874
1468
  startWatcher();
875
1469
  };
876
1470
 
877
- if (logPath) {
878
- startFileTailer(logPath);
879
- } else {
880
- const rl = readline.createInterface({
881
- input: process.stdin,
882
- crlfDelay: Infinity,
883
- });
1471
+ if (runMode !== "security") {
1472
+ if (logPath) {
1473
+ startFileTailer(logPath);
1474
+ } else {
1475
+ const rl = readline.createInterface({
1476
+ input: process.stdin,
1477
+ crlfDelay: Infinity,
1478
+ });
884
1479
 
885
- rl.on("line", (line) => {
886
- handleLine(line);
887
- });
1480
+ rl.on("line", (line) => {
1481
+ handleLine(line);
1482
+ });
888
1483
 
889
- rl.on("close", () => {
890
- if (socket) {
891
- socket.close();
892
- }
893
- });
1484
+ rl.on("close", () => {
1485
+ if (socket) {
1486
+ socket.close();
1487
+ }
1488
+ });
1489
+ }
894
1490
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderail-watch",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "description": "Stream terminal output to CodeRail backend over WebSocket.",
5
5
  "bin": {
6
6
  "coderail-watch": "bin/coderail-watch.js"