@cartanova/qgrid-cli 2.10.3 → 2.10.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.
Files changed (55) hide show
  1. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +63 -140
  2. package/bundle/dist/application/qgrid/qgrid.frame.js +2 -5
  3. package/bundle/dist/application/setting/setting.model.js +2 -64
  4. package/bundle/dist/application/setting/setting.store.js +3 -4
  5. package/bundle/dist/application/setting/setting.types.js +2 -12
  6. package/bundle/dist/i18n/en.js +1 -3
  7. package/bundle/dist/i18n/ko.js +1 -3
  8. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +0 -111
  9. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +82 -178
  10. package/bundle/src/application/qgrid/qgrid.frame.test.ts +0 -38
  11. package/bundle/src/application/qgrid/qgrid.frame.ts +0 -4
  12. package/bundle/src/application/setting/setting.model.test.ts +1 -101
  13. package/bundle/src/application/setting/setting.model.ts +0 -73
  14. package/bundle/src/application/setting/setting.store.ts +2 -3
  15. package/bundle/src/application/setting/setting.types.ts +0 -12
  16. package/bundle/src/application/sonamu.generated.http +2 -7
  17. package/bundle/src/i18n/en.ts +0 -3
  18. package/bundle/src/i18n/ko.ts +0 -3
  19. package/bundle/web-dist/client/assets/index-CvG4UB9v.css +1 -0
  20. package/bundle/web-dist/client/assets/{index-BIC1sQxw.js → index-reW8VvJF.js} +4 -4
  21. package/bundle/web-dist/client/assets/{logs-zvdujZWn.js → logs-DDIt97D1.js} +1 -1
  22. package/bundle/web-dist/client/assets/{monit-CraVV3MQ.js → monit-DkABv7PG.js} +1 -1
  23. package/bundle/web-dist/client/assets/{routes-B3ZBoJ60.js → routes-BRmQ4M3-.js} +1 -1
  24. package/bundle/web-dist/client/assets/services.generated-jnPHYMER.js +51 -0
  25. package/bundle/web-dist/client/assets/settings-BHvtycEZ.js +1 -0
  26. package/bundle/web-dist/client/assets/{show-ItTExdAh.js → show-qeTIzgDZ.js} +1 -1
  27. package/bundle/web-dist/client/assets/{tokens-BynKx8Vn.js → tokens-5Bep_G-w.js} +1 -1
  28. package/bundle/web-dist/client/assets/{use-oauth-login-flow-8xw6on5P.js → use-oauth-login-flow-Cx-QO25M.js} +1 -1
  29. package/bundle/web-dist/client/assets/{use-update-token-mutation-D-Y8OySK.js → use-update-token-mutation-DQCwCYfR.js} +1 -1
  30. package/bundle/web-dist/client/index.html +3 -3
  31. package/bundle/web-dist/server/assets/{lazyRouteComponent-Bj1cPq6x.js → lazyRouteComponent-CUR-kWzI.js} +2 -2
  32. package/bundle/web-dist/server/assets/{logs-BSk9lOHe.js → logs-CagNGTBj.js} +3 -3
  33. package/bundle/web-dist/server/assets/{monit-D9W4ioUi.js → monit-DSxs4Jnl.js} +3 -3
  34. package/bundle/web-dist/server/assets/{plus-CmvbWuDf.js → plus-Bvdj8Pwx.js} +1 -1
  35. package/bundle/web-dist/server/assets/{react-dom-BjR1Iaz8.js → react-dom-CyuShs9K.js} +1 -1
  36. package/bundle/web-dist/server/assets/{refresh-cw-B8U9BL9-.js → refresh-cw-3QAiSWBd.js} +1 -1
  37. package/bundle/web-dist/server/assets/{routes-C7lB2Q85.js → routes-i-sTqXMX.js} +4 -4
  38. package/bundle/web-dist/server/assets/{services.generated-BM4X3Ecj.js → services.generated-FPwsG9p3.js} +116 -131
  39. package/bundle/web-dist/server/assets/{settings-BFVuldch.js → settings-DeHNwoMv.js} +40 -251
  40. package/bundle/web-dist/server/assets/{show-Cswh_tqs.js → show-C4jk1Hrp.js} +2 -2
  41. package/bundle/web-dist/server/assets/{tokens-DiTUwUwr.js → tokens-odgS2-9q.js} +4 -4
  42. package/bundle/web-dist/server/assets/{use-oauth-login-flow-DYKnG4A2.js → use-oauth-login-flow-DTQKdYBO.js} +1 -1
  43. package/bundle/web-dist/server/assets/{use-update-token-mutation-DqGMopII.js → use-update-token-mutation-xvXbPrCI.js} +1 -1
  44. package/bundle/web-dist/server/entry-server.generated.js +10 -10
  45. package/package.json +1 -1
  46. package/skills/qgrid/references/cli-env-and-server-boot.md +12 -9
  47. package/bundle/dist/utils/process-supervisor.js +0 -22
  48. package/bundle/dist/utils/server-restart.js +0 -61
  49. package/bundle/src/utils/process-supervisor.test.ts +0 -24
  50. package/bundle/src/utils/process-supervisor.ts +0 -17
  51. package/bundle/src/utils/server-restart.test.ts +0 -76
  52. package/bundle/src/utils/server-restart.ts +0 -68
  53. package/bundle/web-dist/client/assets/index-D_sagzsR.css +0 -1
  54. package/bundle/web-dist/client/assets/services.generated-Cbvg2sGQ.js +0 -51
  55. package/bundle/web-dist/client/assets/settings-DwbzT5jo.js +0 -1
@@ -1,7 +1,7 @@
1
- import { A as require_jsx_runtime, D as useQuery, T as isAxiosError, Z as __toESM, a as SettingService, j as require_react, k as useQueryClient, l as isSonamuError, n as QgridService, o as TokenService } from "./services.generated-BM4X3Ecj.js";
1
+ import { A as require_react, O as useQueryClient, X as __toESM, a as SettingService, k as require_jsx_runtime, o as TokenService } from "./services.generated-FPwsG9p3.js";
2
2
  import { l as lucideRotateCcw } from "../entry-server.generated.js";
3
- import { t as useUpdateTokenMutation } from "./use-update-token-mutation-DqGMopII.js";
4
- import { t as lucidePlus } from "./plus-CmvbWuDf.js";
3
+ import { t as useUpdateTokenMutation } from "./use-update-token-mutation-xvXbPrCI.js";
4
+ import { t as lucidePlus } from "./plus-Bvdj8Pwx.js";
5
5
  //#region ~icons/lucide/eye.jsx
6
6
  var import_react = /* @__PURE__ */ __toESM(require_react());
7
7
  var import_jsx_runtime = require_jsx_runtime();
