@tienne/gestalt 0.68.0 → 0.70.0

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 (143) hide show
  1. package/CLAUDE.md +10 -2
  2. package/README.md +1 -0
  3. package/dist/package.json +2 -1
  4. package/dist/plugin/review-agents/quality-reviewer/AGENT.md +1 -1
  5. package/dist/plugin/review-agents/writing-reviewer/AGENT.md +2 -2
  6. package/dist/plugin/role-agents/humanize-monolith/AGENT.md +89 -11
  7. package/dist/plugin/skills/local-pr/SKILL.md +201 -0
  8. package/dist/plugin/skills/pr/SKILL.md +73 -4
  9. package/dist/plugin/skills/review/SKILL.md +190 -18
  10. package/dist/plugin/skills/review-reply/SKILL.md +144 -7
  11. package/dist/src/cli/commands/humanize-check.d.ts +2 -0
  12. package/dist/src/cli/commands/humanize-check.d.ts.map +1 -1
  13. package/dist/src/cli/commands/humanize-check.js +11 -5
  14. package/dist/src/cli/commands/humanize-check.js.map +1 -1
  15. package/dist/src/cli/commands/humanize-scan.d.ts +23 -0
  16. package/dist/src/cli/commands/humanize-scan.d.ts.map +1 -0
  17. package/dist/src/cli/commands/humanize-scan.js +30 -0
  18. package/dist/src/cli/commands/humanize-scan.js.map +1 -0
  19. package/dist/src/cli/commands/pr.d.ts +133 -0
  20. package/dist/src/cli/commands/pr.d.ts.map +1 -0
  21. package/dist/src/cli/commands/pr.js +489 -0
  22. package/dist/src/cli/commands/pr.js.map +1 -0
  23. package/dist/src/cli/index.d.ts.map +1 -1
  24. package/dist/src/cli/index.js +96 -0
  25. package/dist/src/cli/index.js.map +1 -1
  26. package/dist/src/core/config.d.ts.map +1 -1
  27. package/dist/src/core/config.js +4 -3
  28. package/dist/src/core/config.js.map +1 -1
  29. package/dist/src/core/home.d.ts +20 -0
  30. package/dist/src/core/home.d.ts.map +1 -0
  31. package/dist/src/core/home.js +52 -0
  32. package/dist/src/core/home.js.map +1 -0
  33. package/dist/src/core/types.d.ts +38 -0
  34. package/dist/src/core/types.d.ts.map +1 -1
  35. package/dist/src/core/version.d.ts.map +1 -1
  36. package/dist/src/core/version.js +4 -7
  37. package/dist/src/core/version.js.map +1 -1
  38. package/dist/src/events/store.d.ts +32 -0
  39. package/dist/src/events/store.d.ts.map +1 -1
  40. package/dist/src/events/store.js +68 -3
  41. package/dist/src/events/store.js.map +1 -1
  42. package/dist/src/humanize/check.d.ts +75 -7
  43. package/dist/src/humanize/check.d.ts.map +1 -1
  44. package/dist/src/humanize/check.js +241 -30
  45. package/dist/src/humanize/check.js.map +1 -1
  46. package/dist/src/humanize/detectors.d.ts.map +1 -1
  47. package/dist/src/humanize/detectors.js +7 -8
  48. package/dist/src/humanize/detectors.js.map +1 -1
  49. package/dist/src/humanize/index.d.ts +3 -2
  50. package/dist/src/humanize/index.d.ts.map +1 -1
  51. package/dist/src/humanize/index.js +3 -2
  52. package/dist/src/humanize/index.js.map +1 -1
  53. package/dist/src/humanize/rules.d.ts +12 -0
  54. package/dist/src/humanize/rules.d.ts.map +1 -1
  55. package/dist/src/humanize/rules.js +4 -0
  56. package/dist/src/humanize/rules.js.map +1 -1
  57. package/dist/src/humanize/scan.d.ts +24 -0
  58. package/dist/src/humanize/scan.d.ts.map +1 -0
  59. package/dist/src/humanize/scan.js +65 -0
  60. package/dist/src/humanize/scan.js.map +1 -0
  61. package/dist/src/local-pr/engine.d.ts +176 -0
  62. package/dist/src/local-pr/engine.d.ts.map +1 -0
  63. package/dist/src/local-pr/engine.js +373 -0
  64. package/dist/src/local-pr/engine.js.map +1 -0
  65. package/dist/src/local-pr/git.d.ts +190 -0
  66. package/dist/src/local-pr/git.d.ts.map +1 -0
  67. package/dist/src/local-pr/git.js +580 -0
  68. package/dist/src/local-pr/git.js.map +1 -0
  69. package/dist/src/local-pr/index.d.ts +6 -0
  70. package/dist/src/local-pr/index.d.ts.map +1 -0
  71. package/dist/src/local-pr/index.js +8 -0
  72. package/dist/src/local-pr/index.js.map +1 -0
  73. package/dist/src/local-pr/policy.d.ts +77 -0
  74. package/dist/src/local-pr/policy.d.ts.map +1 -0
  75. package/dist/src/local-pr/policy.js +80 -0
  76. package/dist/src/local-pr/policy.js.map +1 -0
  77. package/dist/src/local-pr/registry.d.ts +79 -0
  78. package/dist/src/local-pr/registry.d.ts.map +1 -0
  79. package/dist/src/local-pr/registry.js +307 -0
  80. package/dist/src/local-pr/registry.js.map +1 -0
  81. package/dist/src/local-pr/repository.d.ts +67 -0
  82. package/dist/src/local-pr/repository.d.ts.map +1 -0
  83. package/dist/src/local-pr/repository.js +251 -0
  84. package/dist/src/local-pr/repository.js.map +1 -0
  85. package/dist/src/local-pr/types.d.ts +151 -0
  86. package/dist/src/local-pr/types.d.ts.map +1 -0
  87. package/dist/src/local-pr/types.js +9 -0
  88. package/dist/src/local-pr/types.js.map +1 -0
  89. package/dist/src/local-pr-web/engine.d.ts +24 -0
  90. package/dist/src/local-pr-web/engine.d.ts.map +1 -0
  91. package/dist/src/local-pr-web/engine.js +112 -0
  92. package/dist/src/local-pr-web/engine.js.map +1 -0
  93. package/dist/src/local-pr-web/html-generator.d.ts +24 -0
  94. package/dist/src/local-pr-web/html-generator.d.ts.map +1 -0
  95. package/dist/src/local-pr-web/html-generator.js +385 -0
  96. package/dist/src/local-pr-web/html-generator.js.map +1 -0
  97. package/dist/src/local-pr-web/index.d.ts +5 -0
  98. package/dist/src/local-pr-web/index.d.ts.map +1 -0
  99. package/dist/src/local-pr-web/index.js +5 -0
  100. package/dist/src/local-pr-web/index.js.map +1 -0
  101. package/dist/src/local-pr-web/server.d.ts +126 -0
  102. package/dist/src/local-pr-web/server.d.ts.map +1 -0
  103. package/dist/src/local-pr-web/server.js +394 -0
  104. package/dist/src/local-pr-web/server.js.map +1 -0
  105. package/dist/src/local-pr-web/types.d.ts +11 -0
  106. package/dist/src/local-pr-web/types.d.ts.map +1 -0
  107. package/dist/src/local-pr-web/types.js +2 -0
  108. package/dist/src/local-pr-web/types.js.map +1 -0
  109. package/dist/src/mcp/schemas.d.ts +79 -4
  110. package/dist/src/mcp/schemas.d.ts.map +1 -1
  111. package/dist/src/mcp/schemas.js +80 -2
  112. package/dist/src/mcp/schemas.js.map +1 -1
  113. package/dist/src/mcp/server.d.ts.map +1 -1
  114. package/dist/src/mcp/server.js +13 -2
  115. package/dist/src/mcp/server.js.map +1 -1
  116. package/dist/src/mcp/tools/pr.d.ts +5 -0
  117. package/dist/src/mcp/tools/pr.d.ts.map +1 -0
  118. package/dist/src/mcp/tools/pr.js +165 -0
  119. package/dist/src/mcp/tools/pr.js.map +1 -0
  120. package/dist/src/mcp/tools/review-passthrough.d.ts.map +1 -1
  121. package/dist/src/mcp/tools/review-passthrough.js +264 -15
  122. package/dist/src/mcp/tools/review-passthrough.js.map +1 -1
  123. package/dist/src/memory/user-profile-store.d.ts.map +1 -1
  124. package/dist/src/memory/user-profile-store.js +3 -4
  125. package/dist/src/memory/user-profile-store.js.map +1 -1
  126. package/dist/src/review/context-collector.d.ts +1 -1
  127. package/dist/src/review/context-collector.js +1 -1
  128. package/dist/src/review/passthrough-engine.d.ts +1 -0
  129. package/dist/src/review/passthrough-engine.d.ts.map +1 -1
  130. package/dist/src/review/passthrough-engine.js +7 -2
  131. package/dist/src/review/passthrough-engine.js.map +1 -1
  132. package/dist/src/spec/text-based-spec-generator.d.ts.map +1 -1
  133. package/dist/src/spec/text-based-spec-generator.js +5 -2
  134. package/dist/src/spec/text-based-spec-generator.js.map +1 -1
  135. package/package.json +2 -1
  136. package/plugin/.codex-plugin/plugin.json +1 -1
  137. package/plugin/review-agents/quality-reviewer/AGENT.md +1 -1
  138. package/plugin/review-agents/writing-reviewer/AGENT.md +2 -2
  139. package/plugin/role-agents/humanize-monolith/AGENT.md +89 -11
  140. package/plugin/skills/local-pr/SKILL.md +201 -0
  141. package/plugin/skills/pr/SKILL.md +73 -4
  142. package/plugin/skills/review/SKILL.md +190 -18
  143. package/plugin/skills/review-reply/SKILL.md +144 -7
