coderail-watch 0.1.8 → 0.1.12

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 +699 -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,8 +600,33 @@ const connect = () => {
456
600
  ? error.message
457
601
  : String(error);
458
602
  lastErrorMessage = message;
459
- log(`error: ${message}`);
603
+ log(`error: ${message}`, "warn");
604
+ if (error && error.code) {
605
+ log(` code: ${error.code}`, "warn");
606
+ }
607
+ if (error && error.errno) {
608
+ log(` errno: ${error.errno}`, "warn");
609
+ }
460
610
  });
611
+
612
+ // ws library emits 'unexpected-response' when server returns non-101 (e.g. 403)
613
+ if (typeof socket.on === "function") {
614
+ socket.on("unexpected-response", (req, res) => {
615
+ const status = res.statusCode;
616
+ let body = "";
617
+ res.on("data", (chunk) => { body += chunk.toString(); });
618
+ res.on("end", () => {
619
+ log(`server rejected WebSocket upgrade: HTTP ${status}`, "error");
620
+ if (body) {
621
+ log(` response body: ${body.slice(0, 500)}`, "error");
622
+ }
623
+ const headers = res.headers || {};
624
+ if (Object.keys(headers).length) {
625
+ log(` response headers: ${JSON.stringify(headers)}`, "error");
626
+ }
627
+ });
628
+ });
629
+ }
461
630
  };
462
631
 
463
632
  connect();
@@ -480,45 +649,96 @@ const enqueueOrSend = (payload) => {
480
649
  queue.push(payload);
481
650
  };
482
651
 