@@ -62,7 +62,6 @@ var GROUP_LABELS = {
62
62
  slack: "Slack 알림",
63
63
  slackConnection: "Slack 연결"
64
64
  };
65
- var RESTART_READY_TIMEOUT_MS = 180 * 1e3;
66
65
  function maskValue(value) {
67
66
  if (!value) return "";
68
67
  if (value.length <= 12) return "•".repeat(value.length);
@@ -357,212 +356,6 @@ function TriggerReminderButton() {
357
356
  })]
358
357
  });
359
358
  }
360
- /**
361
- * 재시작은 스스로 종료하고 감독자(pm2)가 다시 띄우는 것에 맡긴다. 되돌릴 수 없고 1~2분간
362
- * 요청이 실패하므로 확인 단계를 둔다 — 무엇이 일어나는지 모달이 먼저 설명한다.
363
- */
364
- function RestartPanel({ supervisor }) {
365
- const [confirming, setConfirming] = (0, import_react.useState)(false);
366
- const [restartRequested, setRestartRequested] = (0, import_react.useState)(false);
367
- const [restartStartedAt, setRestartStartedAt] = (0, import_react.useState)(null);
368
- const [sawUnavailable, setSawUnavailable] = (0, import_react.useState)(false);
369
- const [completing, setCompleting] = (0, import_react.useState)(false);
370
- const [error, setError] = (0, import_react.useState)(null);
371
- const completingRef = (0, import_react.useRef)(false);
372
- const restartAttemptRef = (0, import_react.useRef)(0);
373
- const queryClient = useQueryClient();
374
- const mutation = SettingService.useRestartServerMutation();
375
- const resetMutation = mutation.reset;
376
- const healthQuery = useQuery({
377
- ...QgridService.healthQueryOptions(),
378
- enabled: restartRequested && !completing,
379
- refetchInterval: restartRequested && !completing ? 1e3 : false,
380
- refetchIntervalInBackground: true,
381
- retry: false
382
- });
383
- (0, import_react.useEffect)(() => {
384
- if (!restartRequested || restartStartedAt === null || completing) return;
385
- const remaining = Math.max(0, RESTART_READY_TIMEOUT_MS - (Date.now() - restartStartedAt));
386
- const timeout = window.setTimeout(() => {
387
- restartAttemptRef.current += 1;
388
- resetMutation();
389
- setConfirming(false);
390
- setRestartRequested(false);
391
- setRestartStartedAt(null);
392
- setSawUnavailable(false);
393
- setError("재시작 후 3분 안에 서버 준비를 확인하지 못했습니다. 상태를 확인한 뒤 다시 시도하세요.");
394
- }, remaining);
395
- return () => window.clearTimeout(timeout);
396
- }, [
397
- completing,
398
- resetMutation,
399
- restartRequested,
400
- restartStartedAt
401
- ]);
402
- (0, import_react.useEffect)(() => {
403
- if (!restartRequested || restartStartedAt === null) return;
404
- const currentAttemptError = healthQuery.isError && healthQuery.errorUpdatedAt >= restartStartedAt;
405
- const currentAttemptData = healthQuery.isSuccess && healthQuery.dataUpdatedAt >= restartStartedAt;
406
- if (currentAttemptError || currentAttemptData && !healthQuery.data.ready) {
407
- setSawUnavailable(true);
408
- return;
409
- }
410
- if (!sawUnavailable || !currentAttemptData || !healthQuery.data.ready || completingRef.current) return;
411
- completingRef.current = true;
412
- setCompleting(true);
413
- queryClient.invalidateQueries({ queryKey: SettingService.getSettingListQueryOptions().queryKey }).then(() => {
414
- restartAttemptRef.current += 1;
415
- resetMutation();
416
- setConfirming(false);
417
- setRestartRequested(false);
418
- setRestartStartedAt(null);
419
- setSawUnavailable(false);
420
- }).catch((cause) => {
421
- restartAttemptRef.current += 1;
422
- resetMutation();
423
- setConfirming(false);
424
- setRestartRequested(false);
425
- setRestartStartedAt(null);
426
- setSawUnavailable(false);
427
- setError(`서버는 준비됐지만 설정을 새로고침하지 못했습니다: ${cause.message}`);
428
- }).finally(() => {
429
- completingRef.current = false;
430
- setCompleting(false);
431
- });
432
- }, [
433
- healthQuery.dataUpdatedAt,
434
- healthQuery.data?.ready,
435
- healthQuery.errorUpdatedAt,
436
- healthQuery.isError,
437
- healthQuery.isSuccess,
438
- queryClient,
439
- resetMutation,
440
- restartStartedAt,
441
- restartRequested,
442
- sawUnavailable
443
- ]);
444
- const restart = async () => {
445
- const attempt = restartAttemptRef.current + 1;
446
- restartAttemptRef.current = attempt;
447
- setError(null);
448
- setSawUnavailable(false);
449
- setCompleting(false);
450
- completingRef.current = false;
451
- setRestartRequested(true);
452
- setRestartStartedAt(Date.now());
453
- try {
454
- await mutation.mutateAsync();
455
- } catch (e) {
456
- if (restartAttemptRef.current !== attempt) return;
457
- if (isSonamuError(e) ? e.code >= 400 && e.code < 500 : isAxiosError(e) && e.response !== void 0 && e.response.status >= 400 && e.response.status < 500) {
458
- restartAttemptRef.current += 1;
459
- resetMutation();
460
- setConfirming(false);
461
- setRestartRequested(false);
462
- setRestartStartedAt(null);
463
- setError(e.message || "재시작 요청이 거부되었습니다.");
464
- }
465
- }
466
- };
467
- const closeConfirmation = () => {
468
- if (restartRequested) return;
469
- setConfirming(false);
470
- setError(null);
471
- };
472
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
473
- className: "panel overflow-hidden",
474
- children: [
475
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
476
- className: "panel-header px-4 py-2.5 sm:px-5",
477
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
478
- className: "text-[13px] font-medium text-sand-700",
479
- children: "서버 제어"
480
- })
481
- }),
482
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
483
- className: "px-4 py-3 sm:px-5 flex items-start justify-between gap-4",
484
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
485
- className: "min-w-0",
486
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
487
- className: "text-[13px] text-sand-800",
488
- children: "qgrid 재시작"
489
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
490
- className: "mt-0.5 text-[11px] text-sand-500",
491
- children: supervisor ? "재시작 필요 설정을 반영하고 qgrid·Codex·Claude CLI를 확인합니다" : "로컬 실행 환경에서는 재시작할 수 없습니다"
492
- })]
493
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
494
- type: "button",
495
- disabled: !supervisor || restartRequested || mutation.isPending,
496
- onClick: () => setConfirming(true),
497
- className: "shrink-0 px-2.5 py-1.5 text-[11px] font-medium rounded-lg border border-red-200 text-red-600 hover:bg-red-50 disabled:opacity-40 disabled:hover:bg-transparent transition-colors duration-150",
498
- children: "재시작"
499
- })]
500
- }),
501
- error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
502
- className: "px-4 pb-3 sm:px-5 text-[11px] text-red-500",
503
- children: error
504
- }),
505
- confirming && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
506
- className: "fixed inset-0 z-50 flex items-start justify-center pt-[15vh]",
507
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
508
- className: "absolute inset-0 bg-sand-900/8 backdrop-blur-sm",
509
- onClick: closeConfirmation,
510
- onKeyDown: () => {}
511
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
512
- className: "relative panel shadow-xl w-full max-w-sm mx-4",
513
- children: [
514
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
515
- className: "px-5 py-4 border-b border-sand-100/60",
516
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", {
517
- className: "text-base font-medium text-sand-900",
518
- children: restartRequested ? mutation.isPending ? "재시작 요청 중" : "서버 재연결 중" : "qgrid 재시작"
519
- })
520
- }),
521
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
522
- className: "px-5 py-4 space-y-2",
523
- children: [
524
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
525
- className: "text-[13px] text-sand-700",
526
- children: [
527
- "워커가 다시 뜰 때까지 ",
528
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", {
529
- className: "text-sand-900",
530
- children: "1~2분간 요청이 실패"
531
- }),
532
- "합니다."
533
- ]
534
- }),
535
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
536
- className: "text-[12px] text-sand-500",
537
- children: "서버를 종료하면 모니터링 도구가 다시 띄웁니다. 이때 재시작 필요 설정을 반영하고, qgrid와 Codex CLI·Claude Code의 최신 버전을 확인합니다."
538
- }),
539
- restartRequested && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
540
- className: "text-[12px] font-medium text-sienna-600",
541
- children: mutation.isPending ? "종료 요청을 처리하고 있습니다…" : sawUnavailable ? "새 서버가 준비될 때까지 연결을 확인하고 있습니다…" : "서버 종료를 기다리고 있습니다…"
542
- })
543
- ]
544
- }),
545
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
546
- className: "px-5 py-3 border-t border-sand-100/60 flex justify-end gap-2",
547
- children: [!restartRequested && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
548
- type: "button",
549
- onClick: closeConfirmation,
550
- className: "px-3 py-1.5 text-[12px] rounded-lg text-sand-600 hover:bg-sand-100 transition-colors duration-150",
551
- children: "취소"
552
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
553
- type: "button",
554
- disabled: restartRequested || mutation.isPending,
555
- onClick: () => void restart(),
556
- className: "px-3 py-1.5 text-[12px] font-medium rounded-lg bg-red-500 text-white hover:bg-red-600 disabled:opacity-50 transition-colors duration-150",
557
- children: restartRequested ? mutation.isPending ? "요청 중" : "재연결 중" : "재시작"
558
- })]
559
- })
560
- ]
561
- })]
562
- })
563
- ]
564
- });
565
- }
566
359
  function SettingsPanel() {
567
360
  const settingsQuery = SettingService.useSettingList();
568
361
  const { data, isLoading } = settingsQuery;
@@ -611,53 +404,49 @@ function SettingsPanel() {
611
404
  const groups = [...new Set(settings.map((s) => s.group))];
612
405
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
613
406
  className: "space-y-4",
614
- children: [
615
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
407
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
408
+ className: "panel overflow-hidden",
409
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
410
+ className: "panel-header flex items-center justify-between px-4 py-2.5 sm:px-5",
411
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
412
+ className: "text-[13px] font-medium text-sand-700",
413
+ children: "환경"
414
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
415
+ className: "text-[11px] text-sand-400",
416
+ children: "기동 시점 값"
417
+ })]
418
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dl", {
419
+ className: "px-4 py-3 sm:px-5 grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-1.5",
420
+ children: runtime.map((info) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
421
+ className: "flex items-baseline justify-between gap-3 min-w-0",
422
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
423
+ className: "text-[11px] text-sand-500 shrink-0",
424
+ children: info.label
425
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
426
+ className: "text-[12px] font-mono text-sand-700 truncate",
427
+ children: info.value
428
+ })]
429
+ }, info.label))
430
+ })]
431
+ }), groups.map((group) => {
432
+ const items = settings.filter((s) => s.group === group);
433
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
616
434
  className: "panel overflow-hidden",
617
435
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
618
436
  className: "panel-header flex items-center justify-between px-4 py-2.5 sm:px-5",
619
437
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
620
438
  className: "text-[13px] font-medium text-sand-700",
621
- children: "환경"
622
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
623
- className: "text-[11px] text-sand-400",
624
- children: "기동 시점 값"
625
- })]
626
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dl", {
627
- className: "px-4 py-3 sm:px-5 grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-1.5",
628
- children: runtime.map((info) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
629
- className: "flex items-baseline justify-between gap-3 min-w-0",
630
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
631
- className: "text-[11px] text-sand-500 shrink-0",
632
- children: info.label
633
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
634
- className: "text-[12px] font-mono text-sand-700 truncate",
635
- children: info.value
636
- })]
637
- }, info.label))
439
+ children: GROUP_LABELS[group] ?? group
440
+ }), group === "slack" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriggerReminderButton, {})]
441
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
442
+ className: "divide-y divide-sand-100/80",
443
+ children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SettingRow, {
444
+ item,
445
+ disabled: item.key === "qgrid.tokenWindowKeepaliveEnabled" && !data?.keepaliveRunnerEnabled
446
+ }), item.key === "qgrid.tokenWindowKeepaliveEnabled" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeepaliveTokenSettings, { runnerEnabled: data?.keepaliveRunnerEnabled ?? false })] }, item.key))
638
447
  })]