@@ -0,0 +1,77 @@
1
+ import type { ReviewIssue, ContinuityVerdict } from '../core/types.js';
2
+ import type { Actor, Comment, PullRequest, ReviewVerdict } from './types.js';
3
+ /**
4
+ * 로컬 PR의 판단 규칙.
5
+ *
6
+ * 표면이 셋(CLI, MCP, 웹)이고 이 규칙을 산문으로 옮겨 적는 스킬 문서가 셋 더 있다.
7
+ * 규칙을 표면마다 다시 구현하면 하나가 반드시 뒤처진다 — 실제로 미해결 수를 스레드로
8
+ * 고친 뒤에도 웹 UI만 옛 계산으로 남아 같은 PR이 CLI에서 2, 웹에서 4로 보였다.
9
+ * 표면은 여기서 값을 가져다 쓰고 스스로 세지 않는다.
10
+ */
11
+ /**
12
+ * 아직 안 닫힌 코멘트.
13
+ *
14
+ * 미해결을 가르는 술어가 여기 하나뿐이다. 아래 두 함수도 이 목록에서 나온다 —
15
+ * 표면이 헤아릴 때와 늘어놓을 때를 따로 계산하면 같은 화면에서 수가 갈린다.
16
+ * 실제로 `pr show`가 머리글에 스레드 수를, 바로 아래 목록에 코멘트 수를 찍어
17
+ * "미해결 1"이라고 해놓고 세 줄을 늘어놓았다.
18
+ */
19
+ export declare function unresolvedComments(pr: PullRequest): Comment[];
20
+ /** 스레드 하나. 닫힌 것까지 포함한다 */
21
+ export interface Thread {
22
+ /** 이 스레드의 코멘트 전부. 붙은 순서 그대로다 */
23
+ all: Comment[];
24
+ /** 그중 안 닫힌 것만. 전부 닫혔으면 빈 배열이다 */
25
+ unresolved: Comment[];
26
+ /**
27
+ * 표면이 한 줄로 접을 때 보여줄 코멘트. 안 닫힌 것 중 첫 번째다.
28
+ * 전부 닫혔으면 맨 처음 코멘트가 온다.
29
+ */
30
+ head: Comment;
31
+ /** 안 닫힌 코멘트가 하나라도 있는가. 스레드가 열렸는지를 이 값으로 가른다 */
32
+ open: boolean;
33
+ }
34
+ /**
35
+ * 코멘트를 스레드로 묶는다. 닫힌 스레드도 함께 준다.
36
+ *
37
+ * 스레드를 묶는 규칙과 열렸는지 가르는 규칙이 여기 하나뿐이어야 한다. 웹 상세
38
+ * 페이지가 이걸 안 거치고 스레드를 따로 묶은 뒤 첫 코멘트의 resolved로 배지를
39
+ * 찍던 자리가 있었다. 뿌리가 닫히고 답글이 열린 스레드에서 같은 화면의 목록은
40
+ * "미해결 1"을, 상세는 "해결"을 찍었다.
41
+ */
42
+ export declare function threadsOf(pr: PullRequest): Thread[];
43
+ /** 안 닫힌 스레드 하나 */
44
+ export interface OpenThread {
45
+ /**
46
+ * 표면이 한 줄로 접을 때 보여줄 코멘트. 스레드에서 아직 안 닫힌 것 중 첫 번째다.
47
+ * 뿌리가 닫히고 답글만 열려 있으면 그 답글이 여기 온다.
48
+ */
49
+ root: Comment;
50
+ /** 이 스레드에서 안 닫힌 코멘트만. 붙은 순서 그대로다 */
51
+ comments: Comment[];
52
+ }
53
+ export declare function openThreads(pr: PullRequest): OpenThread[];
54
+ /** 안 닫힌 스레드 수. 표면의 "미해결 N"이 전부 이 값이다 */
55
+ export declare function unresolvedCount(pr: PullRequest): number;
56
+ /**
57
+ * 누가 했는지.
58
+ *
59
+ * `--author`나 MCP 인자로 준 값이 먼저다. 없으면 `GESTALT_ACTOR` 환경변수를 본다.
60
+ * 그것도 없으면 fallback이다. CLI와 MCP, 리뷰 게시가 각자 이 규칙을 적어두면
61
+ * 기본값을 바꿀 때 한 곳이 남는다.
62
+ */
63
+ export declare function resolveActor(explicit?: string, fallback?: Actor): Actor;
64
+ /**
65
+ * 리뷰 합의가 통과인가.
66
+ *
67
+ * 결함 심급은 critical과 high가 하나도 없어야 통과다. 정합 심급은 판정이 있고
68
+ * `coherent: false`면 결함이 없어도 막는다.
69
+ *
70
+ * 리뷰 파이프라인의 `approved`와 PR에 남길 판정이 이 함수 하나에서 나온다. 두 곳이
71
+ * 따로 세면 파이프라인은 통과인데 PR은 request_changes인 상태가 생긴다. 그때 어느
72
+ * 쪽을 믿어야 하는지 알 방법이 없다.
73
+ */
74
+ export declare function isConsensusApproved(issues: ReviewIssue[], continuityVerdict?: ContinuityVerdict): boolean;
75
+ /** 합의를 PR 판정으로 옮긴다. 경계는 isConsensusApproved 하나가 정한다 */
76
+ export declare function consensusVerdict(issues: ReviewIssue[], continuityVerdict?: ContinuityVerdict): Extract<ReviewVerdict, 'approve' | 'request_changes'>;
77
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../../src/local-pr/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE7E;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,EAAE,CAE7D;AAED,0BAA0B;AAC1B,MAAM,WAAW,MAAM;IACrB,gCAAgC;IAChC,GAAG,EAAE,OAAO,EAAE,CAAC;IACf,iCAAiC;IACjC,UAAU,EAAE,OAAO,EAAE,CAAC;IACtB;;;OAGG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,+CAA+C;IAC/C,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,EAAE,CAYnD;AAED,kBAAkB;AAClB,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,oCAAoC;IACpC,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,WAAW,GAAG,UAAU,EAAE,CAIzD;AAED,wCAAwC;AACxC,wBAAgB,eAAe,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,CAEvD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAE,KAAqB,GAAG,KAAK,CAEtF;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EAAE,EACrB,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,OAAO,CAIT;AAED,uDAAuD;AACvD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,WAAW,EAAE,EACrB,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,OAAO,CAAC,aAAa,EAAE,SAAS,GAAG,iBAAiB,CAAC,CAEvD"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 로컬 PR의 판단 규칙.
3
+ *
4
+ * 표면이 셋(CLI, MCP, 웹)이고 이 규칙을 산문으로 옮겨 적는 스킬 문서가 셋 더 있다.
5
+ * 규칙을 표면마다 다시 구현하면 하나가 반드시 뒤처진다 — 실제로 미해결 수를 스레드로
6
+ * 고친 뒤에도 웹 UI만 옛 계산으로 남아 같은 PR이 CLI에서 2, 웹에서 4로 보였다.
7
+ * 표면은 여기서 값을 가져다 쓰고 스스로 세지 않는다.
8
+ */
9
+ /**
10
+ * 아직 안 닫힌 코멘트.
11
+ *
12
+ * 미해결을 가르는 술어가 여기 하나뿐이다. 아래 두 함수도 이 목록에서 나온다 —
13
+ * 표면이 헤아릴 때와 늘어놓을 때를 따로 계산하면 같은 화면에서 수가 갈린다.
14
+ * 실제로 `pr show`가 머리글에 스레드 수를, 바로 아래 목록에 코멘트 수를 찍어
15
+ * "미해결 1"이라고 해놓고 세 줄을 늘어놓았다.
16
+ */
17
+ export function unresolvedComments(pr) {
18
+ return pr.comments.filter((c) => !c.resolved);
19
+ }
20
+ /**
21
+ * 코멘트를 스레드로 묶는다. 닫힌 스레드도 함께 준다.
22
+ *
23
+ * 스레드를 묶는 규칙과 열렸는지 가르는 규칙이 여기 하나뿐이어야 한다. 웹 상세
24
+ * 페이지가 이걸 안 거치고 스레드를 따로 묶은 뒤 첫 코멘트의 resolved로 배지를
25
+ * 찍던 자리가 있었다. 뿌리가 닫히고 답글이 열린 스레드에서 같은 화면의 목록은
26
+ * "미해결 1"을, 상세는 "해결"을 찍었다.
27
+ */
28
+ export function threadsOf(pr) {
29
+ const byThread = new Map();
30
+ for (const comment of pr.comments) {
31
+ const bucket = byThread.get(comment.threadId);
32
+ if (bucket)
33
+ bucket.push(comment);
34
+ else
35
+ byThread.set(comment.threadId, [comment]);
36
+ }
37
+ return [...byThread.values()].map((all) => {
38
+ const unresolved = all.filter((c) => !c.resolved);
39
+ return { all, unresolved, head: unresolved[0] ?? all[0], open: unresolved.length > 0 };
40
+ });
41
+ }
42
+ export function openThreads(pr) {
43
+ return threadsOf(pr)
44
+ .filter((t) => t.open)
45
+ .map((t) => ({ root: t.head, comments: t.unresolved }));
46
+ }
47
+ /** 안 닫힌 스레드 수. 표면의 "미해결 N"이 전부 이 값이다 */
48
+ export function unresolvedCount(pr) {
49
+ return openThreads(pr).length;
50
+ }
51
+ /**
52
+ * 누가 했는지.
53
+ *
54
+ * `--author`나 MCP 인자로 준 값이 먼저다. 없으면 `GESTALT_ACTOR` 환경변수를 본다.
55
+ * 그것도 없으면 fallback이다. CLI와 MCP, 리뷰 게시가 각자 이 규칙을 적어두면
56
+ * 기본값을 바꿀 때 한 곳이 남는다.
57
+ */
58
+ export function resolveActor(explicit, fallback = 'human:local') {
59
+ return explicit ?? process.env['GESTALT_ACTOR'] ?? fallback;
60
+ }
61
+ /**
62
+ * 리뷰 합의가 통과인가.
63
+ *
64
+ * 결함 심급은 critical과 high가 하나도 없어야 통과다. 정합 심급은 판정이 있고
65
+ * `coherent: false`면 결함이 없어도 막는다.
66
+ *
67
+ * 리뷰 파이프라인의 `approved`와 PR에 남길 판정이 이 함수 하나에서 나온다. 두 곳이
68
+ * 따로 세면 파이프라인은 통과인데 PR은 request_changes인 상태가 생긴다. 그때 어느
69
+ * 쪽을 믿어야 하는지 알 방법이 없다.
70
+ */
71
+ export function isConsensusApproved(issues, continuityVerdict) {
72
+ const hasDefect = issues.some((i) => i.severity === 'critical' || i.severity === 'high');
73
+ const continuityBlocks = continuityVerdict ? !continuityVerdict.coherent : false;
74
+ return !hasDefect && !continuityBlocks;
75
+ }
76
+ /** 합의를 PR 판정으로 옮긴다. 경계는 isConsensusApproved 하나가 정한다 */
77
+ export function consensusVerdict(issues, continuityVerdict) {
78
+ return isConsensusApproved(issues, continuityVerdict) ? 'approve' : 'request_changes';
79
+ }
80
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../../src/local-pr/policy.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAe;IAChD,OAAO,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AAChD,CAAC;AAiBD;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,EAAe;IACvC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC9C,KAAK,MAAM,OAAO,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,MAAM;YAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;;YAC5B,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACxC,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAClD,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAE,EAAE,IAAI,EAAE,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IAC1F,CAAC,CAAC,CAAC;AACL,CAAC;AAaD,MAAM,UAAU,WAAW,CAAC,EAAe;IACzC,OAAO,SAAS,CAAC,EAAE,CAAC;SACjB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SACrB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,eAAe,CAAC,EAAe;IAC7C,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,QAAiB,EAAE,WAAkB,aAAa;IAC7E,OAAO,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,QAAQ,CAAC;AAC9D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAqB,EACrB,iBAAqC;IAErC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC;IACzF,MAAM,gBAAgB,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;IACjF,OAAO,CAAC,SAAS,IAAI,CAAC,gBAAgB,CAAC;AACzC,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,gBAAgB,CAC9B,MAAqB,EACrB,iBAAqC;IAErC,OAAO,mBAAmB,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC;AACxF,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 로컬 PR을 가진 레포 목록.
3
+ *
4
+ * 웹 UI 하나가 여러 레포를 보여주려면 어떤 레포가 있는지 알아야 한다. 그런데 그
5
+ * 목록을 요청이 정하게 두면 안 된다. 이 서버는 인증이 없다 — 요청이 레포 경로를
6
+ * 지정할 수 있으면 "이 머신의 아무 git 레포나 HTTP로 읽는" 도구가 된다.
7
+ *
8
+ * 그래서 목록은 서버가 시작할 때 아는 닫힌 집합이다. URL에는 경로가 아니라 키만
9
+ * 실리므로 요청으로 새 자리를 가리킬 방법이 없다.
10
+ *
11
+ * 목록에 넣는 쪽도 같은 이유로 좁다. `registerRepo`를 부르는 자리는 `gestalt pr serve`
12
+ * 하나다 — 사람이 그 레포에서 웹 UI를 직접 띄운 순간이다. `--repo-root`로 다른 레포를
13
+ * 가리켜 그 전제를 비껴가는 길은 CLI가 막는다. 그리고 한 번 들어간 줄을 사람이
14
+ * `repos.json`을 손으로 고쳐야만 뺄 수 있으면 안 되므로 `unregisterRepo`를 함께 둔다. 예전에는 `pr create`가
15
+ * 불렀는데, 그 경로는 MCP `ges_pr`의 `repoRoot`로 이어져 있어서 에이전트가 도구 호출
16
+ * 한 번으로 아무 레포나 목록에 영구히 넣을 수 있었다. 한 번 들어가면 사용자가 전혀
17
+ * 다른 레포에서 `pr serve`를 쳐도 그 레포의 diff와 코멘트가 인증 없는 엔드포인트로
18
+ * 계속 나간다.
19
+ *
20
+ * 이 선택으로 못 하게 되는 것: PR을 만들기만 하고 한 번도 serve를 안 돌린 레포는 다른
21
+ * 레포의 목록에 안 뜬다. 그 레포에서 `pr serve`를 한 번 돌려야 들어온다. 워크트리는
22
+ * 저장소를 공유하므로 본체에서 한 번 돌리면 워크트리에서 만든 PR도 함께 보인다.
23
+ *
24
+ * `~/.gestalt/`에 두는 건 이미 있는 관행이다 — events.db와 사용자 프로필이 거기 있다.
25
+ */
26
+ export interface RegisteredRepo {
27
+ /** 공용 git 디렉토리의 해시. URL에 실리는 값이라 경로가 안 드러난다 */
28
+ key: string;
29
+ path: string;
30
+ name: string;
31
+ addedAt: string;
32
+ }
33
+ /** 레포를 가리키는 키. 워크트리 어디서 구해도 같은 값이 나온다 */
34
+ export declare function repoKey(repoRoot: string): string;
35
+ /**
36
+ * 목록을 고치는 동안 다른 프로세스를 막는다.
37
+ *
38
+ * 워크트리 여럿이 같은 목록에 동시에 손대는 게 이 레포의 기본 흐름이다. 잠금 없이
39
+ * 읽고 고쳐 쓰면 나중에 쓴 쪽이 먼저 쓴 쪽의 항목을 덮는다.
40
+ *
41
+ * `mkdir`는 있으면 실패하니까 그 자체가 잠금이다. 쥔 채로 죽은 프로세스가 목록을 영영
42
+ * 못 고치게 만들면 안 되므로 버려진 잠금은 부순다. 부수는 순간에 둘이 겹치면 둘 다
43
+ * `mkdir`을 시도하고 한쪽만 성공한다 — 진 쪽은 다시 기다린다.
44
+ *
45
+ * **버려졌는지는 주인의 pid로 가른다.** 예전에는 잠금 디렉토리의 mtime만 봤는데,
46
+ * 잠금은 잡을 때 한 번 만들어지고 쥔 동안 mtime이 안 올라간다. 그래서 `fn()`이
47
+ * 5초를 넘기면 살아 있는 잠금이 버려진 것으로 보였다. 남이 부수고 들어와 둘이 나란히
48
+ * 읽고 고쳐 써서 늦게 쓴 쪽이 상대 항목을 덮었다. 파일은 rename이라 안 깨지고 등록만
49
+ * 조용히 사라진다.
50
+ *
51
+ * 쥔 동안 mtime을 주기적으로 올리는 방법은 여기서 안 통한다. `fn()`이 통째로 동기라
52
+ * 타이머가 돌 틈이 없다 — 갱신이 필요한 바로 그 순간에 이벤트 루프가 막혀 있다.
53
+ * 대신 주인이 살아 있는지를 직접 묻는다.
54
+ *
55
+ * `fn`에는 `stillMine`을 준다. 부수기와 다시 잡기가 겹치는 좁은 틈이 남아 있어,
56
+ * 쓰기 직전에 잠금이 아직 내 것인지 다시 확인할 수 있어야 한다.
57
+ *
58
+ * 잠금 소유권을 테스트에서 확인할 수 있게 내보낸다. 그 갈래는 잠금을 오래 쥔 쪽과
59
+ * 부순 쪽이 겹쳐야 열리는데, `registerRepo`로는 그 상황을 만들 수 없다.
60
+ */
61
+ export declare function withLock<T>(fn: (ctx: {
62
+ stillMine: () => boolean;
63
+ }) => T): T;
64
+ /**
65
+ * 레포를 목록에 넣는다. 이미 있으면 경로만 갱신한다.
66
+ *
67
+ * `pr serve`가 부른다. 여기 말고 목록을 늘리는 자리는 없다 — 위 머리말 참고.
68
+ */
69
+ export declare function registerRepo(repoRoot: string): RegisteredRepo;
70
+ /**
71
+ * 레포를 목록에서 뺀다. 없던 키면 false다.
72
+ *
73
+ * 넣는 문은 좁게 뒀는데 빼는 문이 없으면, 한 번 들어간 레포는 사용자가 손으로
74
+ * `~/.gestalt/repos.json`을 고치기 전까지 인증 없는 뷰어 목록에 남는다.
75
+ */
76
+ export declare function unregisterRepo(key: string): boolean;
77
+ /** 지금도 디스크에 있는 레포만 돌려준다. 지워진 자리는 목록에서 뺀다 */
78
+ export declare function listRepos(): RegisteredRepo[];
79
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/local-pr/registry.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAiCD,yCAAyC;AACzC,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhD;AAgFD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE;IAAE,SAAS,EAAE,MAAM,OAAO,CAAA;CAAE,KAAK,CAAC,GAAG,CAAC,CA+C3E;AAoED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAkB7D;AAcD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASnD;AAED,4CAA4C;AAC5C,wBAAgB,SAAS,IAAI,cAAc,EAAE,CAE5C"}
@@ -0,0 +1,307 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
3
+ import { dirname, isAbsolute, join, normalize } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { ensureGestaltHome, gestaltPath } from '../core/home.js';
6
+ import { gitCommonDir } from './git.js';
7
+ /**
8
+ * 목록 항목의 모양.
9
+ *
10
+ * 읽을 때 강제한다. 이 `path`는 `new LocalPrEngine(repo.path)`의 cwd가 되고 상세 페이지가
11
+ * 거기서 git을 돌린다. 파일이 손으로 고쳐졌거나 다른 버전이 쓴 모양이면 그 값이 그대로
12
+ * 명령의 작업 디렉토리가 되므로, 파싱만 하고 믿을 자리가 아니다.
13
+ *
14
+ * `path`는 정규화된 절대경로만 받는다 — `..`가 섞인 값은 `normalize`를 거치면 자기 자신과
15
+ * 달라져서 걸린다. 다만 이 검사는 모양만 본다. 그 경로가 열어도 되는 레포인지는 못 가른다.
16
+ */
17
+ const registeredRepoSchema = z.object({
18
+ key: z.string().regex(/^[0-9a-f]{8}$/),
19
+ path: z
20
+ .string()
21
+ .min(1)
22
+ .refine((p) => !p.includes('\0'), { message: 'NUL 포함' })
23
+ .refine((p) => isAbsolute(p) && normalize(p) === p, { message: '정규화된 절대경로가 아님' }),
24
+ name: z.string().min(1),
25
+ addedAt: z.string(),
26
+ });
27
+ /** 목록 파일 자리. 홈 해석은 `core/home.ts`가 한다 */
28
+ function registryPath() {
29
+ return gestaltPath('repos.json');
30
+ }
31
+ /** 지금도 디스크에 있는가. 지워진 레포는 목록에 둘 이유가 없다 */
32
+ function alive(repo) {
33
+ return existsSync(join(repo.path, '.git'));
34
+ }
35
+ /** 레포를 가리키는 키. 워크트리 어디서 구해도 같은 값이 나온다 */
36
+ export function repoKey(repoRoot) {
37
+ return createHash('sha1').update(gitCommonDir(repoRoot)).digest('hex').slice(0, 8);
38
+ }
39
+ /**
40
+ * 목록을 읽는다. 모양이 안 맞는 항목은 버린다.
41
+ *
42
+ * 항목 하나가 이상하다고 전체를 버리지는 않는다. 손상된 목록 때문에 서버가 아예 안 뜨는
43
+ * 게 더 나쁘다.
44
+ */
45
+ function read() {
46
+ const path = registryPath();
47
+ if (!existsSync(path))
48
+ return [];
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(readFileSync(path, 'utf-8'));
52
+ }
53
+ catch {
54
+ return [];
55
+ }
56
+ if (!Array.isArray(parsed))
57
+ return [];
58
+ const kept = [];
59
+ for (const item of parsed) {
60
+ const result = registeredRepoSchema.safeParse(item);
61
+ if (result.success)
62
+ kept.push(result.data);
63
+ }
64
+ return kept;
65
+ }
66
+ /** 주인이 안 잡히는 잠금을 부수기까지 */
67
+ const LOCK_STALE_MS = 5_000;
68
+ /**
69
+ * 주인이 살아 있어도 이만큼 지나면 부순다.
70
+ *
71
+ * pid는 재활용된다. 잠금을 쥔 채 죽은 프로세스의 번호를 남이 물려받으면 살아 있다고
72
+ * 읽혀서 아무도 목록을 못 고치게 된다. 그 자리를 여는 마지막 문이다.
73
+ */
74
+ const LOCK_HARD_STALE_MS = 60_000;
75
+ const LOCK_WAIT_MS = 2_000;
76
+ /** 대기 간격. 바퀴마다 배로 늘리되 이 값에서 멈춘다 */
77
+ const LOCK_BACKOFF_START_MS = 5;
78
+ const LOCK_BACKOFF_MAX_MS = 100;
79
+ /** 동기 대기. 이 파일의 쓰기 경로는 전부 동기라 여기서만 잠깐 멈춘다 */
80
+ function sleep(ms) {
81
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
82
+ }
83
+ function readOwner(ownerPath) {
84
+ try {
85
+ const parsed = JSON.parse(readFileSync(ownerPath, 'utf-8'));
86
+ if (typeof parsed !== 'object' || parsed === null)
87
+ return null;
88
+ const { token, pid } = parsed;
89
+ if (typeof token !== 'string' || typeof pid !== 'number')
90
+ return null;
91
+ return { token, pid };
92
+ }
93
+ catch {
94
+ // 아직 안 쓰였거나(mkdir 직후) 옛 버전이 쓴 모양이다. 주인을 모르는 것으로 친다
95
+ return null;
96
+ }
97
+ }
98
+ /** 그 프로세스가 아직 도는가. 신호 0은 아무것도 안 보내고 존재만 묻는다 */
99
+ function alivePid(pid) {
100
+ try {
101
+ process.kill(pid, 0);
102
+ return true;
103
+ }
104
+ catch (e) {
105
+ // EPERM은 남의 것이라 못 건드리는 것뿐이다. 그래도 살아는 있다
106
+ return e.code === 'EPERM';
107
+ }
108
+ }
109
+ /**
110
+ * 목록을 고치는 동안 다른 프로세스를 막는다.
111
+ *
112
+ * 워크트리 여럿이 같은 목록에 동시에 손대는 게 이 레포의 기본 흐름이다. 잠금 없이
113
+ * 읽고 고쳐 쓰면 나중에 쓴 쪽이 먼저 쓴 쪽의 항목을 덮는다.
114
+ *
115
+ * `mkdir`는 있으면 실패하니까 그 자체가 잠금이다. 쥔 채로 죽은 프로세스가 목록을 영영
116
+ * 못 고치게 만들면 안 되므로 버려진 잠금은 부순다. 부수는 순간에 둘이 겹치면 둘 다
117
+ * `mkdir`을 시도하고 한쪽만 성공한다 — 진 쪽은 다시 기다린다.
118
+ *
119
+ * **버려졌는지는 주인의 pid로 가른다.** 예전에는 잠금 디렉토리의 mtime만 봤는데,
120
+ * 잠금은 잡을 때 한 번 만들어지고 쥔 동안 mtime이 안 올라간다. 그래서 `fn()`이
121
+ * 5초를 넘기면 살아 있는 잠금이 버려진 것으로 보였다. 남이 부수고 들어와 둘이 나란히
122
+ * 읽고 고쳐 써서 늦게 쓴 쪽이 상대 항목을 덮었다. 파일은 rename이라 안 깨지고 등록만
123
+ * 조용히 사라진다.
124
+ *
125
+ * 쥔 동안 mtime을 주기적으로 올리는 방법은 여기서 안 통한다. `fn()`이 통째로 동기라
126
+ * 타이머가 돌 틈이 없다 — 갱신이 필요한 바로 그 순간에 이벤트 루프가 막혀 있다.
127
+ * 대신 주인이 살아 있는지를 직접 묻는다.
128
+ *
129
+ * `fn`에는 `stillMine`을 준다. 부수기와 다시 잡기가 겹치는 좁은 틈이 남아 있어,
130
+ * 쓰기 직전에 잠금이 아직 내 것인지 다시 확인할 수 있어야 한다.
131
+ *
132
+ * 잠금 소유권을 테스트에서 확인할 수 있게 내보낸다. 그 갈래는 잠금을 오래 쥔 쪽과
133
+ * 부순 쪽이 겹쳐야 열리는데, `registerRepo`로는 그 상황을 만들 수 없다.
134
+ */
135
+ export function withLock(fn) {
136
+ const lock = join(ensureGestaltHome(), 'repos.json.lock');
137
+ // 잠금을 누가 쥐고 있는지 적어둔다. 버려진 잠금은 남이 부수고 자기 것을 새로 만드는데,
138
+ // 그때 원래 주인이 끝나며 무조건 지우면 **남의** 잠금을 푼다. 토큰이 내 것일 때만 지운다
139
+ const ownerPath = join(lock, 'owner');
140
+ const token = randomUUID();
141
+ const start = Date.now();
142
+ let waitMs = LOCK_BACKOFF_START_MS;
143
+ for (;;) {
144
+ try {
145
+ mkdirSync(lock);
146
+ writeFileSync(ownerPath, JSON.stringify({ token, pid: process.pid }), 'utf-8');
147
+ break;
148
+ }
149
+ catch (e) {
150
+ if (e.code !== 'EEXIST')
151
+ throw e;
152
+ let heldFor;
153
+ try {
154
+ heldFor = Date.now() - statSync(lock).mtimeMs;
155
+ }
156
+ catch {
157
+ // 그 사이에 풀렸다. 다음 바퀴에서 잡는다
158
+ heldFor = 0;
159
+ }
160
+ const owner = readOwner(ownerPath);
161
+ const holderAlive = owner !== null && alivePid(owner.pid);
162
+ const abandoned = heldFor > LOCK_HARD_STALE_MS || (!holderAlive && heldFor > LOCK_STALE_MS);
163
+ if (abandoned) {
164
+ rmSync(lock, { recursive: true, force: true });
165
+ continue;
166
+ }
167
+ if (Date.now() - start > LOCK_WAIT_MS) {
168
+ throw new Error('레포 목록이 잠겨 있어서 못 고쳤어요', { cause: e });
169
+ }
170
+ // 바퀴마다 mkdir 실패와 stat이 붙는다. 간격을 배로 늘려 바퀴 수를 줄인다
171
+ sleep(waitMs);
172
+ waitMs = Math.min(waitMs * 2, LOCK_BACKOFF_MAX_MS);
173
+ }
174
+ }
175
+ const stillMine = () => readOwner(ownerPath)?.token === token;
176
+ try {
177
+ return fn({ stillMine });
178
+ }
179
+ finally {
180
+ if (stillMine())
181
+ rmSync(lock, { recursive: true, force: true });
182
+ }
183
+ }
184
+ /**
185
+ * 목록을 통째로 바꿔 쓴다.
186
+ *
187
+ * 옆에 다 쓴 뒤 `rename`으로 갈아 끼운다. `writeFileSync`는 자르고 나서 쓰기 때문에 그
188
+ * 사이에 읽는 쪽이 반쯤 쓰인 JSON을 본다. `read()`는 그 예외를 삼키고 빈 목록을 주므로
189
+ * 등록된 레포가 통째로 사라진 것처럼 보인다. `rename`은 원자적이라 읽는 쪽은 항상 이전
190
+ * 목록이나 새 목록 중 하나를 온전히 본다.
191
+ *
192
+ * 0600인 이유는 이 파일에 사용자가 작업하는 비공개 레포의 절대 경로가 모이기 때문이다.
193
+ */
194
+ function writeList(repos) {
195
+ const path = registryPath();
196
+ const dir = ensureTmpDir();
197
+ sweepStaleTmp(dir);
198
+ const tmp = join(dir, `repos.json.${process.pid}.tmp`);
199
+ writeFileSync(tmp, `${JSON.stringify(repos, null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
200
+ try {
201
+ renameSync(tmp, path);
202
+ }
203
+ catch (e) {
204
+ try {
205
+ unlinkSync(tmp);
206
+ }
207
+ catch {
208
+ // 이미 없으면 됐다
209
+ }
210
+ throw e;
211
+ }
212
+ }
213
+ /**
214
+ * 옆에 쓸 임시 파일을 두는 자리.
215
+ *
216
+ * `~/.gestalt` 바로 아래가 아니라 전용 칸이다. 그 자리에는 이벤트 DB랑 사용자
217
+ * 프로필, 업데이트 캐시가 함께 모이는데 쓸 때마다 그 전부를 훑을 이유가 없다.
218
+ * 항목이 늘수록 스캔도 같이 늘던 자리다.
219
+ *
220
+ * 같은 파일시스템이라 `rename`은 그대로 원자적이다.
221
+ */
222
+ function ensureTmpDir() {
223
+ const dir = gestaltPath('tmp');
224
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
225
+ return dir;
226
+ }
227
+ /**
228
+ * 쓰다 만 임시 파일을 치운다.
229
+ *
230
+ * pid를 붙여 쓰므로 쓰는 도중 죽으면 그 이름이 영영 남는다. 목록에는 영향이 없지만
231
+ * 디스크에 쌓인다. 잠금 안에서 도니까 지금 쓰는 중인 남의 tmp를 뺏을 일은 없다.
232
+ */
233
+ function sweepStaleTmp(dir) {
234
+ let names;
235
+ try {
236
+ names = readdirSync(dir);
237
+ }
238
+ catch {
239
+ return;
240
+ }
241
+ for (const name of names) {
242
+ if (!name.startsWith('repos.json.') || !name.endsWith('.tmp'))
243
+ continue;
244
+ try {
245
+ unlinkSync(join(dir, name));
246
+ }
247
+ catch {
248
+ // 남이 방금 갈아 끼웠다. 없어졌으면 된 것이다
249
+ }
250
+ }
251
+ }
252
+ /**
253
+ * 레포를 목록에 넣는다. 이미 있으면 경로만 갱신한다.
254
+ *
255
+ * `pr serve`가 부른다. 여기 말고 목록을 늘리는 자리는 없다 — 위 머리말 참고.
256
+ */
257
+ export function registerRepo(repoRoot) {
258
+ const key = repoKey(repoRoot);
259
+ const main = dirname(gitCommonDir(repoRoot));
260
+ const entry = {
261
+ key,
262
+ path: main,
263
+ name: main.split('/').filter(Boolean).at(-1) ?? main,
264
+ addedAt: new Date().toISOString(),
265
+ };
266
+ return withLock(({ stillMine }) => {
267
+ // 쓸 때 죽은 항목을 걷어내는 건 파일이 무한정 길어지는 걸 막으려는 것뿐이다.
268
+ // 지워진 레포를 안 보여주는 보장은 `listRepos`의 읽기 시점 필터가 진다
269
+ const kept = read().filter((r) => r.key !== key && alive(r));
270
+ assertHeld(stillMine);
271
+ writeList([...kept, entry].sort((a, b) => a.name.localeCompare(b.name)));
272
+ return entry;
273
+ });
274
+ }
275
+ /**
276
+ * 읽고 고친 값을 쓰기 직전에 잠금이 아직 내 것인지 다시 묻는다.
277
+ *
278
+ * 남이 내 잠금을 부수고 자기 것을 만든 사이라면, 지금 손에 든 목록은 그 사이에 남이
279
+ * 쓴 항목을 안 담고 있다. 그대로 쓰면 상대 등록이 조용히 사라진다. 덮느니 실패한다.
280
+ */
281
+ function assertHeld(stillMine) {
282
+ if (!stillMine()) {
283
+ throw new Error('레포 목록 잠금을 뺏겨서 안 썼어요. 다시 불러주세요');
284
+ }
285
+ }
286
+ /**
287
+ * 레포를 목록에서 뺀다. 없던 키면 false다.
288
+ *
289
+ * 넣는 문은 좁게 뒀는데 빼는 문이 없으면, 한 번 들어간 레포는 사용자가 손으로
290
+ * `~/.gestalt/repos.json`을 고치기 전까지 인증 없는 뷰어 목록에 남는다.
291
+ */
292
+ export function unregisterRepo(key) {
293
+ return withLock(({ stillMine }) => {
294
+ const before = read();
295
+ const kept = before.filter((r) => r.key !== key);
296
+ if (kept.length === before.length)
297
+ return false;
298
+ assertHeld(stillMine);
299
+ writeList(kept);
300
+ return true;
301
+ });
302
+ }
303
+ /** 지금도 디스크에 있는 레포만 돌려준다. 지워진 자리는 목록에서 뺀다 */
304
+ export function listRepos() {
305
+ return read().filter(alive);
306
+ }
307
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/local-pr/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import type { IEventStore } from '../events/store.js';
2
+ import type { PrClosedPayload, PrCreatedPayload, PrEditedPayload, PrMergedPayload, PullRequest } from './types.js';
3
+ /** 이 도메인이 쓰는 이벤트 이름 */
4
+ export declare const PrEvent: {
5
+ readonly CREATED: "pr.created";
6
+ readonly UPDATED: "pr.updated";
7
+ readonly EDITED: "pr.edited";
8
+ readonly COMMENT_ADDED: "pr.comment.added";
9
+ readonly COMMENT_RESOLVED: "pr.comment.resolved";
10
+ readonly REVIEW_SUBMITTED: "pr.review.submitted";
11
+ readonly MERGED: "pr.merged";
12
+ readonly CLOSED: "pr.closed";
13
+ };
14
+ /**
15
+ * 이벤트를 접어 PR 상태를 만든다.
16
+ *
17
+ * 상태를 따로 저장하지 않아서 DB를 지우고 이벤트만 남아도 같은 결과가 나온다.
18
+ * ExecuteSessionRepository가 쓰는 방식과 같다.
19
+ */
20
+ export declare class PullRequestRepository {
21
+ private eventStore;
22
+ constructor(eventStore: IEventStore);
23
+ reconstruct(prId: string): PullRequest | null;
24
+ list(): string[];
25
+ /**
26
+ * 모든 PR을 한 번의 조회로 만든다.
27
+ *
28
+ * id를 훑어 하나씩 replay하면 PR 수만큼 쿼리가 나간다. 목록 화면과 CLI list가
29
+ * 부르는 자리라 PR이 늘수록 그대로 느려진다.
30
+ */
31
+ reconstructAll(): PullRequest[];
32
+ /**
33
+ * 상태별 개수만 센다.
34
+ *
35
+ * 배지 하나 찍자고 PR을 다 접던 자리가 있다. 코멘트와 리뷰와 라운드 배열을 만들고
36
+ * 개수만 읽고 버린다. 여기서는 `foldStatus`로 상태만 정하고 접기를 안 한다.
37
+ */
38
+ countByStatus(): Record<PullRequest['status'], number>;
39
+ /**
40
+ * 이 상태인 PR만 만든다.
41
+ *
42
+ * 먼저 상태만 접어 후보를 좁히고 남은 것만 통째로 접는다. 상태 필터가 붙은 호출은
43
+ * 대부분을 버리는데 버릴 것까지 다 만들어 놓을 이유가 없다.
44
+ */
45
+ reconstructByStatus(status: PullRequest['status']): PullRequest[];
46
+ /**
47
+ * 상태만 접는다. `fold`가 만드는 것 중 `status` 하나만 나오는 가벼운 갈래다.
48
+ *
49
+ * 상태를 정하는 이벤트는 다섯뿐이다 — created(open), updated(고쳐 올렸으니 다시
50
+ * open), review.submitted(request_changes일 때만), merged, closed. 코멘트를 붙이고
51
+ * 닫는 이벤트는 `comments`와 라운드의 코멘트 수만 건드리고 상태에는 손대지 않는다.
52
+ * approve와 comment 판정도 라운드의 판정만 적고 상태를 안 옮긴다.
53
+ *
54
+ * `pr.edited`도 여기 없다. 일부러 없다 — 본문 오타를 고쳤다고 리뷰어가 내린
55
+ * `changes_requested`가 풀리면 판정이 글자 수정으로 리셋된다. `fold`만 그 이벤트를
56
+ * 읽고 제목과 본문을 갈아 끼운다.
57
+ *
58
+ * 두 갈래가 갈리면 목록과 필터가 어긋난다. `list(status)`가 `list()`를 거른 것과
59
+ * 같은지 보는 테스트가 그 자리를 잡는데, 그 테스트는 approve와 comment 판정을 둘 다
60
+ * 지난다 — 위 문장이 둘을 나란히 적어 두니 밟는 것도 둘이어야 한다.
61
+ */
62
+ private foldStatus;
63
+ private fold;
64
+ private apply;
65
+ }
66
+ export type { PrCreatedPayload, PrEditedPayload, PrMergedPayload, PrClosedPayload };
67
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../../src/local-pr/repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,KAAK,EAIV,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,eAAe,EAEf,WAAW,EAIZ,MAAM,YAAY,CAAC;AAEpB,uBAAuB;AACvB,eAAO,MAAM,OAAO;;;;;;;;;CASV,CAAC;AAEX;;;;;GAKG;AACH,qBAAa,qBAAqB;IACpB,OAAO,CAAC,UAAU;gBAAV,UAAU,EAAE,WAAW;IAE3C,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAM7C,IAAI,IAAI,MAAM,EAAE;IAIhB;;;;;OAKG;IACH,cAAc,IAAI,WAAW,EAAE;IAU/B;;;;;OAKG;IACH,aAAa,IAAI,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IActD;;;;;OAKG;IACH,mBAAmB,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,WAAW,EAAE;IAUjE;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,UAAU;IA8BlB,OAAO,CAAC,IAAI;IA8BZ,OAAO,CAAC,KAAK;CA6Fd;AAMD,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,CAAC"}