483
- const sendLogLine = (line) => {
652
+ const flushLogBuffer = () => {
653
+ if (logFlushTimer) {
654
+ clearTimeout(logFlushTimer);
655
+ logFlushTimer = null;
656
+ }
657
+ if (!logBuffer.length) return;
658
+ const lines = logBuffer.splice(0, logBuffer.length);
484
659
  const payload = JSON.stringify({
485
660
  type: "log",
486
- content: line,
661
+ content: lines.join("\n"),
487
662
  timestamp: new Date().toISOString(),
488
663
  });
489
664
  enqueueOrSend(payload);
490
665
  };
491
666
 
667
+ const enqueueLogLine = (line) => {
668
+ if (!line) return;
669
+ logBuffer.push(line);
670
+ if (logBatchMs === 0 || logBuffer.length >= logBatchMaxLines) {
671
+ flushLogBuffer();
672
+ return;
673
+ }
674
+ if (!logFlushTimer) {
675
+ logFlushTimer = setTimeout(flushLogBuffer, logBatchMs);
676
+ }
677
+ };
678
+
679
+ const sendLogLine = (line) => {
680
+ enqueueLogLine(line);
681
+ };
682
+
492
683
  const sendApiCheckLine = (line) => {
493
684
  const tagged = `[api-check] ${line}`;
494
- log(tagged);
685
+ log(tagged, "debug");
686
+ sendLogLine(tagged);
687
+ };
688
+
689
+ const sendCodeAnalysisLine = (line) => {
690
+ const tagged = `[code-analysis] ${line}`;
691
+ log(tagged, "info");
495
692
  sendLogLine(tagged);
496
693
  };
497
694
 
498
695
  const sendScreenCheckLine = (line) => {
499
696
  const tagged = `[screen-check] ${line}`;
500
- log(tagged);
697
+ log(tagged, "debug");
698
+ sendLogLine(tagged);
699
+ };
700
+
701
+ const sendSecurityCheckLine = (line) => {
702
+ const tagged = `[security-check] ${line}`;
703
+ log(tagged, "info");
501
704
  sendLogLine(tagged);
502
705
  };
503
706
 
504
707
  const logApiCheckStatus = () => {
505
708
  const maskedKey = openaiApiKey ? `${openaiApiKey.slice(0, 6)}...` : "";
709
+ const statusLabel =
710
+ apiEndpointFetchStatus === "error"
711
+ ? `NG(fetch_failed${apiEndpointFetchDetail ? `: ${apiEndpointFetchDetail}` : ""})`
712
+ : apiEndpointFetchStatus === "ok_empty"
713
+ ? "NG(empty)"
714
+ : apiEndpointCandidatesCached.length
715
+ ? "OK"
716
+ : "NG(unknown)";
506
717
  sendApiCheckLine(
507
- `必須チェック: api_endpoint_list=${
508
- apiEndpointCandidatesCached.length ? "OK" : "NG"
509
- } openai_api_key=${openaiApiKey ? "OK" : "NG"} ${
718
+ `必須チェック: api_endpoint_list=${statusLabel} openai_api_key=${
719
+ openaiApiKey ? "OK" : "NG"
720
+ } ${
510
721
  maskedKey ? `(${maskedKey})` : ""
511
722
  } openai_model=${openaiModel ? "OK" : "NG"}`,
512
723
  );
513
724
  };
514
725
 
726
+ const logScreenCheckStatus = () => {
727
+ const statusLabel =
728
+ screenListFetchStatus === "error"
729
+ ? `NG(fetch_failed${screenListFetchDetail ? `: ${screenListFetchDetail}` : ""})`
730
+ : screenListFetchStatus === "ok_empty"
731
+ ? "NG(empty)"
732
+ : screenCandidatesCached.length
733
+ ? "OK"
734
+ : "NG(unknown)";
735
+ sendScreenCheckLine(
736
+ `必須チェック: screen_list=${statusLabel}`,
737
+ );
738
+ };
739
+
515
740
  const handleLine = (line) => {
516
- const payload = JSON.stringify({
517
- type: "log",
518
- content: line,
519
- timestamp: new Date().toISOString(),
520
- });
521
- enqueueOrSend(payload);
741
+ sendLogLine(line);
522
742
  if (isErrorLine(line)) {
523
743
  const alertPayload = JSON.stringify({
524
744
  type: "alert",
@@ -538,11 +758,21 @@ const emitProjectTree = () => {
538
758
  projectTreeMaxBytes,
539
759
  );
540
760
  lines.forEach((line) => {
541
- log(line);
761
+ log(line, "debug");
542
762
  sendLogLine(line);
543
763
  });
544
764
  };
545
765
 
766
+ process.on("SIGINT", () => {
767
+ flushLogBuffer();
768
+ process.exit(0);
769
+ });
770
+
771
+ process.on("SIGTERM", () => {
772
+ flushLogBuffer();
773
+ process.exit(0);
774
+ });
775
+
546
776
  const buildApiCheckPrompt = (endpoints, treeLines) => {
547
777
  return [
548
778
  "You are a senior backend engineer. Determine which API endpoints are NOT implemented in the codebase.",
@@ -569,19 +799,25 @@ const collectTreeForApiCheck = () => {
569
799
  };
570
800
 
571
801
  const runApiCheck = async () => {
572
- if (!enableApiCheck) return;
573
- if (!apiEndpointCandidatesCached.length) return;
802
+ const allMissingResult = () => {
803
+ const items = apiEndpointCandidatesCached.map(String);
804
+ return { api_missing_count: items.length, api_missing_items: items, api_implemented_count: 0, api_implemented_items: [] };
805
+ };
806
+ if (!enableApiCheck) return allMissingResult();
807
+ if (!apiEndpointCandidatesCached.length) {
808
+ return { api_missing_count: 0, api_missing_items: [], api_implemented_count: 0, api_implemented_items: [] };
809
+ }
574
810
  if (!openaiApiKey) {
575
- sendApiCheckLine("OPENAI_API_KEY is not set; skipping API check.");
576
- return;
811
+ sendApiCheckLine("OPENAI_API_KEY is not set; すべて未実装として扱います。");
812
+ return allMissingResult();
577
813
  }
578
814
  if (!openaiModel) {
579
- sendApiCheckLine("OPENAI_MODEL is not set; skipping API check.");
580
- return;
815
+ sendApiCheckLine("OPENAI_MODEL is not set; すべて未実装として扱います。");
816
+ return allMissingResult();
581
817
  }
582
818
  if (typeof fetch !== "function") {
583
- sendApiCheckLine("fetch is not available in this runtime; skipping API check.");
584
- return;
819
+ sendApiCheckLine("fetch is not available in this runtime; すべて未実装として扱います。");
820
+ return allMissingResult();
585
821
  }
586
822
  const treeLines = collectTreeForApiCheck();
587
823
  const prompt = buildApiCheckPrompt(apiEndpointCandidatesCached, treeLines);
@@ -609,7 +845,7 @@ const runApiCheck = async () => {
609
845
  });
610
846
  } catch (error) {
611
847
  sendApiCheckLine(`OpenAI request failed: ${error}`);
612
- return;
848
+ return allMissingResult();
613
849
  }
614
850
  if (!response.ok) {
615
851
  const text = await response.text();
@@ -617,19 +853,19 @@ const runApiCheck = async () => {
617
853
  `OpenAI request failed: ${response.status} ${response.statusText}`,
618
854
  );
619
855
  sendApiCheckLine(text.slice(0, 1000));
620
- return;
856
+ return allMissingResult();
621
857
  }
622
858
  let data = null;
623
859
  try {
624
860
  data = await response.json();
625
861
  } catch (error) {
626
862
  sendApiCheckLine(`OpenAI response parse failed: ${error}`);
627
- return;
863
+ return allMissingResult();
628
864
  }
629
865
  const content = data?.choices?.[0]?.message?.content?.trim();
630
866
  if (!content) {
631
867
  sendApiCheckLine("OpenAI response was empty.");
632
- return;
868
+ return allMissingResult();
633
869
  }
634
870
  try {
635
871
  const parsed = JSON.parse(content);
@@ -641,38 +877,68 @@ const runApiCheck = async () => {
641
877
  sendApiCheckLine(
642
878
  `missing=${missing.length} unknown=${unknown.length} implemented=${implemented.length}`,
643
879
  );
880
+ const missingItems = [];
644
881
  missing.forEach((item) => {
645
882
  if (item && typeof item === "object") {
883
+ const label = item.endpoint || "";
884
+ missingItems.push(label);
646
885
  sendApiCheckLine(
647
- `missing: ${item.endpoint || ""} (${item.reason || "no reason"})`,
886
+ `missing: ${label} (${item.reason || "no reason"})`,
648
887
  );
649
888
  } else {
889
+ missingItems.push(String(item));
650
890
  sendApiCheckLine(`missing: ${String(item)}`);
651
891
  }
652
892
  });
893
+ const implementedItems = [];
653
894
  unknown.forEach((item) => sendApiCheckLine(`unknown: ${String(item)}`));
654
- implemented.forEach((item) =>
655
- sendApiCheckLine(`implemented: ${String(item)}`),
656
- );
895
+ implemented.forEach((item) => {
896
+ implementedItems.push(String(item));
897
+ sendApiCheckLine(`implemented: ${String(item)}`);
898
+ });
899
+ return {
900
+ api_missing_count: missing.length,
901
+ api_missing_items: missingItems,
902
+ api_implemented_count: implemented.length,
903
+ api_implemented_items: implementedItems,
904
+ };
657
905
  } catch (error) {
658
906
  sendApiCheckLine("OpenAI output was not valid JSON. Raw output:");
659
907
  sendApiCheckLine(content.slice(0, 2000));
908
+ return allMissingResult();
660
909
  }
661
910
  };
662
911
 
663
912
  const screenCheckUrl = `${normalizeBaseHttpUrl(baseUrl)}/api/v1/project_snapshot/check`;
664
913
 
665
914
  const runScreenCheck = async () => {
915
+ const allMissingResult = () => {
916
+ const items = screenCandidatesCached.map(toScreenName);
917
+ return { screen_missing_count: items.length, screen_missing_items: items, screen_implemented_count: 0, screen_implemented_items: [] };
918
+ };
666
919
  if (!projectKey) {
667
- sendScreenCheckLine("project_key is missing; skipping screen check.");
668
- return;
920
+ sendScreenCheckLine("project_key is missing; すべて未実装として扱います。");
921
+ return allMissingResult();
669
922
  }
670
923
  if (typeof fetch !== "function") {
671
- sendScreenCheckLine("fetch is not available in this runtime; skipping screen check.");
672
- return;
924
+ sendScreenCheckLine("fetch is not available in this runtime; すべて未実装として扱います。");
925
+ return allMissingResult();
673
926
  }
674
- const paths = collectSnapshotPaths(projectRoot, DEFAULT_SNAPSHOT_EXCLUDES);
927
+ const paths = collectSnapshotPaths(projectRoot, DEFAULT_SNAPSHOT_EXCLUDES).filter(
928
+ (item) => item.startsWith("frontend/"),
929
+ );
675
930
  const limitedPaths = paths.slice(0, projectTreeMaxFiles);
931
+ if (!limitedPaths.length) {
932
+ sendScreenCheckLine("frontend/ 配下のファイルが見つからないため、すべて未実装として扱います。");
933
+ const allMissing = screenCandidatesCached.map(toScreenName);
934
+ allMissing.forEach((item) => sendScreenCheckLine(`missing: ${item}`));
935
+ return {
936
+ screen_missing_count: allMissing.length,
937
+ screen_missing_items: allMissing,
938
+ screen_implemented_count: 0,
939
+ screen_implemented_items: [],
940
+ };
941
+ }
676
942
  const payload = {
677
943
  projectPublicId: projectKey,
678
944
  paths: limitedPaths,
@@ -686,7 +952,7 @@ const runScreenCheck = async () => {
686
952
  });
687
953
  } catch (error) {
688
954
  sendScreenCheckLine(`screen check request failed: ${error}`);
689
- return;
955
+ return allMissingResult();
690
956
  }
691
957
  if (!response.ok) {
692
958
  const text = await response.text();
@@ -694,19 +960,24 @@ const runScreenCheck = async () => {
694
960
  `screen check failed: ${response.status} ${response.statusText}`,
695
961
  );
696
962
  sendScreenCheckLine(text.slice(0, 1000));
697
- return;
963
+ return allMissingResult();
698
964
  }
699
965
  let data = null;
700
966
  try {
701
967
  data = await response.json();
702
968
  } catch (error) {
703
969
  sendScreenCheckLine(`screen check response parse failed: ${error}`);
704
- return;
970
+ return allMissingResult();
705
971
  }
706
- const expected = Array.isArray(data?.expectedScreens) ? data.expectedScreens : [];
707
- const missing = Array.isArray(data?.check?.missingScreens)
972
+ const expected = screenCandidatesCached.length
973
+ ? screenCandidatesCached.map(toScreenName)
974
+ : (Array.isArray(data?.expectedScreens) ? data.expectedScreens : []);
975
+ const rawMissing = Array.isArray(data?.check?.missingScreens)
708
976
  ? data.check.missingScreens
709
977
  : [];
978
+ // expected をキャッシュに揃えたので、missing も expected に含まれるもののみに絞る
979
+ const expectedSet = new Set(expected.map(String));
980
+ const missing = rawMissing.filter((item) => expectedSet.has(String(item)));
710
981
  const implemented = expected.filter((item) => !missing.includes(item));
711
982
  sendScreenCheckLine(
712
983
  `missing_screens=${missing.length} expected=${expected.length} implemented=${implemented.length}`,
@@ -718,10 +989,17 @@ const runScreenCheck = async () => {
718
989
  if (data?.check?.notes) {
719
990
  sendScreenCheckLine(`note: ${String(data.check.notes)}`);
720
991
  }
992
+ return {
993
+ screen_missing_count: missing.length,
994
+ screen_missing_items: missing.map(String),
995
+ screen_implemented_count: implemented.length,
996
+ screen_implemented_items: implemented.map(String),
997
+ };
721
998
  };
722
999
 
723
1000
  const triggerApiCheck = async (reason) => {
724
- if (apiCheckInFlight) return;
1001
+ const emptyResult = { api_missing_count: 0, api_missing_items: [], api_implemented_count: 0, api_implemented_items: [] };
1002
+ if (apiCheckInFlight) return emptyResult;
725
1003
  apiCheckInFlight = true;
726
1004
  try {
727
1005
  sendApiCheckLine(`api-check start (${reason})`);
@@ -731,27 +1009,292 @@ const triggerApiCheck = async (reason) => {
731
1009
  logApiCheckStatus();
732
1010
  if (!apiEndpointCandidatesCached.length) {
733
1011
  sendApiCheckLine("api_endpoint_list is empty; skipping api-check.");
734
- return;
1012
+ return emptyResult;
735
1013
  }
736
- await runApiCheck();
1014
+ return await runApiCheck();
737
1015
  } finally {
738
1016
  apiCheckInFlight = false;
739
1017
  }
740
1018
  };
741
1019
 
742
1020
  const triggerScreenCheck = async (reason) => {
1021
+ const emptyResult = { screen_missing_count: 0, screen_missing_items: [], screen_implemented_count: 0, screen_implemented_items: [] };
743
1022
  try {
744
1023
  sendScreenCheckLine(`screen-check start (${reason})`);
745
- await runScreenCheck();
1024
+ if (!screenCandidatesCached.length) {
1025
+ await fetchScreenList();
1026
+ }
1027
+ logScreenCheckStatus();
1028
+ if (!screenCandidatesCached.length) {
1029
+ sendScreenCheckLine("screen_list is empty; skipping screen-check.");
1030
+ return emptyResult;
1031
+ }
1032
+ return await runScreenCheck();
746
1033
  } catch (error) {
747
1034
  sendScreenCheckLine(`screen-check failed: ${error}`);
1035
+ return emptyResult;
748
1036
  }
749
1037
  };
750
1038
 
1039
+ // ---------------------------------------------------------------------------
1040
+ // Security Check (static regex-based)
1041
+ // ---------------------------------------------------------------------------
1042
+
1043
+ const SECURITY_RULES = [
1044
+ {
1045
+ id: "raw_sql",
1046
+ label: "SQLインジェクション",
1047
+ pattern: /(?:execute\s*\(\s*f["']|\.raw\s*\(|cursor\.execute\s*\(\s*f["'])/,
1048
+ extensions: new Set([".py", ".js", ".ts"]),
1049
+ },
1050
+ {
1051
+ id: "xss",
1052
+ label: "XSS",
1053
+ pattern: /(?:dangerouslySetInnerHTML|v-html)/,
1054
+ extensions: new Set([".js", ".jsx", ".ts", ".tsx", ".vue"]),
1055
+ },
1056
+ {
1057
+ id: "hardcoded_secrets",
1058
+ label: "秘密情報ハードコード",
1059
+ pattern: /(?:api_key|apikey|secret_key|password|aws_secret)\s*=\s*["'][^"']{4,}["']/i,
1060
+ extensions: new Set([".py", ".js", ".ts", ".env", ".yaml", ".yml", ".json"]),
1061
+ },
1062
+ {
1063
+ id: "stack_trace_leak",
1064
+ label: "スタックトレース漏洩",
1065
+ pattern: /(?:traceback\.format_exc|import\s+traceback)/,
1066
+ extensions: new Set([".py"]),
1067
+ },
1068
+ {
1069
+ id: "log_secrets",
1070
+ label: "ログ秘密情報",
1071
+ pattern: /(?:logger\.\w+.*(?:password|token|secret)|print\s*\(.*(?:password|token|secret))/i,
1072
+ extensions: new Set([".py", ".js", ".ts"]),
1073
+ },
1074
+ {
1075
+ id: "cors_wildcard",
1076
+ label: "CORSワイルドカード",
1077
+ pattern: /allow_origins\s*=\s*\[\s*["']\*["']\s*\]/,
1078
+ extensions: new Set([".py"]),
1079
+ },
1080
+ ];
1081
+
1082
+ const SECURITY_EXTENSIONS = new Set([".py", ".js", ".jsx", ".ts", ".tsx", ".vue", ".yaml", ".yml", ".json"]);
1083
+
1084
+ const collectSecurityTargetPaths = (rootDir, excludes) => {
1085
+ const results = [];
1086
+ const queue = [rootDir];
1087
+ while (queue.length) {
1088
+ const current = queue.pop();
1089
+ if (!current) continue;
1090
+ let entries = [];
1091
+ try {
1092
+ entries = fs.readdirSync(current, { withFileTypes: true });
1093
+ } catch (error) {
1094
+ continue;
1095
+ }
1096
+ for (const entry of entries) {
1097
+ const fullPath = path.join(current, entry.name);
1098
+ const relPath = path.relative(rootDir, fullPath).replace(/\\/g, "/");
1099
+ if (isExcludedPath(relPath, entry.name, excludes)) continue;
1100
+ if (entry.isSymbolicLink && entry.isSymbolicLink()) continue;
1101
+ if (entry.isDirectory()) {
1102
+ queue.push(fullPath);
1103
+ continue;
1104
+ }
1105
+ if (entry.isFile()) {
1106
+ const ext = path.extname(entry.name).toLowerCase();
1107
+ if (SECURITY_EXTENSIONS.has(ext)) {
1108
+ results.push({ relPath, fullPath, ext });
1109
+ }
1110
+ }
1111
+ }
1112
+ }
1113
+ return results;
1114
+ };
1115
+
1116
+ const SECURITY_CHECK_STEP_DELAY_MS = 400;
1117
+ const SECURITY_DETAIL_MAX_MATCHES = 5;
1118
+
1119
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1120
+
1121
+ const collectRuleMatches = (rule, files) => {
1122
+ const matches = [];
1123
+ for (const file of files) {
1124
+ if (!rule.extensions.has(file.ext)) continue;
1125
+ let content = "";
1126
+ try {
1127
+ const buffer = fs.readFileSync(file.fullPath);
1128
+ if (isBinaryBuffer(buffer)) continue;
1129
+ content = buffer.toString("utf8");
1130
+ } catch (error) {
1131
+ continue;
1132
+ }
1133
+ const lines = content.split("\n");
1134
+ const re = new RegExp(rule.pattern.source, rule.pattern.flags);
1135
+ for (let i = 0; i < lines.length; i++) {
1136
+ if (!re.test(lines[i])) continue;
1137
+ const contextStart = Math.max(0, i - 3);
1138
+ const contextEnd = Math.min(lines.length, i + 4);
1139
+ matches.push({
1140
+ file: file.relPath,
1141
+ line: i + 1,
1142
+ code: lines[i].trim(),
1143
+ context: lines.slice(contextStart, contextEnd).join("\n"),
1144
+ });
1145
+ if (matches.length >= SECURITY_DETAIL_MAX_MATCHES) return matches;
1146
+ }
1147
+ }
1148
+ return matches;
1149
+ };
1150
+
1151
+ const runSecurityCheck = async () => {
1152
+ const files = collectSecurityTargetPaths(projectRoot, DEFAULT_SNAPSHOT_EXCLUDES);
1153
+ const issueMap = {}; // id -> count
1154
+ const passedIds = [];
1155
+ const issueDetails = []; // { id, label, count }
1156
+ const total = SECURITY_RULES.length + 1; // +1 for .env check
1157
+
1158
+ // Check each rule
1159
+ let ruleIndex = 0;
1160
+ for (const rule of SECURITY_RULES) {
1161
+ ruleIndex += 1;
1162
+ // Collect detailed match info
1163
+ const ruleMatches = collectRuleMatches(rule, files);
1164
+ const hitCount = ruleMatches.length;
1165
+ if (hitCount > 0) {
1166
+ issueMap[rule.id] = hitCount;
1167
+ issueDetails.push({ id: rule.id, label: rule.label, count: hitCount });
1168
+ sendSecurityCheckLine(`progress ${ruleIndex}/${total} ${rule.id} issue ${hitCount}`);
1169
+ // Send detail line with match locations
1170
+ sendSecurityCheckLine(`detail ${JSON.stringify({ rule_id: rule.id, label: rule.label, matches: ruleMatches })}`);
1171
+ } else {
1172
+ passedIds.push(rule.id);
1173
+ sendSecurityCheckLine(`progress ${ruleIndex}/${total} ${rule.id} passed`);
1174
+ }
1175
+ // 1項目ずつ送信してバルーン更新を可視化
1176
+ flushLogBuffer();
1177
+ await sleep(SECURITY_CHECK_STEP_DELAY_MS);
1178
+ }
1179
+
1180
+ // Check .env in .gitignore
1181
+ let envGitignored = false;
1182
+ try {
1183
+ const gitignore = fs.readFileSync(path.join(projectRoot, ".gitignore"), "utf8");
1184
+ envGitignored = /^\.env$/m.test(gitignore);
1185
+ } catch (error) {
1186
+ // no .gitignore
1187
+ }
1188
+ if (!envGitignored) {
1189
+ issueDetails.push({ id: "env_not_gitignored", label: ".env未gitignore", count: 1 });
1190
+ issueMap["env_not_gitignored"] = 1;
1191
+ sendSecurityCheckLine(`progress ${total}/${total} env_not_gitignored issue 1`);
1192
+ } else {
1193
+ passedIds.push("env_not_gitignored");
1194
+ sendSecurityCheckLine(`progress ${total}/${total} env_not_gitignored passed`);
1195
+ }
1196
+ flushLogBuffer();
1197
+
1198
+ return {
1199
+ security_issue_count: issueDetails.length,
1200
+ security_issue_items: issueDetails.map((d) => `${d.id} (${d.count}箇所)`),
1201
+ security_passed_count: passedIds.length,
1202
+ security_passed_items: passedIds,
1203
+ };
1204
+ };
1205
+
1206
+ const triggerSecurityCheck = async (reason) => {
1207
+ sendSecurityCheckLine(`security-check start (${reason})`);
1208
+ flushLogBuffer();
1209
+ const result = await runSecurityCheck();
1210
+
1211
+ // Emit detail lines
1212
+ for (const item of result.security_issue_items) {
1213
+ sendSecurityCheckLine(`issue: ${item}`);
1214
+ }
1215
+ for (const id of result.security_passed_items) {
1216
+ sendSecurityCheckLine(`passed: ${id}`);
1217
+ }
1218
+ sendSecurityCheckLine(
1219
+ `issues=${result.security_issue_count} passed=${result.security_passed_count}`,
1220
+ );
1221
+ return result;
1222
+ };
1223
+
1224
+ const triggerCodeAnalysis = async (reason, role = null) => {
1225
+ const runApi = !role || role === "api";
1226
+ const runScreen = !role || role === "screen";
1227
+
1228
+ const steps = [];
1229
+ if (runApi) {
1230
+ const apiCount = apiEndpointCandidatesCached.length;
1231
+ const apiSuffix = apiCount ? ` (対象: ${apiCount}件)` : "";
1232
+ steps.push({ label: `API実装チェック中...${apiSuffix}`, fn: () => triggerApiCheck(`code-analysis:${reason}`) });
1233
+ }
1234
+ if (runScreen && enableScreenCheck) {
1235
+ const screenCount = screenCandidatesCached.length;
1236
+ const screenSuffix = screenCount ? ` (対象: ${screenCount}件)` : "";
1237
+ steps.push({ label: `画面実装チェック中...${screenSuffix}`, fn: () => triggerScreenCheck(`code-analysis:${reason}`) });
1238
+ }
1239
+ // Security check is always included
1240
+ steps.push({ label: "セキュリティチェック中...", fn: async () => triggerSecurityCheck(`code-analysis:${reason}`) });
1241
+
1242
+ const totalSteps = steps.length;
1243
+ let currentStep = 0;
1244
+
1245
+ sendCodeAnalysisLine(`code-analysis start (${reason})`);
1246
+
1247
+ // 各ステップの結果を収集
1248
+ const summary = {
1249
+ api_missing_count: 0, api_missing_items: [], api_implemented_count: 0, api_implemented_items: [],
1250
+ screen_missing_count: 0, screen_missing_items: [], screen_implemented_count: 0, screen_implemented_items: [],
1251
+ security_issue_count: 0, security_issue_items: [], security_passed_count: 0, security_passed_items: [],
1252
+ };
1253
+
1254
+ for (const step of steps) {
1255
+ currentStep += 1;
1256
+ sendCodeAnalysisLine(`[${currentStep}/${totalSteps}] ${step.label}`);
1257
+ const result = await step.fn();
1258
+ if (result) {
1259
+ Object.assign(summary, result);
1260
+ }
1261
+ }
1262
+
1263
+ const apiExpected = apiEndpointCandidatesCached.map(String);
1264
+ const screenExpected = screenCandidatesCached.map(toScreenName);
1265
+ const apiResultEmpty =
1266
+ summary.api_missing_count === 0 &&
1267
+ summary.api_implemented_count === 0 &&
1268
+ !summary.api_missing_items.length &&
1269
+ !summary.api_implemented_items.length;
1270
+ const screenResultEmpty =
1271
+ summary.screen_missing_count === 0 &&
1272
+ summary.screen_implemented_count === 0 &&
1273
+ !summary.screen_missing_items.length &&
1274
+ !summary.screen_implemented_items.length;
1275
+
1276
+ if (runApi && apiExpected.length && apiResultEmpty) {
1277
+ summary.api_missing_count = apiExpected.length;
1278
+ summary.api_missing_items = apiExpected;
1279
+ summary.api_check_inferred = true;
1280
+ }
1281
+ if (runScreen && enableScreenCheck && screenExpected.length && screenResultEmpty) {
1282
+ summary.screen_missing_count = screenExpected.length;
1283
+ summary.screen_missing_items = screenExpected;
1284
+ summary.screen_check_inferred = true;
1285
+ }
1286
+
1287
+ // done 行にサマリー JSON を埋め込む(バックエンドが確実にパースできるように)
1288
+ flushLogBuffer();
1289
+ sendCodeAnalysisLine(`done ${JSON.stringify(summary)}`);
1290
+ };
1291
+
751
1292
  const fetchApiEndpointList = async () => {
752
1293
  if (!apiEndpointUrl) return [];
753
1294
  if (typeof fetch !== "function") {
754
1295
  sendApiCheckLine("fetch is not available in this runtime; skipping fetch.");
1296
+ apiEndpointFetchStatus = "error";
1297
+ apiEndpointFetchDetail = "fetch_unavailable";
755
1298
  return [];
756
1299
  }
757
1300
  try {
@@ -770,6 +1313,8 @@ const fetchApiEndpointList = async () => {
770
1313
  sendApiCheckLine(
771
1314
  `api_endpoint_list取得失敗: ${response.status} ${response.statusText} (${apiEndpointUrl})${detailSnippet}`,
772
1315
  );
1316
+ apiEndpointFetchStatus = "error";
1317
+ apiEndpointFetchDetail = `${response.status}`;
773
1318
  return [];
774
1319
  }
775
1320
  const data = await response.json();
@@ -777,17 +1322,90 @@ const fetchApiEndpointList = async () => {
777
1322
  apiEndpointCandidatesCached = extractApiEndpointItems(
778
1323
  data.api_endpoint_list,
779
1324
  );
1325
+ apiEndpointFetchStatus = apiEndpointCandidatesCached.length
1326
+ ? "ok"
1327
+ : "ok_empty";
1328
+ apiEndpointFetchDetail = "";
780
1329
  return apiEndpointCandidatesCached;
781
1330
  }
782
1331
  if (Array.isArray(data?.endpoints)) {
783
1332
  apiEndpointCandidatesCached = extractApiEndpointItems(
784
1333
  JSON.stringify({ endpoints: data.endpoints }),
785
1334
  );
1335
+ apiEndpointFetchStatus = apiEndpointCandidatesCached.length
1336
+ ? "ok"
1337
+ : "ok_empty";
1338
+ apiEndpointFetchDetail = "";
786
1339
  return apiEndpointCandidatesCached;
787
1340
  }
1341
+ apiEndpointFetchStatus = "ok_empty";
1342
+ apiEndpointFetchDetail = "";
788
1343
  return [];
789
1344
  } catch (error) {
790
1345
  sendApiCheckLine(`api_endpoint_list fetch error: ${error}`);
1346
+ apiEndpointFetchStatus = "error";
1347
+ apiEndpointFetchDetail = "exception";
1348
+ return [];
1349
+ }
1350
+ };
1351
+
1352
+ const fetchScreenList = async () => {
1353
+ if (!screenListUrl) return [];
1354
+ if (typeof fetch !== "function") {
1355
+ sendScreenCheckLine("fetch is not available in this runtime; skipping fetch.");
1356
+ screenListFetchStatus = "error";
1357
+ screenListFetchDetail = "fetch_unavailable";
1358
+ return [];
1359
+ }
1360
+ try {
1361
+ const response = await fetch(screenListUrl, {
1362
+ method: "GET",
1363
+ headers: { "Content-Type": "application/json" },
1364
+ });
1365
+ if (!response.ok) {
1366
+ let detail = "";
1367
+ try {
1368
+ detail = await response.text();
1369
+ } catch (error) {
1370
+ detail = "";
1371
+ }
1372
+ const detailSnippet = detail ? ` detail=${detail.slice(0, 200)}` : "";
1373
+ sendScreenCheckLine(
1374
+ `screen_list取得失敗: ${response.status} ${response.statusText} (${screenListUrl})${detailSnippet}`,
1375
+ );
1376
+ screenListFetchStatus = "error";
1377
+ screenListFetchDetail = `${response.status}`;
1378
+ return [];
1379
+ }
1380
+ const data = await response.json();
1381
+ if (data?.screen_list) {
1382
+ const raw = Array.isArray(data?.screens) ? data.screens : [];
1383
+ screenCandidatesCached = raw.map((item) =>
1384
+ typeof item === "object" && item !== null ? item.name || String(item) : String(item)
1385
+ );
1386
+ screenListFetchStatus = screenCandidatesCached.length
1387
+ ? "ok"
1388
+ : "ok_empty";
1389
+ screenListFetchDetail = "";
1390
+ return screenCandidatesCached;
1391
+ }
1392
+ if (Array.isArray(data?.screens)) {
1393
+ screenCandidatesCached = data.screens.map((item) =>
1394
+ typeof item === "object" && item !== null ? item.name || String(item) : String(item)
1395
+ );
1396
+ screenListFetchStatus = screenCandidatesCached.length
1397
+ ? "ok"
1398
+ : "ok_empty";
1399
+ screenListFetchDetail = "";
1400
+ return screenCandidatesCached;
1401
+ }
1402
+ screenListFetchStatus = "ok_empty";
1403
+ screenListFetchDetail = "";
1404
+ return [];
1405
+ } catch (error) {
1406
+ sendScreenCheckLine(`screen_list fetch error: ${error}`);
1407
+ screenListFetchStatus = "error";
1408
+ screenListFetchDetail = "exception";
791
1409
  return [];
792
1410
  }
793
1411
  };
@@ -802,7 +1420,7 @@ const startFileTailer = (targetPath) => {
802
1420
  fileOffset = stats.size;
803
1421
  return true;
804
1422
  } catch (error) {
805
- log(`log-path not found: ${targetPath} (retrying...)`);
1423
+ log(`log-path not found: ${targetPath} (retrying...)`, "warn");
806
1424
  return false;
807
1425
  }
808
1426
  };
@@ -821,7 +1439,7 @@ const startFileTailer = (targetPath) => {
821
1439
  const readNewData = () => {
822
1440
  fs.stat(targetPath, (error, stats) => {
823
1441
  if (error) {
824
- log(`log-path read error: ${error.message || String(error)}`);
1442
+ log(`log-path read error: ${error.message || String(error)}`, "warn");
825
1443
  return;
826
1444
  }
827
1445
  if (stats.size < fileOffset) {
@@ -844,6 +1462,7 @@ const startFileTailer = (targetPath) => {
844
1462
  `log-path stream error: ${
845
1463
  streamError.message || String(streamError)
846
1464
  }`,
1465
+ "warn",
847
1466
  );
848
1467
  });
849
1468
  });
@@ -857,7 +1476,7 @@ const startFileTailer = (targetPath) => {
857
1476
  }
858
1477
  });
859
1478
  } catch (error) {
860
- log(`log-path watch error: ${error.message || String(error)}`);
1479
+ log(`log-path watch error: ${error.message || String(error)}`, "warn");
861
1480
  }
862
1481
  };
863
1482
 
@@ -874,21 +1493,23 @@ const startFileTailer = (targetPath) => {
874
1493
  startWatcher();
875
1494
  };
876
1495
 
877
- if (logPath) {
878
- startFileTailer(logPath);
879
- } else {
880
- const rl = readline.createInterface({
881
- input: process.stdin,
882
- crlfDelay: Infinity,
883
- });
1496
+ if (runMode !== "security") {
1497
+ if (logPath) {
1498
+ startFileTailer(logPath);
1499
+ } else {
1500
+ const rl = readline.createInterface({
1501
+ input: process.stdin,
1502
+ crlfDelay: Infinity,
1503
+ });
884
1504
 
885
- rl.on("line", (line) => {
886
- handleLine(line);
887
- });
1505
+ rl.on("line", (line) => {
1506
+ handleLine(line);
1507
+ });
888
1508
 
889
- rl.on("close", () => {
890
- if (socket) {
891
- socket.close();
892
- }
893
- });
1509
+ rl.on("close", () => {
1510
+ if (socket) {
1511
+ socket.close();
1512
+ }
1513
+ });
1514
+ }
894
1515
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderail-watch",
3
- "version": "0.1.8",
3
+ "version": "0.1.12",
4
4
  "description": "Stream terminal output to CodeRail backend over WebSocket.",
5
5
  "bin": {
6
6
  "coderail-watch": "bin/coderail-watch.js"