639
- }),
640
- groups.map((group) => {
641
- const items = settings.filter((s) => s.group === group);
642
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
643
- className: "panel overflow-hidden",
644
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
645
- className: "panel-header flex items-center justify-between px-4 py-2.5 sm:px-5",
646
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
647
- className: "text-[13px] font-medium text-sand-700",
648
- children: GROUP_LABELS[group] ?? group
649
- }), group === "slack" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriggerReminderButton, {})]
650
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
651
- className: "divide-y divide-sand-100/80",
652
- children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SettingRow, {
653
- item,
654
- disabled: item.key === "qgrid.tokenWindowKeepaliveEnabled" && !data?.keepaliveRunnerEnabled
655
- }), item.key === "qgrid.tokenWindowKeepaliveEnabled" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeepaliveTokenSettings, { runnerEnabled: data?.keepaliveRunnerEnabled ?? false })] }, item.key))
656
- })]
657
- }, group);
658
- }),
659
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RestartPanel, { supervisor: data?.supervisor ?? null })
660
- ]
448
+ }, group);
449
+ })]
661
450
  });
662
451
  }
663
452
  //#endregion
@@ -1,5 +1,5 @@
1
- import { A as require_jsx_runtime, D as useQuery, J as __commonJSMin, Z as __toESM, i as RequestLogStepService, j as require_react, l as isSonamuError, r as RequestLogService } from "./services.generated-BM4X3Ecj.js";
2
- import { a as Link } from "./lazyRouteComponent-Bj1cPq6x.js";
1
+ import { A as require_react, E as useQuery, X as __toESM, i as RequestLogStepService, k as require_jsx_runtime, l as isSonamuError, q as __commonJSMin, r as RequestLogService } from "./services.generated-FPwsG9p3.js";
2
+ import { a as Link } from "./lazyRouteComponent-CUR-kWzI.js";
3
3
  import { c as lucideX, d as Markdown, t as Route, u as remarkGfm } from "../entry-server.generated.js";
4
4
  import { n as formatMicroUsd, t as cacheHitRate } from "./cost-CSH_t17I.js";
5
5
  import { t as requestModelDisplay } from "./request-model-CnCeaNLE.js";
@@ -1,8 +1,8 @@
1
- import { A as require_jsx_runtime, Z as __toESM, j as require_react, k as useQueryClient, n as QgridService, o as TokenService } from "./services.generated-BM4X3Ecj.js";
1
+ import { A as require_react, O as useQueryClient, X as __toESM, k as require_jsx_runtime, n as QgridService, o as TokenService } from "./services.generated-FPwsG9p3.js";
2
2
  import { f as lucideKeyRound, o as Q } from "../entry-server.generated.js";
3
- import { t as useUpdateTokenMutation } from "./use-update-token-mutation-DqGMopII.js";
4
- import { n as OAuthCodeEntry, t as useOAuthLoginFlow } from "./use-oauth-login-flow-DYKnG4A2.js";
5
- import { t as lucidePlus } from "./plus-CmvbWuDf.js";
3
+ import { t as useUpdateTokenMutation } from "./use-update-token-mutation-xvXbPrCI.js";
4
+ import { n as OAuthCodeEntry, t as useOAuthLoginFlow } from "./use-oauth-login-flow-DTQKdYBO.js";
5
+ import { t as lucidePlus } from "./plus-Bvdj8Pwx.js";
6
6
  //#region src/components/qgrid/AddTokenModal.tsx
7
7
  var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
8
8
  var import_jsx_runtime = require_jsx_runtime();
@@ -1,4 +1,4 @@
1
- import { A as require_jsx_runtime, Z as __toESM, j as require_react, k as useQueryClient, n as QgridService, o as TokenService } from "./services.generated-BM4X3Ecj.js";
1
+ import { A as require_react, O as useQueryClient, X as __toESM, k as require_jsx_runtime, n as QgridService, o as TokenService } from "./services.generated-FPwsG9p3.js";
2
2
  import { o as Q } from "../entry-server.generated.js";
3
3
  //#region src/components/qgrid/OAuthCodeEntry.tsx
4
4
  var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
@@ -1,4 +1,4 @@
1
- import { E as useMutation, n as QgridService } from "./services.generated-BM4X3Ecj.js";
1
+ import { T as useMutation, n as QgridService } from "./services.generated-FPwsG9p3.js";
2
2
  //#region src/services/token/use-update-token-mutation.ts
3
3
  function useUpdateTokenMutation() {
4
4
  return useMutation({ mutationFn: ({ id, name, quotaThreshold, weight, keepaliveEnabled }) => QgridService.updateToken(id, name, quotaThreshold, weight, keepaliveEnabled) });
@@ -1,6 +1,6 @@
1
- import { A as require_jsx_runtime, B as matchMutation, C as union, F as onlineManager, G as skipToken, H as noop$1, I as notifyManager, J as __commonJSMin, K as focusManager, L as functionalUpdate, M as Mutation, N as infiniteQueryBehavior, O as QueryClientProvider, P as Query, R as hashKey, S as string$2, U as partialMatchKey, V as matchQuery, W as resolveStaleTime, X as __require, Y as __exportAll, Z as __toESM, _ as custom, b as number$1, c as fetch$1, d as SD, f as SUPPORTED_LOCALES, g as boolean$1, h as array, j as require_react, m as _enum, n as QgridService, o as TokenService, p as setLocale, q as Subscribable, s as SonamuQueryMode, u as zArrayable, v as date, w as unknown, x as object, y as int, z as hashQueryKeyByOptions } from "./assets/services.generated-BM4X3Ecj.js";
2
- import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-Bj1cPq6x.js";
3
- import { n as require_shim, t as require_react_dom } from "./assets/react-dom-BjR1Iaz8.js";
1
+ import { A as require_react, B as matchQuery, C as union, D as QueryClientProvider, F as notifyManager, G as focusManager, H as partialMatchKey, I as functionalUpdate, J as __exportAll, K as Subscribable, L as hashKey, M as infiniteQueryBehavior, N as Query, P as onlineManager, R as hashQueryKeyByOptions, S as string$2, U as resolveStaleTime, V as noop$1, W as skipToken, X as __toESM, Y as __require, _ as custom, b as number$1, c as fetch$1, d as SD, f as SUPPORTED_LOCALES, g as boolean$1, h as array, j as Mutation, k as require_jsx_runtime, m as _enum, n as QgridService, o as TokenService, p as setLocale, q as __commonJSMin, s as SonamuQueryMode, u as zArrayable, v as date, w as unknown, x as object, y as int, z as matchMutation } from "./assets/services.generated-FPwsG9p3.js";
2
+ import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-CUR-kWzI.js";
3
+ import { n as require_shim, t as require_react_dom } from "./assets/react-dom-CyuShs9K.js";
4
4
  import minpath from "node:path";
5
5
  import minproc from "node:process";
6
6
  import { fileURLToPath as urlToPath } from "node:url";
@@ -46874,7 +46874,7 @@ function ChatWidget() {
46874
46874
  }
46875
46875
  //#endregion
46876
46876
  //#region src/components/Sidebar.tsx
46877
- var qgridVersion = "2.10.3";
46877
+ var qgridVersion = "2.10.4";
46878
46878
  function Sidebar({ className }) {
46879
46879
  const pathname = useRouterState({ select: (s) => s.location.pathname });
46880
46880
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
@@ -68752,15 +68752,15 @@ function RootComponent() {
68752
68752
  }
68753
68753
  //#endregion
68754
68754
  //#region src/routes/tokens.tsx
68755
- var $$splitComponentImporter$5 = () => import("./assets/tokens-DiTUwUwr.js");
68755
+ var $$splitComponentImporter$5 = () => import("./assets/tokens-odgS2-9q.js");
68756
68756
  var Route$5 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$5, "component") });
68757
68757
  //#endregion
68758
68758
  //#region src/routes/settings.tsx
68759
- var $$splitComponentImporter$4 = () => import("./assets/settings-BFVuldch.js");
68759
+ var $$splitComponentImporter$4 = () => import("./assets/settings-DeHNwoMv.js");
68760
68760
  var Route$4 = createFileRoute("/settings")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
68761
68761
  //#endregion
68762
68762
  //#region src/routes/monit.tsx
68763
- var $$splitComponentImporter$3 = () => import("./assets/monit-D9W4ioUi.js");
68763
+ var $$splitComponentImporter$3 = () => import("./assets/monit-DSxs4Jnl.js");
68764
68764
  var monitSearchSchema = object({ level: _enum([
68765
68765
  "all",
68766
68766
  "warn",
@@ -69137,7 +69137,7 @@ object({
69137
69137
  _enum(["A"]);
69138
69138
  //#endregion
69139
69139
  //#region src/routes/logs.tsx
69140
- var $$splitComponentImporter$2 = () => import("./assets/logs-BSk9lOHe.js");
69140
+ var $$splitComponentImporter$2 = () => import("./assets/logs-CagNGTBj.js");
69141
69141
  var logsSearchSchema = object({
69142
69142
  token: string$2().optional(),
69143
69143
  project: string$2().optional(),
@@ -69151,11 +69151,11 @@ var Route$2 = createFileRoute("/logs")({
69151
69151
  });
69152
69152
  //#endregion
69153
69153
  //#region src/routes/index.tsx
69154
- var $$splitComponentImporter$1 = () => import("./assets/routes-C7lB2Q85.js");
69154
+ var $$splitComponentImporter$1 = () => import("./assets/routes-i-sTqXMX.js");
69155
69155
  var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
69156
69156
  //#endregion
69157
69157
  //#region src/routes/requests/show.tsx
69158
- var $$splitComponentImporter = () => import("./assets/show-Cswh_tqs.js");
69158
+ var $$splitComponentImporter = () => import("./assets/show-C4jk1Hrp.js");
69159
69159
  var showSearchSchema = object({ id: number$1() });
69160
69160
  var Route = createFileRoute("/requests/show")({
69161
69161
  validateSearch: showSearchSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-cli",
3
- "version": "2.10.3",
3
+ "version": "2.10.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cartanova-ai/qgrid"
@@ -92,17 +92,20 @@ Some env values are editable at runtime through the dashboard's Settings page, b
92
92
  - Editable: Slack delivery control (master switch, reminder on/off, reminder interval, quiet-hours window, weekend behaviour), and the Slack channel/user-map/bot-token. `SETTING_DEFS` in `setting.constant.ts` is the single definition — key, env name, type, bounds, and whether the change applies immediately or needs a restart. The `settings` table itself (`setting.entity.json`) only stores `(key, value)` strings; everything the UI needs to render and validate a key lives in that constant, not in the schema.
93
93
  - Not editable: anything needed before the server can read its own database (`QGRID_DB_*`, `HOST`, `PORT`, `NODE_ENV`). These are exposed read-only on the same page so an operator can confirm which environment they are looking at; the DB password is masked.
94
94
 
95
- ### Restart from the dashboard
95
+ ### Process lifecycle
96
96
 
97
- The Settings page can restart the server. There is no `pm2 restart` call — the process exits and the supervisor respawns it, which is what "restart" means here.
97
+ qgrid does not detect a process supervisor or expose a restart API or dashboard
98
+ control. Restarting, replacing, and recovering the process belong to the
99
+ operator's deployment tooling. Settings marked `restart` still take effect on
100
+ the next process start; the label does not imply an in-app restart action.
101
+
102
+ There is no SIGTERM drain handler. Replacing the process during traffic can
103
+ interrupt active responses, so deployment tooling must account for startup
104
+ readiness and interruption. Ordinary request success/error/abort accounting and
105
+ stale tool-wait cleanup remain independent of the deployment mechanism.
106
+
107
+ ### Live notification settings
98
108
 
99
- - `detectSupervisor` (`utils/process-supervisor.ts`) accepts only `pm_id` (pm2). `INVOCATION_ID` proves systemd launched a process but does not prove the unit has a restart policy, so it is not sufficient. Without pm2, `restartServer` returns 400 and the button is disabled: exiting with nothing to relaunch the process is a shutdown, not a restart. Presence is the signal, not the value — pm2 sets `pm_id=0` for the first process.
100
- - Browser requests must be same-host: when `Origin` is present and its host differs from `X-Forwarded-Host`/`Host`, `restartServer` returns 403. Origin-less operational clients such as `curl` remain supported. This is CSRF protection, not a substitute for authenticating the management API at Caddy.
101
- - The first restart request latches `restartPending`, blocks new native query/stream dispatch with 503, and marks only process-local active native request-log IDs as `error: server restarted`. Tool-result-waiting runs and externally owned logger runs are untouched. This is state reconciliation, not request draining.
102
- - Exit is process-wide and one-shot. It runs after the restart response emits `finish`; a response `close` or a five-second fallback covers disconnects. Concurrent restart calls do not schedule multiple exits.
103
- - The supervisor relaunches from `ecosystem.config.cjs`, not from the dying process's argv. That file carries no `--skip-update`, so each restart also runs the CLI self-update — a restart is a deploy. The confirmation dialog says so; dropping the update means adding `--skip-update` there.
104
- - The relaunch reuses the environment pm2 captured when the app was first started. Neither the dashboard restart nor `pm2 restart qgrid` re-reads `ecosystem.config.cjs`, so an env edit in that file (for example `QGRID_TOKEN_WINDOW_KEEPALIVE_ENABLED`) stays invisible until `pm2 restart ecosystem.config.cjs --update-env` (or `pm2 delete` + `pm2 start`) followed by `pm2 save`. Verify with `GET /api/setting/listSettings`, which reports the env the process actually sees. dev0 ran for a week with the keepalive env in the file but not in the process because of this (2026-09-02).
105
- - In-flight provider responses are not drained. There is no SIGTERM handler, so a restart during traffic cuts active responses, and the OpenAI pool needs 1–2 minutes before requests succeed again (`QgridDispatcher.startupState` answers 503 in that window). The dashboard stays locked after confirmation, observes an unavailable/not-ready health state, then waits for a later `ready: true` before refreshing settings.
106
109
  - Slack channel, bot-token, and user-map changes are read on the next notification. Changing the expiry-reminder interval or toggling the reminder replaces the current timer immediately without sending a notification merely because the setting changed; process boot still performs the intended immediate reminder run.
107
110
  - `slack.enabled` is the holiday switch: an operator turns it off for periods no rule can express. It suppresses ordinary notifications but not `urgent` ones, so a provider losing its last token still reports during a long break. `slack.remindersEnabled` is narrower — it stops only the repeat timer and preserves the chosen interval so re-enabling restores it.
108
111
  - Quiet hours are `slack.quietFromHour`/`slack.quietUntilHour` (0–23, Asia/Seoul) plus `slack.notifyOnWeekends`. From > until reads as an overnight window (20→8); from < until reads as a same-day window, which night-shift teams may want. Equal values mean no quiet window at all. Out-of-range values fall back to 20/8 at runtime, since env can bypass the stored-value validation.
@@ -1,22 +0,0 @@
1
- //#region src/utils/process-supervisor.ts
2
- /**
3
- * 프로세스 모니터링 도구 감지.
4
- *
5
- * 화면의 "재시작"은 스스로를 종료하는 것이다 — pm2 같은 도구가 죽은 프로세스를 다시
6
- * 띄워주는 것이 곧 재시작이다. 그래서 그런 도구 없이 종료하면 그냥 서버가 내려간다.
7
- * 로컬 개발에서 버튼을 누르면 아무도 살려주지 않으므로, 종료 전에 반드시 확인한다.
8
- *
9
- * 현재 배포 계약은 pm2 만 재시작을 보장한다. systemd 의 `INVOCATION_ID` 는 해당
10
- * unit 이 `Restart=` 를 쓴다는 뜻이 아니므로 안전한 감독자 신호로 사용할 수 없다.
11
- *
12
- * 값이 아니라 존재 여부가 신호다. pm2 는 첫 프로세스에 `pm_id=0` 을 주므로 truthy 검사로
13
- * 판별하면 놓친다.
14
- */
15
- function detectSupervisor(env = process.env) {
16
- if (env.pm_id !== void 0) return "pm2";
17
- return null;
18
- }
19
- //#endregion
20
- export { detectSupervisor };
21
-
22
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJvY2Vzcy1zdXBlcnZpc29yLmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9wcm9jZXNzLXN1cGVydmlzb3IudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiDtlITroZzshLjsiqQg66qo64uI7YSw66eBIOuPhOq1rCDqsJDsp4AuXG4gKlxuICog7ZmU66m07J2YIFwi7J6s7Iuc7J6RXCLsnYAg7Iqk7Iqk66Gc66W8IOyiheujjO2VmOuKlCDqsoPsnbTri6Qg4oCUIHBtMiDqsJnsnYAg64+E6rWs6rCAIOyjveydgCDtlITroZzshLjsiqTrpbwg64uk7IucXG4gKiDrnYTsm4zso7zripQg6rKD7J20IOqzpyDsnqzsi5zsnpHsnbTri6QuIOq3uOuemOyEnCDqt7jrn7Ag64+E6rWsIOyXhuydtCDsooXro4ztlZjrqbQg6re464OlIOyEnOuyhOqwgCDrgrTroKTqsITri6QuXG4gKiDroZzsu6wg6rCc67Cc7JeQ7IScIOuyhO2KvOydhCDriITrpbTrqbQg7JWE66y064+EIOyCtOugpOyjvOyngCDslYrsnLzrr4DroZwsIOyiheujjCDsoITsl5Ag67CY65Oc7IucIO2ZleyduO2VnOuLpC5cbiAqXG4gKiDtmITsnqwg67Cw7Y+sIOqzhOyVveydgCBwbTIg66eMIOyerOyLnOyekeydhCDrs7TsnqXtlZzri6QuIHN5c3RlbWQg7J2YIGBJTlZPQ0FUSU9OX0lEYCDripQg7ZW064u5XG4gKiB1bml0IOydtCBgUmVzdGFydD1gIOulvCDsk7Tri6TripQg65y77J20IOyVhOuLiOuvgOuhnCDslYjsoITtlZwg6rCQ64+F7J6QIOyLoO2YuOuhnCDsgqzsmqntlaAg7IiYIOyXhuuLpC5cbiAqXG4gKiDqsJLsnbQg7JWE64uI6528IOyhtOyerCDsl6zrtoDqsIAg7Iug7Zi464ukLiBwbTIg64qUIOyyqyDtlITroZzshLjsiqTsl5AgYHBtX2lkPTBgIOydhCDso7zrr4DroZwgdHJ1dGh5IOqygOyCrOuhnFxuICog7YyQ67OE7ZWY66m0IOuGk+y5nOuLpC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGRldGVjdFN1cGVydmlzb3IoZW52OiBOb2RlSlMuUHJvY2Vzc0VudiA9IHByb2Nlc3MuZW52KTogXCJwbTJcIiB8IG51bGwge1xuICBpZiAoZW52LnBtX2lkICE9PSB1bmRlZmluZWQpIHJldHVybiBcInBtMlwiO1xuICByZXR1cm4gbnVsbDtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7QUFhQSxTQUFnQixpQkFBaUIsTUFBeUIsUUFBUSxLQUFtQjtDQUNuRixJQUFJLElBQUksVUFBVSxRQUFXLE9BQU87Q0FDcEMsT0FBTztBQUNUIn0=
@@ -1,61 +0,0 @@
1
- //#region src/utils/server-restart.ts
2
- const RESTART_EXIT_FALLBACK_MS = 5e3;
3
- let restartPending = false;
4
- let exitTriggered = false;
5
- let exitArmed = false;
6
- let fallbackTimer;
7
- let armedReplyRaw;
8
- let armedExit;
9
- /** 재시작 요청 뒤에는 새 native provider 실행을 받지 않는다. */
10
- function isRestartPending() {
11
- return restartPending;
12
- }
13
- /**
14
- * 프로세스 전체에서 최초 요청만 재시작을 시작한다. 종료 신호는 active run 정합성 처리가
15
- * 끝난 뒤 별도로 arm 한다. 그래야 이미 닫힌 연결이나 fallback 타이머가 DB 정리보다 앞서
16
- * 프로세스를 끝내지 않는다.
17
- */
18
- function beginServerRestart() {
19
- if (restartPending) return false;
20
- restartPending = true;
21
- return true;
22
- }
23
- function clearExitArm() {
24
- if (fallbackTimer) clearTimeout(fallbackTimer);
25
- fallbackTimer = void 0;
26
- if (armedReplyRaw && armedExit) {
27
- armedReplyRaw.removeListener("finish", armedExit);
28
- armedReplyRaw.removeListener("close", armedExit);
29
- }
30
- armedReplyRaw = void 0;
31
- armedExit = void 0;
32
- }
33
- /** 정합성 처리가 끝난 뒤 HTTP 응답 flush/연결 종료/유한 fallback 중 먼저 온 신호로 종료한다. */
34
- function armServerRestartExit(replyRaw) {
35
- if (!restartPending || exitArmed) return;
36
- exitArmed = true;
37
- const exitOnce = () => {
38
- if (exitTriggered) return;
39
- exitTriggered = true;
40
- clearExitArm();
41
- process.exit(0);
42
- };
43
- armedReplyRaw = replyRaw;
44
- armedExit = exitOnce;
45
- replyRaw.once("finish", exitOnce);
46
- replyRaw.once("close", exitOnce);
47
- fallbackTimer = setTimeout(exitOnce, RESTART_EXIT_FALLBACK_MS);
48
- fallbackTimer.unref();
49
- if (replyRaw.destroyed && !replyRaw.writableEnded) exitOnce();
50
- }
51
- /** 모듈 전역 상태를 격리해야 하는 단위 테스트 전용. */
52
- function resetServerRestartForTests() {
53
- clearExitArm();
54
- restartPending = false;
55
- exitTriggered = false;
56
- exitArmed = false;
57
- }
58
- //#endregion
59
- export { armServerRestartExit, beginServerRestart, isRestartPending, resetServerRestartForTests };
60
-
61
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLXJlc3RhcnQuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3NlcnZlci1yZXN0YXJ0LnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHR5cGUgU2VydmVyUmVzcG9uc2UgfSBmcm9tIFwibm9kZTpodHRwXCI7XG5cbmNvbnN0IFJFU1RBUlRfRVhJVF9GQUxMQkFDS19NUyA9IDVfMDAwO1xuXG5sZXQgcmVzdGFydFBlbmRpbmcgPSBmYWxzZTtcbmxldCBleGl0VHJpZ2dlcmVkID0gZmFsc2U7XG5sZXQgZXhpdEFybWVkID0gZmFsc2U7XG5sZXQgZmFsbGJhY2tUaW1lcjogTm9kZUpTLlRpbWVvdXQgfCB1bmRlZmluZWQ7XG5sZXQgYXJtZWRSZXBseVJhdzogUmVwbHlSYXcgfCB1bmRlZmluZWQ7XG5sZXQgYXJtZWRFeGl0OiAoKCkgPT4gdm9pZCkgfCB1bmRlZmluZWQ7XG5cbnR5cGUgUmVwbHlSYXcgPSBQaWNrPFNlcnZlclJlc3BvbnNlLCBcIm9uY2VcIiB8IFwicmVtb3ZlTGlzdGVuZXJcIiB8IFwiZGVzdHJveWVkXCIgfCBcIndyaXRhYmxlRW5kZWRcIj47XG5cbi8qKiDsnqzsi5zsnpEg7JqU7LKtIOuSpOyXkOuKlCDsg4ggbmF0aXZlIHByb3ZpZGVyIOyLpO2WieydhCDrsJvsp4Ag7JWK64qU64ukLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGlzUmVzdGFydFBlbmRpbmcoKTogYm9vbGVhbiB7XG4gIHJldHVybiByZXN0YXJ0UGVuZGluZztcbn1cblxuLyoqXG4gKiDtlITroZzshLjsiqQg7KCE7LK07JeQ7IScIOy1nOy0iCDsmpTssq3rp4wg7J6s7Iuc7J6R7J2EIOyLnOyeke2VnOuLpC4g7KKF66OMIOyLoO2YuOuKlCBhY3RpdmUgcnVuIOygle2VqeyEsSDsspjrpqzqsIBcbiAqIOuBneuCnCDrkqQg67OE64+E66GcIGFybSDtlZzri6QuIOq3uOuemOyVvCDsnbTrr7gg64ur7Z6MIOyXsOqysOydtOuCmCBmYWxsYmFjayDtg4DsnbTrqLjqsIAgREIg7KCV66as67O064ukIOyVnuyEnFxuICog7ZSE66Gc7IS47Iqk66W8IOuBneuCtOyngCDslYrripTri6QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBiZWdpblNlcnZlclJlc3RhcnQoKTogYm9vbGVhbiB7XG4gIGlmIChyZXN0YXJ0UGVuZGluZykgcmV0dXJuIGZhbHNlO1xuICByZXN0YXJ0UGVuZGluZyA9IHRydWU7XG4gIHJldHVybiB0cnVlO1xufVxuXG5mdW5jdGlvbiBjbGVhckV4aXRBcm0oKTogdm9pZCB7XG4gIGlmIChmYWxsYmFja1RpbWVyKSBjbGVhclRpbWVvdXQoZmFsbGJhY2tUaW1lcik7XG4gIGZhbGxiYWNrVGltZXIgPSB1bmRlZmluZWQ7XG4gIGlmIChhcm1lZFJlcGx5UmF3ICYmIGFybWVkRXhpdCkge1xuICAgIGFybWVkUmVwbHlSYXcucmVtb3ZlTGlzdGVuZXIoXCJmaW5pc2hcIiwgYXJtZWRFeGl0KTtcbiAgICBhcm1lZFJlcGx5UmF3LnJlbW92ZUxpc3RlbmVyKFwiY2xvc2VcIiwgYXJtZWRFeGl0KTtcbiAgfVxuICBhcm1lZFJlcGx5UmF3ID0gdW5kZWZpbmVkO1xuICBhcm1lZEV4aXQgPSB1bmRlZmluZWQ7XG59XG5cbi8qKiDsoJXtlanshLEg7LKY66as6rCAIOuBneuCnCDrkqQgSFRUUCDsnZHri7UgZmx1c2gv7Jew6rKwIOyiheujjC/snKDtlZwgZmFsbGJhY2sg7KSRIOuovOyggCDsmKgg7Iug7Zi466GcIOyiheujjO2VnOuLpC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBhcm1TZXJ2ZXJSZXN0YXJ0RXhpdChyZXBseVJhdzogUmVwbHlSYXcpOiB2b2lkIHtcbiAgaWYgKCFyZXN0YXJ0UGVuZGluZyB8fCBleGl0QXJtZWQpIHJldHVybjtcbiAgZXhpdEFybWVkID0gdHJ1ZTtcblxuICBjb25zdCBleGl0T25jZSA9ICgpID0+IHtcbiAgICBpZiAoZXhpdFRyaWdnZXJlZCkgcmV0dXJuO1xuICAgIGV4aXRUcmlnZ2VyZWQgPSB0cnVlO1xuICAgIGNsZWFyRXhpdEFybSgpO1xuICAgIHByb2Nlc3MuZXhpdCgwKTtcbiAgfTtcblxuICBhcm1lZFJlcGx5UmF3ID0gcmVwbHlSYXc7XG4gIGFybWVkRXhpdCA9IGV4aXRPbmNlO1xuICByZXBseVJhdy5vbmNlKFwiZmluaXNoXCIsIGV4aXRPbmNlKTtcbiAgcmVwbHlSYXcub25jZShcImNsb3NlXCIsIGV4aXRPbmNlKTtcbiAgZmFsbGJhY2tUaW1lciA9IHNldFRpbWVvdXQoZXhpdE9uY2UsIFJFU1RBUlRfRVhJVF9GQUxMQkFDS19NUyk7XG4gIGZhbGxiYWNrVGltZXIudW5yZWYoKTtcbiAgaWYgKHJlcGx5UmF3LmRlc3Ryb3llZCAmJiAhcmVwbHlSYXcud3JpdGFibGVFbmRlZCkgZXhpdE9uY2UoKTtcbn1cblxuLyoqIOuqqOuTiCDsoITsl60g7IOB7YOc66W8IOqyqeumrO2VtOyVvCDtlZjripQg64uo7JyEIO2FjOyKpO2KuCDsoITsmqkuICovXG5leHBvcnQgZnVuY3Rpb24gcmVzZXRTZXJ2ZXJSZXN0YXJ0Rm9yVGVzdHMoKTogdm9pZCB7XG4gIGNsZWFyRXhpdEFybSgpO1xuICByZXN0YXJ0UGVuZGluZyA9IGZhbHNlO1xuICBleGl0VHJpZ2dlcmVkID0gZmFsc2U7XG4gIGV4aXRBcm1lZCA9IGZhbHNlO1xufVxuIl0sIm1hcHBpbmdzIjoiO0FBRUEsTUFBTSwyQkFBMkI7QUFFakMsSUFBSSxpQkFBaUI7QUFDckIsSUFBSSxnQkFBZ0I7QUFDcEIsSUFBSSxZQUFZO0FBQ2hCLElBQUk7QUFDSixJQUFJO0FBQ0osSUFBSTs7QUFLSixTQUFnQixtQkFBNEI7Q0FDMUMsT0FBTztBQUNUOzs7Ozs7QUFPQSxTQUFnQixxQkFBOEI7Q0FDNUMsSUFBSSxnQkFBZ0IsT0FBTztDQUMzQixpQkFBaUI7Q0FDakIsT0FBTztBQUNUO0FBRUEsU0FBUyxlQUFxQjtDQUM1QixJQUFJLGVBQWUsYUFBYSxhQUFhO0NBQzdDLGdCQUFnQjtDQUNoQixJQUFJLGlCQUFpQixXQUFXO0VBQzlCLGNBQWMsZUFBZSxVQUFVLFNBQVM7RUFDaEQsY0FBYyxlQUFlLFNBQVMsU0FBUztDQUNqRDtDQUNBLGdCQUFnQjtDQUNoQixZQUFZO0FBQ2Q7O0FBR0EsU0FBZ0IscUJBQXFCLFVBQTBCO0NBQzdELElBQUksQ0FBQyxrQkFBa0IsV0FBVztDQUNsQyxZQUFZO0NBRVosTUFBTSxpQkFBaUI7RUFDckIsSUFBSSxlQUFlO0VBQ25CLGdCQUFnQjtFQUNoQixhQUFhO0VBQ2IsUUFBUSxLQUFLLENBQUM7Q0FDaEI7Q0FFQSxnQkFBZ0I7Q0FDaEIsWUFBWTtDQUNaLFNBQVMsS0FBSyxVQUFVLFFBQVE7Q0FDaEMsU0FBUyxLQUFLLFNBQVMsUUFBUTtDQUMvQixnQkFBZ0IsV0FBVyxVQUFVLHdCQUF3QjtDQUM3RCxjQUFjLE1BQU07Q0FDcEIsSUFBSSxTQUFTLGFBQWEsQ0FBQyxTQUFTLGVBQWUsU0FBUztBQUM5RDs7QUFHQSxTQUFnQiw2QkFBbUM7Q0FDakQsYUFBYTtDQUNiLGlCQUFpQjtDQUNqQixnQkFBZ0I7Q0FDaEIsWUFBWTtBQUNkIn0=
@@ -1,24 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { detectSupervisor } from "./process-supervisor";
4
-
5
- describe("detectSupervisor", () => {
6
- it("pm2 아래에서는 pm2 로 판별한다", () => {
7
- // pm2 는 자식에게 pm_id 를 넣는다. "0" 이 흔한 값이라 falsy 검사로는 놓친다.
8
- expect(detectSupervisor({ pm_id: "0" })).toBe("pm2");
9
- });
10
-
11
- it("systemd 환경변수만으로는 재시작 보장을 가정하지 않는다", () => {
12
- expect(detectSupervisor({ INVOCATION_ID: "abc123" })).toBeNull();
13
- });
14
-
15
- it("로컬 실행에서는 null 이다", () => {
16
- // 종료해도 다시 띄워줄 주체가 없어 재시작을 막아야 한다.
17
- expect(detectSupervisor({})).toBeNull();
18
- });
19
-
20
- it("빈 문자열도 pm2 로 인정한다", () => {
21
- // 값이 아니라 존재 여부가 신호다. pm_id 가 빈 문자열로 오는 환경이 있어도 pm2 다.
22
- expect(detectSupervisor({ pm_id: "" })).toBe("pm2");
23
- });
24
- });
@@ -1,17 +0,0 @@
1
- /**
2
- * 프로세스 모니터링 도구 감지.
3
- *
4
- * 화면의 "재시작"은 스스로를 종료하는 것이다 — pm2 같은 도구가 죽은 프로세스를 다시
5
- * 띄워주는 것이 곧 재시작이다. 그래서 그런 도구 없이 종료하면 그냥 서버가 내려간다.
6
- * 로컬 개발에서 버튼을 누르면 아무도 살려주지 않으므로, 종료 전에 반드시 확인한다.
7
- *
8
- * 현재 배포 계약은 pm2 만 재시작을 보장한다. systemd 의 `INVOCATION_ID` 는 해당
9
- * unit 이 `Restart=` 를 쓴다는 뜻이 아니므로 안전한 감독자 신호로 사용할 수 없다.
10
- *
11
- * 값이 아니라 존재 여부가 신호다. pm2 는 첫 프로세스에 `pm_id=0` 을 주므로 truthy 검사로
12
- * 판별하면 놓친다.
13
- */
14
- export function detectSupervisor(env: NodeJS.ProcessEnv = process.env): "pm2" | null {
15
- if (env.pm_id !== undefined) return "pm2";
16
- return null;
17
- }