@adanami/kaleido 0.1.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 (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/dist/cli/index.js +1489 -0
  4. package/dist/client/assets/abap-08VXUWAP.js +1 -0
  5. package/dist/client/assets/abnfDiagram-N423BO3Z-BNC_bOwN.js +1 -0
  6. package/dist/client/assets/apex-BWPQTe0t.js +1 -0
  7. package/dist/client/assets/arc-BQjVBT7z.js +1 -0
  8. package/dist/client/assets/architecture-TIHT7OUA-rtiyad39.js +1 -0
  9. package/dist/client/assets/architectureDiagram-T3A2C74G-NbyTzj4B.js +36 -0
  10. package/dist/client/assets/array-BifhSqXX.js +1 -0
  11. package/dist/client/assets/azcli-Bc_sGQ0U.js +1 -0
  12. package/dist/client/assets/bat-i0X4ZdIN.js +1 -0
  13. package/dist/client/assets/bicep-B5-_aFwp.js +2 -0
  14. package/dist/client/assets/blockDiagram-VBNYF7ZC-D7RFrUDl.js +132 -0
  15. package/dist/client/assets/c4Diagram-5PPSVZJV-CVtHbZ8m.js +10 -0
  16. package/dist/client/assets/cameligo-DMUM7wLl.js +1 -0
  17. package/dist/client/assets/channel-Cg-CR3Kv.js +1 -0
  18. package/dist/client/assets/chunk-2GRJ4B5K-vymlEdvz.js +1 -0
  19. package/dist/client/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  20. package/dist/client/assets/chunk-4I5QYGJK-Dru7MVd5.js +1 -0
  21. package/dist/client/assets/chunk-5RXB4S5H-_mW4jXZD.js +231 -0
  22. package/dist/client/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  23. package/dist/client/assets/chunk-6Q2QTUOP-C-Jr6Q1u.js +88 -0
  24. package/dist/client/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  25. package/dist/client/assets/chunk-7Z6QIM7H-CH0etl6L.js +10 -0
  26. package/dist/client/assets/chunk-GF5L2VYU-l0ZYrFWn.js +206 -0
  27. package/dist/client/assets/chunk-I66GZJ75-CKcp5S17.js +127 -0
  28. package/dist/client/assets/chunk-JQJVKLGR-BXKeK8Yn.js +156 -0
  29. package/dist/client/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  30. package/dist/client/assets/chunk-KBJHAD2P-DPP8q1jj.js +1 -0
  31. package/dist/client/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
  32. package/dist/client/assets/chunk-NSK5VX7P-CJvQSFjo.js +2 -0
  33. package/dist/client/assets/chunk-QR6OTTB3-DAmoW-jk.js +62 -0
  34. package/dist/client/assets/chunk-RYQCIY6F-mEV_z1Vs.js +1 -0
  35. package/dist/client/assets/chunk-UBXNYLIW-B9FQ3EVs.js +1 -0
  36. package/dist/client/assets/chunk-W5SLKNZC-B7q4L6_i.js +1 -0
  37. package/dist/client/assets/chunk-WRU74C26-DwPGHVIY.js +70 -0
  38. package/dist/client/assets/chunk-XXDRQBXY-B_1m3XyG.js +1 -0
  39. package/dist/client/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  40. package/dist/client/assets/classDiagram-JCYQIIEL-DN-cM4gO.js +1 -0
  41. package/dist/client/assets/classDiagram-v2-OCEON4UE-DN-cM4gO.js +1 -0
  42. package/dist/client/assets/clojure-Cm7r79vr.js +1 -0
  43. package/dist/client/assets/codicon-Brq4_Ui5.ttf +0 -0
  44. package/dist/client/assets/coffee-Ba7i2nA0.js +1 -0
  45. package/dist/client/assets/cose-bilkent-JH36ORCC-_wSiGHW9.js +1 -0
  46. package/dist/client/assets/cpp-C7h46wYY.js +1 -0
  47. package/dist/client/assets/csharp-BKxtCVv1.js +1 -0
  48. package/dist/client/assets/csp-bTuwJoIa.js +1 -0
  49. package/dist/client/assets/css-DIMkf-bt.js +3 -0
  50. package/dist/client/assets/cynefin-VYW2F7L2-BSXkfF6B.js +1 -0
  51. package/dist/client/assets/cynefinDiagram-MW4NZA55-DWqP9pCq.js +62 -0
  52. package/dist/client/assets/cypher-CVaqCwHa.js +1 -0
  53. package/dist/client/assets/cytoscape.esm-C2x7inlt.js +321 -0
  54. package/dist/client/assets/dagre-Buvkdvvj.js +1 -0
  55. package/dist/client/assets/dagre-VZM6K2ZE-D-6mKeP4.js +4 -0
  56. package/dist/client/assets/dart-onAF5SnQ.js +1 -0
  57. package/dist/client/assets/defaultLocale-BFoDCU3G.js +1 -0
  58. package/dist/client/assets/diagram-7IWD3JNH-DwifWIOM.js +30 -0
  59. package/dist/client/assets/diagram-B4RE2ZJO-CZy5Cter.js +3 -0
  60. package/dist/client/assets/diagram-LBJQPF4R-D64OHGQ1.js +24 -0
  61. package/dist/client/assets/diagram-Q27KOJAE-CYJYm3MT.js +24 -0
  62. package/dist/client/assets/diagram-UB23O5K3-D9XqDFWB.js +41 -0
  63. package/dist/client/assets/dist-D9557-UX.js +1 -0
  64. package/dist/client/assets/dockerfile-DZFCIeNp.js +1 -0
  65. package/dist/client/assets/ebnfDiagram-BXEA7PRR-DXv0laGY.js +1 -0
  66. package/dist/client/assets/ecl-D05T4iGw.js +1 -0
  67. package/dist/client/assets/editor.worker-D9zwrD0f.js +30 -0
  68. package/dist/client/assets/elixir-6RTg0lbw.js +1 -0
  69. package/dist/client/assets/erDiagram-JOGREHBK-DtgpDnFq.js +85 -0
  70. package/dist/client/assets/eventmodeling-45OFAUF4-CIrFy3PN.js +1 -0
  71. package/dist/client/assets/flow9-C5_-GSwl.js +1 -0
  72. package/dist/client/assets/flowDiagram-UKHOOZJN-CsaPoqV0.js +1 -0
  73. package/dist/client/assets/freemarker2-C0K9bJg3.js +3 -0
  74. package/dist/client/assets/fsharp-C8Ef5oNN.js +1 -0
  75. package/dist/client/assets/ganttDiagram-PKOTCBZU-DbPT_9cm.js +292 -0
  76. package/dist/client/assets/gitGraph-TEB2WS4Q-BSzJ-zot.js +1 -0
  77. package/dist/client/assets/gitGraphDiagram-DS77QQ5N-Dv47ST8y.js +106 -0
  78. package/dist/client/assets/go-C-y9NEjX.js +1 -0
  79. package/dist/client/assets/graphlib-DS17s2tU.js +1 -0
  80. package/dist/client/assets/graphql-fmXr3nnJ.js +1 -0
  81. package/dist/client/assets/handlebars-C5Uh7yG8.js +1 -0
  82. package/dist/client/assets/hcl-CpzslTdj.js +1 -0
  83. package/dist/client/assets/html-CVq09h_T.js +1 -0
  84. package/dist/client/assets/index-BdNjwiL_.css +1 -0
  85. package/dist/client/assets/index-C-Az4wQZ.js +1288 -0
  86. package/dist/client/assets/info-DKCQHKI2-DrnuTwtV.js +1 -0
  87. package/dist/client/assets/infoDiagram-6WML65LV-BbOLkXMO.js +2 -0
  88. package/dist/client/assets/ini-sBoK_t0W.js +1 -0
  89. package/dist/client/assets/init-C-OQMol4.js +1 -0
  90. package/dist/client/assets/ishikawaDiagram-WSZJBQD7-BU0Xf4Jv.js +70 -0
  91. package/dist/client/assets/java-BEtHBSE6.js +1 -0
  92. package/dist/client/assets/javascript-BLNuR3ma.js +1 -0
  93. package/dist/client/assets/journeyDiagram-NVQOT4AX-Be02AxgF.js +139 -0
  94. package/dist/client/assets/julia-Bri6UV-V.js +1 -0
  95. package/dist/client/assets/kanban-definition-27J2QSJJ-DAJOmq0i.js +89 -0
  96. package/dist/client/assets/katex-CXMH3UgJ.js +257 -0
  97. package/dist/client/assets/kotlin-BOotOW0E.js +1 -0
  98. package/dist/client/assets/less-B9JPFI3C.js +2 -0
  99. package/dist/client/assets/lexon-CfSJPG6W.js +1 -0
  100. package/dist/client/assets/line-CY8MZqA_.js +1 -0
  101. package/dist/client/assets/linear-VmQ0cgWf.js +1 -0
  102. package/dist/client/assets/liquid-CqxeTiTR.js +1 -0
  103. package/dist/client/assets/lua-CsQS60Ue.js +1 -0
  104. package/dist/client/assets/m3-D-oSqn_W.js +1 -0
  105. package/dist/client/assets/map-BaFkSB1l.js +1 -0
  106. package/dist/client/assets/markdown-Cimd5fb3.js +1 -0
  107. package/dist/client/assets/mdx-CZgnrcpZ.js +1 -0
  108. package/dist/client/assets/mermaid-parser.core-BNyH4fWd.js +7 -0
  109. package/dist/client/assets/mermaid.core-D6SqRXfj.js +42 -0
  110. package/dist/client/assets/mindmap-definition-FAOFIHXS-Cb4njDU8.js +96 -0
  111. package/dist/client/assets/mips-CIPQ_RoX.js +1 -0
  112. package/dist/client/assets/msdax-DauUninz.js +1 -0
  113. package/dist/client/assets/mysql-SOo6toE5.js +1 -0
  114. package/dist/client/assets/objective-c-FvmIjYaQ.js +1 -0
  115. package/dist/client/assets/ordinal-BDEzSJ7C.js +1 -0
  116. package/dist/client/assets/packet-7NZHBO7P-C0OLtmrN.js +1 -0
  117. package/dist/client/assets/pascal-DrH0SRf2.js +1 -0
  118. package/dist/client/assets/pascaligo-D-ptJ9y-.js +1 -0
  119. package/dist/client/assets/path-fybaL0A-.js +1 -0
  120. package/dist/client/assets/pegDiagram-VL7TDLO6-CUlmavTd.js +1 -0
  121. package/dist/client/assets/perl-oz_6vUea.js +1 -0
  122. package/dist/client/assets/pgsql-DTj74zXo.js +1 -0
  123. package/dist/client/assets/php-nr791fC2.js +1 -0
  124. package/dist/client/assets/pie-RZYD4A2V-BOaXF8wo.js +1 -0
  125. package/dist/client/assets/pieDiagram-7S7Q4E2Y-DAuLOXyl.js +39 -0
  126. package/dist/client/assets/pla-CopQ2nXW.js +1 -0
  127. package/dist/client/assets/postiats-43DmfD33.js +1 -0
  128. package/dist/client/assets/powerquery-D3hlyOfw.js +1 -0
  129. package/dist/client/assets/powershell-DmHpPYUd.js +1 -0
  130. package/dist/client/assets/protobuf-C531GsRP.js +2 -0
  131. package/dist/client/assets/pug-Z5eAx3Zn.js +1 -0
  132. package/dist/client/assets/python-BKTOsIAC.js +1 -0
  133. package/dist/client/assets/qsharp-DkqhCAOL.js +1 -0
  134. package/dist/client/assets/quadrantDiagram-CIZ2JOQS-6T2tUzKE.js +7 -0
  135. package/dist/client/assets/r-BwWrilGY.js +1 -0
  136. package/dist/client/assets/radar-I7S5WNFK-84ZPjfzV.js +1 -0
  137. package/dist/client/assets/railroad-3IZDKUUU-BIancogu.js +1 -0
  138. package/dist/client/assets/railroad-abnf-AHOZXSZD-DY5Umeyq.js +1 -0
  139. package/dist/client/assets/railroad-ebnf-EBAXGLYW-BvHqcDCh.js +1 -0
  140. package/dist/client/assets/railroad-peg-LSFZ7HO6-Bcfw7tSJ.js +1 -0
  141. package/dist/client/assets/railroadDiagram-AXF67PYL-CG4n3oVN.js +1 -0
  142. package/dist/client/assets/razor-D3D2-y9K.js +1 -0
  143. package/dist/client/assets/redis-ClamHrr6.js +1 -0
  144. package/dist/client/assets/redshift-DT7zqm-g.js +1 -0
  145. package/dist/client/assets/requirementDiagram-LRYGKXZP-mZX3Srsb.js +84 -0
  146. package/dist/client/assets/restructuredtext-BYgofb2h.js +1 -0
  147. package/dist/client/assets/rough.esm-Dy-Kn_BL.js +1 -0
  148. package/dist/client/assets/ruby-DezsRK8O.js +1 -0
  149. package/dist/client/assets/rust-DdL9SqIa.js +1 -0
  150. package/dist/client/assets/sankeyDiagram-W5VNT64P-yPa5u_0L.js +40 -0
  151. package/dist/client/assets/sb-CcwsVR0C.js +1 -0
  152. package/dist/client/assets/scala-DHpiXF5c.js +1 -0
  153. package/dist/client/assets/scheme-BeGwcela.js +1 -0
  154. package/dist/client/assets/scss-gp-XZpBa.js +3 -0
  155. package/dist/client/assets/sequenceDiagram-SI44F4Z6-CyNIlTWm.js +162 -0
  156. package/dist/client/assets/shell-CC2rA5mh.js +1 -0
  157. package/dist/client/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  158. package/dist/client/assets/solidity-BEEn4gHE.js +1 -0
  159. package/dist/client/assets/sophia-CRfGWb83.js +1 -0
  160. package/dist/client/assets/sparql-D_Lu-MrJ.js +1 -0
  161. package/dist/client/assets/sql-NEE52Syq.js +1 -0
  162. package/dist/client/assets/src-CUuFImQF.js +1 -0
  163. package/dist/client/assets/st-DbInun42.js +1 -0
  164. package/dist/client/assets/stateDiagram-OKZ733FA-CPylCUxu.js +1 -0
  165. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-87clA1Da.js +1 -0
  166. package/dist/client/assets/swift-Bxkupp3x.js +1 -0
  167. package/dist/client/assets/swimlanes-SLNWSIFB-DBNGs0gR.js +2 -0
  168. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-DrshQU12.js +8 -0
  169. package/dist/client/assets/systemverilog-Bz4Y3fRF.js +1 -0
  170. package/dist/client/assets/tcl-DISqw1ZD.js +1 -0
  171. package/dist/client/assets/timeline-definition-Z64GVDOM-BAUrOlpT.js +120 -0
  172. package/dist/client/assets/treeView-QDETBFTQ-DWU_Tu3e.js +1 -0
  173. package/dist/client/assets/treemap-6X3UGDF4-C0VSsv4b.js +1 -0
  174. package/dist/client/assets/twig-De2hgUGE.js +1 -0
  175. package/dist/client/assets/typescript-DHaWXPl5.js +1 -0
  176. package/dist/client/assets/typespec-B8J7ngcE.js +1 -0
  177. package/dist/client/assets/vb-DV3o63ZY.js +1 -0
  178. package/dist/client/assets/vennDiagram-T6HMQDX7-DMDrxVrh.js +34 -0
  179. package/dist/client/assets/wardley-OPB4EBWU-2FXuD4eH.js +1 -0
  180. package/dist/client/assets/wardleyDiagram-T6FBY63Y-B6MdwST-.js +78 -0
  181. package/dist/client/assets/wgsl-DpFanUEy.js +298 -0
  182. package/dist/client/assets/xml-UOCAZ6xX.js +1 -0
  183. package/dist/client/assets/xychartDiagram-ELKLHX3M-4m0lgQEp.js +7 -0
  184. package/dist/client/assets/yaml-c5v3wojq.js +1 -0
  185. package/dist/client/index.html +22 -0
  186. package/dist/eslint-WWJOZ2KR.js +140 -0
  187. package/dist/server/ts/worker.js +360 -0
  188. package/package.json +85 -0
@@ -0,0 +1,1489 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+ import open from "open";
6
+
7
+ // src/shared/commentFormat.ts
8
+ function formatCommentPrompt(comment) {
9
+ if (comment.startLine === void 0 || comment.endLine === void 0) {
10
+ return `${comment.path}
11
+ ${comment.body}`;
12
+ }
13
+ const lines = comment.startLine === comment.endLine ? `L${comment.startLine}` : `L${comment.startLine}-L${comment.endLine}`;
14
+ return `${comment.path}:${lines}
15
+ ${comment.body}`;
16
+ }
17
+ function formatAllCommentsPrompt(comments) {
18
+ return comments.map(formatCommentPrompt).join("\n\n=====\n\n");
19
+ }
20
+
21
+ // src/shared/types.ts
22
+ var BROWSE_RANGE = { target: "browse", base: "browse" };
23
+ function rangeKey(range) {
24
+ const sanitize = (s) => s.replace(/[^a-zA-Z0-9_.-]/g, "_");
25
+ const mode = range.baseMode === "merge-base" ? "~mb" : "";
26
+ return `${sanitize(range.base)}${mode}__${sanitize(range.target)}`;
27
+ }
28
+ function describeRange(range) {
29
+ if (range.target === "browse") return "files";
30
+ if (range.target === "working") return "unstaged changes";
31
+ if (range.target === "staged") return `${range.base}..staged`;
32
+ if (range.target === ".") return `${range.base}..working tree`;
33
+ const sep3 = range.baseMode === "merge-base" ? "..." : "..";
34
+ return `${range.base}${sep3}${range.target}`;
35
+ }
36
+
37
+ // src/server/index.ts
38
+ import { existsSync } from "fs";
39
+ import { dirname, join as join4, relative as relative2 } from "path";
40
+ import { fileURLToPath } from "url";
41
+ import { serve } from "@hono/node-server";
42
+ import { serveStatic } from "@hono/node-server/serve-static";
43
+
44
+ // src/server/app.ts
45
+ import { Hono } from "hono";
46
+ import { streamSSE } from "hono/streaming";
47
+ import { z } from "zod";
48
+ import { zValidator } from "@hono/zod-validator";
49
+
50
+ // src/server/files/tree.ts
51
+ import { readdir, stat } from "fs/promises";
52
+ import { join as join2 } from "path";
53
+
54
+ // src/shared/documents.ts
55
+ var MARKDOWN_EXTENSIONS = [".md", ".markdown", ".mdown", ".mkd", ".mkdn"];
56
+ var HTML_EXTENSIONS = [".html", ".htm"];
57
+ function isDocumentPath(path) {
58
+ const lower = path.toLowerCase();
59
+ return [...MARKDOWN_EXTENSIONS, ...HTML_EXTENSIONS].some((ext) => lower.endsWith(ext));
60
+ }
61
+
62
+ // src/server/files/config.ts
63
+ import { readFileSync } from "fs";
64
+ import { join } from "path";
65
+ var CONFIG_FILE_NAME = ".kaleido.json";
66
+ var DEFAULT_EXCLUDES = [
67
+ ".git",
68
+ "node_modules",
69
+ ".next",
70
+ ".nuxt",
71
+ ".svelte-kit",
72
+ ".turbo",
73
+ ".cache",
74
+ ".parcel-cache",
75
+ ".pytest_cache",
76
+ ".mypy_cache",
77
+ ".gradle",
78
+ ".venv",
79
+ "venv",
80
+ "__pycache__",
81
+ "dist",
82
+ "build",
83
+ "out",
84
+ "coverage",
85
+ "target",
86
+ "vendor",
87
+ ".DS_Store"
88
+ ];
89
+ var DEFAULT_MAX_FILES = 2e4;
90
+ function loadViewerConfig(rootDir, cliExcludes = []) {
91
+ const file = readConfigFile(rootDir);
92
+ const useDefaults = file?.useDefaultExcludes !== false;
93
+ const exclude = [
94
+ ...useDefaults ? DEFAULT_EXCLUDES : [".git"],
95
+ ...file?.exclude ?? [],
96
+ ...cliExcludes
97
+ ];
98
+ const maxFiles = typeof file?.maxFiles === "number" && file.maxFiles > 0 ? file.maxFiles : DEFAULT_MAX_FILES;
99
+ return { exclude: [...new Set(exclude)], maxFiles };
100
+ }
101
+ function readConfigFile(rootDir) {
102
+ try {
103
+ const parsed = JSON.parse(readFileSync(join(rootDir, CONFIG_FILE_NAME), "utf8"));
104
+ if (typeof parsed !== "object" || parsed === null) return null;
105
+ const raw = parsed;
106
+ return {
107
+ exclude: Array.isArray(raw.exclude) ? raw.exclude.filter((v) => typeof v === "string") : void 0,
108
+ useDefaultExcludes: typeof raw.useDefaultExcludes === "boolean" ? raw.useDefaultExcludes : void 0,
109
+ maxFiles: typeof raw.maxFiles === "number" ? raw.maxFiles : void 0
110
+ };
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+ function createExcludeMatcher(patterns) {
116
+ const regexps = patterns.map(toRegExp).filter((r) => r !== null);
117
+ return (relPath) => regexps.some((re) => re.test(relPath));
118
+ }
119
+ function toRegExp(pattern) {
120
+ let source = pattern.trim().replace(/\\/g, "/");
121
+ if (source.length === 0) return null;
122
+ source = source.replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
123
+ if (source.length === 0) return null;
124
+ if (!source.includes("/")) source = `**/${source}`;
125
+ let out = "";
126
+ for (let i = 0; i < source.length; i++) {
127
+ const char = source[i];
128
+ if (char === "*") {
129
+ if (source[i + 1] === "*") {
130
+ i++;
131
+ if (source[i + 1] === "/") {
132
+ i++;
133
+ out += "(?:[^/]+/)*";
134
+ } else {
135
+ out += ".*";
136
+ }
137
+ } else {
138
+ out += "[^/]*";
139
+ }
140
+ continue;
141
+ }
142
+ if (char === "?") {
143
+ out += "[^/]";
144
+ continue;
145
+ }
146
+ out += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
147
+ }
148
+ try {
149
+ return new RegExp(`^${out}$`);
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ // src/server/files/tree.ts
156
+ async function collectFiles(rootDir, config, isTarget) {
157
+ const isExcluded = createExcludeMatcher(config.exclude);
158
+ const paths = [];
159
+ let isTruncated = false;
160
+ const walk = async (relDir) => {
161
+ if (isTruncated) return;
162
+ let entries;
163
+ try {
164
+ entries = await readdir(join2(rootDir, relDir), { withFileTypes: true });
165
+ } catch {
166
+ return;
167
+ }
168
+ const subdirs = [];
169
+ for (const entry of entries) {
170
+ const rel = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
171
+ if (isExcluded(rel)) continue;
172
+ if (entry.isDirectory()) {
173
+ subdirs.push(rel);
174
+ } else if (entry.isFile() && isTarget(rel)) {
175
+ if (paths.length >= config.maxFiles) {
176
+ isTruncated = true;
177
+ return;
178
+ }
179
+ paths.push(rel);
180
+ }
181
+ }
182
+ for (const subdir of subdirs) {
183
+ await walk(subdir);
184
+ if (isTruncated) return;
185
+ }
186
+ };
187
+ await walk("");
188
+ return { paths, isTruncated };
189
+ }
190
+ async function listDirectoryFiles(rootDir, config) {
191
+ const { paths, isTruncated } = await collectFiles(rootDir, config, () => true);
192
+ paths.sort((a, b) => a.localeCompare(b));
193
+ return { paths, isTruncated };
194
+ }
195
+ async function listDocumentFiles(rootDir, config) {
196
+ const { paths, isTruncated } = await collectFiles(rootDir, config, isDocumentPath);
197
+ const files = await Promise.all(
198
+ paths.map(async (path) => {
199
+ try {
200
+ return { path, mtime: (await stat(join2(rootDir, path))).mtimeMs };
201
+ } catch {
202
+ return { path, mtime: 0 };
203
+ }
204
+ })
205
+ );
206
+ files.sort((a, b) => a.path.localeCompare(b.path));
207
+ return { files, isTruncated };
208
+ }
209
+ function applyExcludes(paths, config) {
210
+ const isExcluded = createExcludeMatcher(config.exclude);
211
+ const filtered = paths.filter((path) => !isExcluded(path));
212
+ return {
213
+ paths: filtered.slice(0, config.maxFiles),
214
+ isTruncated: filtered.length > config.maxFiles
215
+ };
216
+ }
217
+
218
+ // src/server/app.ts
219
+ var VERSION = "0.1.0";
220
+ var MAX_RAW_BYTES = 10 * 1024 * 1024;
221
+ var MAX_PREVIEW_BYTES = 20 * 1024 * 1024;
222
+ var IMAGE_CONTENT_TYPES = {
223
+ ".png": "image/png",
224
+ ".jpg": "image/jpeg",
225
+ ".jpeg": "image/jpeg",
226
+ ".gif": "image/gif",
227
+ ".webp": "image/webp",
228
+ ".avif": "image/avif",
229
+ ".bmp": "image/bmp",
230
+ ".ico": "image/x-icon",
231
+ ".svg": "image/svg+xml"
232
+ };
233
+ var PREVIEW_CONTENT_TYPES = {
234
+ ...IMAGE_CONTENT_TYPES,
235
+ ".html": "text/html; charset=utf-8",
236
+ ".htm": "text/html; charset=utf-8",
237
+ ".css": "text/css; charset=utf-8",
238
+ ".js": "text/javascript; charset=utf-8",
239
+ ".mjs": "text/javascript; charset=utf-8",
240
+ ".json": "application/json; charset=utf-8",
241
+ ".txt": "text/plain; charset=utf-8",
242
+ ".md": "text/plain; charset=utf-8",
243
+ ".wasm": "application/wasm",
244
+ ".woff": "font/woff",
245
+ ".woff2": "font/woff2",
246
+ ".ttf": "font/ttf",
247
+ ".otf": "font/otf",
248
+ ".mp4": "video/mp4",
249
+ ".webm": "video/webm",
250
+ ".mp3": "audio/mpeg",
251
+ ".wav": "audio/wav"
252
+ };
253
+ function extensionOf(path) {
254
+ const index = path.lastIndexOf(".");
255
+ return index === -1 ? "" : path.slice(index).toLowerCase();
256
+ }
257
+ var rangeSchema = z.object({
258
+ target: z.string().min(1),
259
+ base: z.string().min(1),
260
+ baseMode: z.enum(["direct", "merge-base"]).optional()
261
+ });
262
+ function toRange(query) {
263
+ return { target: query.target, base: query.base, baseMode: query.baseMode };
264
+ }
265
+ function createApp(ctx) {
266
+ const app = new Hono();
267
+ app.onError((err, c) => {
268
+ console.error(err);
269
+ return c.json({ error: err instanceof Error ? err.message : "Internal error" }, 500);
270
+ });
271
+ app.use("/api/*", async (c, next) => {
272
+ const origin = c.req.header("Origin");
273
+ if (origin !== void 0 && origin !== new URL(c.req.url).origin) {
274
+ return c.json({ error: "Cross-origin request is not allowed" }, 403);
275
+ }
276
+ await next();
277
+ });
278
+ app.get("/preview/*", async (c) => {
279
+ const path = decodeURIComponent(c.req.path.slice("/preview/".length));
280
+ if (path.length === 0) return c.json({ error: "File not found" }, 404);
281
+ let buf;
282
+ try {
283
+ buf = await ctx.fileContent.readBlob("working", path);
284
+ } catch {
285
+ return c.json({ error: "File not found" }, 404);
286
+ }
287
+ if (buf.length > MAX_PREVIEW_BYTES) return c.json({ error: "File too large" }, 413);
288
+ return c.body(new Uint8Array(buf), 200, {
289
+ "Content-Type": PREVIEW_CONTENT_TYPES[extensionOf(path)] ?? "application/octet-stream",
290
+ "X-Content-Type-Options": "nosniff",
291
+ // opaque origin のページが自分のファイルを fetch できるようにする
292
+ "Access-Control-Allow-Origin": "*",
293
+ "Cache-Control": "no-store"
294
+ });
295
+ });
296
+ app.get("/api/meta", (c) => {
297
+ const meta = {
298
+ rootDir: ctx.rootDir,
299
+ isGitRepo: ctx.isGitRepo,
300
+ repoId: ctx.repoId,
301
+ initialRange: ctx.initialRange,
302
+ version: VERSION
303
+ };
304
+ return c.json(meta);
305
+ });
306
+ app.get("/api/ranges", async (c) => {
307
+ if (!ctx.gitRefs) {
308
+ const empty = { branches: [], recentCommits: [], defaultBranch: null };
309
+ return c.json(empty);
310
+ }
311
+ return c.json(await ctx.gitRefs.getRanges());
312
+ });
313
+ app.get("/api/files", async (c) => {
314
+ if (ctx.gitDiff) {
315
+ return c.json(applyExcludes(await ctx.gitDiff.listRepoFiles(), ctx.config));
316
+ }
317
+ return c.json(await listDirectoryFiles(ctx.rootDir, ctx.config));
318
+ });
319
+ app.get("/api/docs", async (c) => {
320
+ return c.json(await listDocumentFiles(ctx.rootDir, ctx.config));
321
+ });
322
+ app.get("/api/diff", zValidator("query", rangeSchema), async (c) => {
323
+ const range = toRange(c.req.valid("query"));
324
+ if (!ctx.gitDiff || range.target === "browse") {
325
+ const empty = {
326
+ files: [],
327
+ label: "Files",
328
+ resolvedBase: "browse",
329
+ resolvedTarget: "working"
330
+ };
331
+ return c.json(empty);
332
+ }
333
+ const diff = await ctx.gitDiff.getDiff(range);
334
+ ctx.tsService.warmup(diff.files.map((f) => f.path));
335
+ return c.json(diff);
336
+ });
337
+ app.post(
338
+ "/api/lang/hover",
339
+ zValidator(
340
+ "json",
341
+ z.object({
342
+ path: z.string().min(1),
343
+ line: z.number().int().min(1),
344
+ column: z.number().int().min(1),
345
+ content: z.string().optional()
346
+ })
347
+ ),
348
+ async (c) => {
349
+ return c.json(await ctx.tsService.hover(c.req.valid("json")));
350
+ }
351
+ );
352
+ app.post(
353
+ "/api/lang/definition",
354
+ zValidator(
355
+ "json",
356
+ z.object({
357
+ path: z.string().min(1),
358
+ line: z.number().int().min(1),
359
+ column: z.number().int().min(1),
360
+ content: z.string().optional()
361
+ })
362
+ ),
363
+ async (c) => {
364
+ return c.json(await ctx.tsService.definition(c.req.valid("json")));
365
+ }
366
+ );
367
+ app.post(
368
+ "/api/lang/references",
369
+ zValidator(
370
+ "json",
371
+ z.object({
372
+ path: z.string().min(1),
373
+ line: z.number().int().min(1),
374
+ column: z.number().int().min(1),
375
+ content: z.string().optional()
376
+ })
377
+ ),
378
+ async (c) => {
379
+ return c.json(await ctx.tsService.references(c.req.valid("json")));
380
+ }
381
+ );
382
+ app.post(
383
+ "/api/lint",
384
+ zValidator("json", z.object({ paths: z.array(z.string().min(1)).max(500) })),
385
+ async (c) => {
386
+ const { paths } = c.req.valid("json");
387
+ const { runEslintForFiles } = await import("../eslint-WWJOZ2KR.js");
388
+ return c.json(await runEslintForFiles(ctx.rootDir, paths));
389
+ }
390
+ );
391
+ app.post(
392
+ "/api/lang/diagnostics",
393
+ zValidator(
394
+ "json",
395
+ z.object({
396
+ path: z.string().min(1),
397
+ content: z.string().optional()
398
+ })
399
+ ),
400
+ async (c) => {
401
+ return c.json(await ctx.tsService.diagnostics(c.req.valid("json")));
402
+ }
403
+ );
404
+ app.get(
405
+ "/api/file",
406
+ zValidator(
407
+ "query",
408
+ rangeSchema.extend({
409
+ path: z.string().min(1),
410
+ oldPath: z.string().optional(),
411
+ status: z.string().default("modified")
412
+ })
413
+ ),
414
+ async (c) => {
415
+ const q = c.req.valid("query");
416
+ const range = toRange(q);
417
+ const resolvedBase = ctx.gitDiff && range.target !== "browse" ? await ctx.gitDiff.resolveBase(range) : range.base;
418
+ const contents = await ctx.fileContent.getFileContents({
419
+ range,
420
+ resolvedBase,
421
+ path: q.path,
422
+ oldPath: q.oldPath,
423
+ status: q.status
424
+ });
425
+ return c.json(contents);
426
+ }
427
+ );
428
+ app.get(
429
+ "/api/raw",
430
+ zValidator("query", z.object({ path: z.string().min(1) })),
431
+ async (c) => {
432
+ const { path } = c.req.valid("query");
433
+ const contentType = IMAGE_CONTENT_TYPES[extensionOf(path)];
434
+ if (!contentType) return c.json({ error: "Unsupported file type" }, 415);
435
+ let buf;
436
+ try {
437
+ buf = await ctx.fileContent.readBlob("working", path);
438
+ } catch {
439
+ return c.json({ error: "File not found" }, 404);
440
+ }
441
+ if (buf.length > MAX_RAW_BYTES) return c.json({ error: "File too large" }, 413);
442
+ return c.body(new Uint8Array(buf), 200, {
443
+ "Content-Type": contentType,
444
+ "X-Content-Type-Options": "nosniff",
445
+ "Content-Security-Policy": "default-src 'none'; sandbox",
446
+ "Cache-Control": "no-cache"
447
+ });
448
+ }
449
+ );
450
+ app.post(
451
+ "/api/file/save",
452
+ zValidator("json", z.object({ path: z.string().min(1), content: z.string() })),
453
+ async (c) => {
454
+ const { path, content } = c.req.valid("json");
455
+ await ctx.fileContent.writeWorkingFile(path, content);
456
+ return c.json({ ok: true });
457
+ }
458
+ );
459
+ const commentBodySchema = z.object({
460
+ path: z.string().min(1),
461
+ side: z.enum(["original", "modified"]),
462
+ // startLine/endLine 省略時はファイル全体へのコメント
463
+ startLine: z.number().int().min(1).optional(),
464
+ endLine: z.number().int().min(1).optional(),
465
+ body: z.string().min(1),
466
+ codeSnapshot: z.string().optional()
467
+ }).refine((v) => v.startLine === void 0 === (v.endLine === void 0), {
468
+ message: "startLine and endLine must be provided together"
469
+ });
470
+ app.get("/api/comments", zValidator("query", rangeSchema), (c) => {
471
+ return c.json(ctx.reviewStore.getComments(rangeKey(toRange(c.req.valid("query")))));
472
+ });
473
+ app.post(
474
+ "/api/comments",
475
+ zValidator("query", rangeSchema),
476
+ zValidator("json", commentBodySchema),
477
+ (c) => {
478
+ const key = rangeKey(toRange(c.req.valid("query")));
479
+ return c.json(ctx.reviewStore.addComment(key, c.req.valid("json")));
480
+ }
481
+ );
482
+ app.patch(
483
+ "/api/comments/:id",
484
+ zValidator("query", rangeSchema),
485
+ zValidator("json", z.object({ body: z.string().min(1) })),
486
+ (c) => {
487
+ const key = rangeKey(toRange(c.req.valid("query")));
488
+ const updated = ctx.reviewStore.updateComment(key, c.req.param("id"), c.req.valid("json").body);
489
+ if (!updated) return c.json({ error: "Comment not found" }, 404);
490
+ return c.json(updated);
491
+ }
492
+ );
493
+ app.delete("/api/comments", zValidator("query", rangeSchema), (c) => {
494
+ const key = rangeKey(toRange(c.req.valid("query")));
495
+ return c.json({ comments: ctx.reviewStore.clearComments(key) });
496
+ });
497
+ const storedCommentSchema = commentBodySchema.safeExtend({
498
+ id: z.string().min(1),
499
+ createdAt: z.string(),
500
+ updatedAt: z.string()
501
+ });
502
+ app.post(
503
+ "/api/comments/restore",
504
+ zValidator("query", rangeSchema),
505
+ zValidator("json", z.object({ comments: z.array(storedCommentSchema).max(1e3) })),
506
+ (c) => {
507
+ const key = rangeKey(toRange(c.req.valid("query")));
508
+ ctx.reviewStore.restoreComments(key, c.req.valid("json").comments);
509
+ return c.json({ ok: true });
510
+ }
511
+ );
512
+ app.delete("/api/comments/:id", zValidator("query", rangeSchema), (c) => {
513
+ const key = rangeKey(toRange(c.req.valid("query")));
514
+ const isDeleted = ctx.reviewStore.deleteComment(key, c.req.param("id"));
515
+ if (!isDeleted) return c.json({ error: "Comment not found" }, 404);
516
+ return c.json({ ok: true });
517
+ });
518
+ app.get(
519
+ "/api/events",
520
+ (c) => streamSSE(c, async (stream) => {
521
+ let isAborted = false;
522
+ const client = {
523
+ write: (event, data) => stream.writeSSE({ event, data })
524
+ };
525
+ ctx.eventBus.register(client);
526
+ stream.onAbort(() => {
527
+ isAborted = true;
528
+ ctx.eventBus.unregister(client);
529
+ });
530
+ await stream.writeSSE({ event: "connected", data: "{}" });
531
+ while (!isAborted) {
532
+ await stream.sleep(15e3);
533
+ if (isAborted) break;
534
+ await stream.writeSSE({ event: "heartbeat", data: "{}" }).catch(() => {
535
+ isAborted = true;
536
+ });
537
+ }
538
+ })
539
+ );
540
+ app.get("/api/viewed", (c) => c.json(ctx.reviewStore.getViewed()));
541
+ app.put(
542
+ "/api/viewed",
543
+ zValidator(
544
+ "json",
545
+ z.object({ path: z.string().min(1), hash: z.string().min(1), viewed: z.boolean() })
546
+ ),
547
+ (c) => {
548
+ const { path, hash, viewed } = c.req.valid("json");
549
+ ctx.reviewStore.setViewed(path, hash, viewed);
550
+ return c.json({ ok: true });
551
+ }
552
+ );
553
+ return app;
554
+ }
555
+
556
+ // src/server/context.ts
557
+ import { realpathSync as realpathSync3 } from "fs";
558
+
559
+ // src/server/files/content.ts
560
+ import { execFile } from "child_process";
561
+ import { realpathSync } from "fs";
562
+ import { readFile, writeFile } from "fs/promises";
563
+ import { resolve, sep } from "path";
564
+ import { promisify } from "util";
565
+ var execFileAsync = promisify(execFile);
566
+ var MAX_BUFFER = 10 * 1024 * 1024;
567
+ var MAX_CONTENT_BYTES = 2 * 1024 * 1024;
568
+ var MAX_CONTENT_LINES = 2e4;
569
+ var FileContent = class {
570
+ constructor(rootDir, isGitRepo) {
571
+ this.rootDir = rootDir;
572
+ this.isGitRepo = isGitRepo;
573
+ }
574
+ rootDir;
575
+ isGitRepo;
576
+ /**
577
+ * RangeSpec から original/modified 両側の参照 ref を決める。
578
+ * - browse: 比較なし (modified 側のみ)
579
+ * - working: index vs working tree
580
+ * - staged: base vs index
581
+ * - '.': base vs working tree
582
+ * - commit: base vs target
583
+ */
584
+ resolveSideRefs(range, resolvedBase) {
585
+ if (range.target === "browse") return { originalRef: null, modifiedRef: "working" };
586
+ if (range.target === "working") return { originalRef: "staged", modifiedRef: "working" };
587
+ if (range.target === "staged") return { originalRef: resolvedBase, modifiedRef: "staged" };
588
+ if (range.target === ".") return { originalRef: resolvedBase, modifiedRef: "working" };
589
+ return { originalRef: resolvedBase, modifiedRef: range.target };
590
+ }
591
+ async getFileContents(params) {
592
+ const { range, resolvedBase, path, oldPath, status } = params;
593
+ const refs = this.resolveSideRefs(range, resolvedBase);
594
+ const [original, modified] = await Promise.all([
595
+ refs.originalRef === null || status === "added" ? null : this.readSide(refs.originalRef, oldPath ?? path),
596
+ status === "deleted" ? null : this.readSide(refs.modifiedRef, path)
597
+ ]);
598
+ const isTooLarge = isTooLargeContent(original) || isTooLargeContent(modified);
599
+ if (isTooLarge) {
600
+ return {
601
+ original: original ? { ...original, content: "" } : null,
602
+ modified: modified ? { ...modified, content: "" } : null,
603
+ isTooLarge: true
604
+ };
605
+ }
606
+ return { original, modified, isTooLarge: false };
607
+ }
608
+ async readSide(ref, filepath) {
609
+ try {
610
+ const buf = await this.readBlob(ref, filepath);
611
+ return { content: buf.toString("utf8"), ref };
612
+ } catch {
613
+ return null;
614
+ }
615
+ }
616
+ /** working tree の既存ファイルを上書き保存する (編集機能用) */
617
+ async writeWorkingFile(filepath, content) {
618
+ await writeFile(this.resolveInsideRoot(filepath), content, "utf8");
619
+ }
620
+ async readBlob(ref, filepath) {
621
+ if (ref === "working") {
622
+ return readFile(this.resolveInsideRoot(filepath));
623
+ }
624
+ if (!this.isGitRepo) {
625
+ throw new Error("Not a git repository");
626
+ }
627
+ const rel = normalizeRelPath(filepath);
628
+ if (ref === "staged") {
629
+ const { stdout: stdout2 } = await execFileAsync("git", ["show", `:${rel}`], {
630
+ cwd: this.rootDir,
631
+ maxBuffer: MAX_BUFFER,
632
+ encoding: "buffer"
633
+ });
634
+ return stdout2;
635
+ }
636
+ const { stdout: hashOut } = await execFileAsync("git", ["rev-parse", `${ref}:${rel}`], {
637
+ cwd: this.rootDir,
638
+ maxBuffer: MAX_BUFFER
639
+ });
640
+ const { stdout } = await execFileAsync("git", ["cat-file", "blob", hashOut.trim()], {
641
+ cwd: this.rootDir,
642
+ maxBuffer: MAX_BUFFER,
643
+ encoding: "buffer"
644
+ });
645
+ return stdout;
646
+ }
647
+ /** ルート配下の既存ファイルの絶対パスを返す (realpathSync は存在しないパスで throw する) */
648
+ resolveInsideRoot(filepath) {
649
+ const rootDir = realpathSync(resolve(this.rootDir));
650
+ const abs = realpathSync(resolve(rootDir, normalizeRelPath(filepath)));
651
+ if (abs !== rootDir && !abs.startsWith(`${rootDir}${sep}`)) {
652
+ throw new Error("File path outside the viewer root");
653
+ }
654
+ return abs;
655
+ }
656
+ };
657
+ function normalizeRelPath(filepath) {
658
+ if (filepath.length === 0) throw new Error("Invalid file path");
659
+ const normalized = filepath.replace(/\\/g, "/");
660
+ if (normalized.startsWith("/") || normalized.split("/").includes("..")) {
661
+ throw new Error("File path outside the viewer root");
662
+ }
663
+ return normalized;
664
+ }
665
+ function isTooLargeContent(side) {
666
+ if (!side) return false;
667
+ if (Buffer.byteLength(side.content, "utf8") > MAX_CONTENT_BYTES) return true;
668
+ let lineCount = 1;
669
+ for (let i = 0; i < side.content.length; i++) {
670
+ if (side.content.charCodeAt(i) === 10) lineCount++;
671
+ if (lineCount > MAX_CONTENT_LINES) return true;
672
+ }
673
+ return false;
674
+ }
675
+
676
+ // src/server/git/diff.ts
677
+ import { createHash } from "crypto";
678
+ import { readFileSync as readFileSync2 } from "fs";
679
+ import { resolve as resolve2 } from "path";
680
+ import { simpleGit } from "simple-git";
681
+ var MAX_UNTRACKED_FILE_BYTES = 2 * 1024 * 1024;
682
+ var GitDiff = class {
683
+ constructor(repoRoot) {
684
+ this.repoRoot = repoRoot;
685
+ this.git = simpleGit(repoRoot);
686
+ }
687
+ repoRoot;
688
+ git;
689
+ async resolveBase(range) {
690
+ if (range.baseMode !== "merge-base") return range.base;
691
+ const target = range.target === "working" || range.target === "staged" || range.target === "." ? "HEAD" : range.target;
692
+ const out = await this.git.raw(["merge-base", target, range.base]);
693
+ return out.trim();
694
+ }
695
+ async getDiff(range) {
696
+ const base = await this.resolveBase(range);
697
+ let diffArgs;
698
+ let label;
699
+ let resolvedBase = base;
700
+ let resolvedTarget = range.target;
701
+ if (range.target === "working") {
702
+ diffArgs = [];
703
+ label = "Working Directory (unstaged changes)";
704
+ resolvedBase = "staged";
705
+ } else if (range.target === "staged") {
706
+ const baseHash = shortHash(await this.git.revparse([base]));
707
+ diffArgs = ["--cached", base];
708
+ label = `${baseHash} vs Staging Area`;
709
+ resolvedBase = baseHash;
710
+ } else if (range.target === ".") {
711
+ const baseHash = shortHash(await this.git.revparse([base]));
712
+ diffArgs = [base];
713
+ label = `${baseHash} vs Working Directory`;
714
+ resolvedBase = baseHash;
715
+ resolvedTarget = "working";
716
+ } else {
717
+ const targetHash = shortHash(await this.git.revparse([range.target]));
718
+ const baseHash = shortHash(await this.git.revparse([base]));
719
+ diffArgs = [baseHash, targetHash];
720
+ label = `${baseHash}..${targetHash}`;
721
+ resolvedBase = baseHash;
722
+ resolvedTarget = targetHash;
723
+ }
724
+ diffArgs.push("--no-ext-diff", "--color=never");
725
+ const raw = await this.git.diff(diffArgs);
726
+ const files = parseUnifiedDiff(raw);
727
+ if (range.target === "working" || range.target === ".") {
728
+ files.push(...await this.getUntrackedFiles(new Set(files.map((f) => f.path))));
729
+ }
730
+ files.sort((a, b) => a.path.localeCompare(b.path));
731
+ return { files, label, resolvedBase, resolvedTarget };
732
+ }
733
+ /** untracked ファイルを added 扱いで diff に含める */
734
+ async getUntrackedFiles(known) {
735
+ const out = await this.git.raw(["ls-files", "--others", "--exclude-standard", "-z"]);
736
+ const paths = out.split("\0").filter((p) => p.length > 0 && !known.has(p));
737
+ const result = [];
738
+ for (const path of paths) {
739
+ try {
740
+ const buf = readFileSync2(resolve2(this.repoRoot, path));
741
+ if (buf.length > MAX_UNTRACKED_FILE_BYTES) continue;
742
+ const isBinary = buf.includes(0);
743
+ const text = isBinary ? "" : buf.toString("utf8");
744
+ const lines = text.length === 0 ? 0 : text.split("\n").filter((l, i, a) => i < a.length - 1 || l !== "").length;
745
+ result.push({
746
+ path,
747
+ status: "added",
748
+ additions: isBinary ? 0 : lines,
749
+ deletions: 0,
750
+ isBinary,
751
+ contentHash: sha256(`untracked:${path}:${sha256(buf.toString("base64"))}`)
752
+ });
753
+ } catch {
754
+ }
755
+ }
756
+ return result;
757
+ }
758
+ /** tracked + untracked (gitignore 除外) の全ファイル一覧 (Quick Open 用) */
759
+ async listRepoFiles() {
760
+ const [tracked, untracked] = await Promise.all([
761
+ this.git.raw(["ls-files", "-z"]),
762
+ this.git.raw(["ls-files", "--others", "--exclude-standard", "-z"])
763
+ ]);
764
+ const paths = /* @__PURE__ */ new Set();
765
+ for (const out of [tracked, untracked]) {
766
+ for (const path of out.split("\0")) {
767
+ if (path.length > 0) paths.add(path);
768
+ }
769
+ }
770
+ return [...paths].sort((a, b) => a.localeCompare(b));
771
+ }
772
+ async validateRef(ref) {
773
+ if (ref === "." || ref === "working" || ref === "staged") return true;
774
+ try {
775
+ await this.git.revparse(["--verify", `${ref}^{commit}`]);
776
+ return true;
777
+ } catch {
778
+ return false;
779
+ }
780
+ }
781
+ };
782
+ function shortHash(hash) {
783
+ return hash.trim().slice(0, 7);
784
+ }
785
+ function sha256(input) {
786
+ return createHash("sha256").update(input).digest("hex");
787
+ }
788
+ function parseUnifiedDiff(diffText) {
789
+ const files = [];
790
+ const blocks = diffText.split(/^diff --git /m).slice(1);
791
+ for (const block of blocks) {
792
+ const file = parseFileBlock(`diff --git ${block}`);
793
+ if (file) files.push(file);
794
+ }
795
+ return files;
796
+ }
797
+ function parseFileBlock(block) {
798
+ const lines = block.split("\n");
799
+ const headerPaths = parseDiffHeaderPaths(lines[0] ?? "");
800
+ const minusLine = lines.find((l) => l.startsWith("--- "));
801
+ const plusLine = lines.find((l) => l.startsWith("+++ "));
802
+ const renameFrom = extractPath(lines.find((l) => l.startsWith("rename from ")), "rename from ");
803
+ const renameTo = extractPath(lines.find((l) => l.startsWith("rename to ")), "rename to ");
804
+ const plusPath = extractPath(plusLine, "+++ ");
805
+ const minusPath = extractPath(minusLine, "--- ");
806
+ const newPath = renameTo ?? plusPath ?? headerPaths?.newPath;
807
+ const oldPath = renameFrom ?? minusPath ?? headerPaths?.oldPath ?? newPath;
808
+ const path = newPath ?? oldPath;
809
+ if (!path) return null;
810
+ let status = "modified";
811
+ if (lines.some((l) => l.startsWith("new file mode")) || minusLine?.includes("/dev/null")) {
812
+ status = "added";
813
+ } else if (lines.some((l) => l.startsWith("deleted file mode")) || plusLine?.includes("/dev/null")) {
814
+ status = "deleted";
815
+ } else if (oldPath !== void 0 && newPath !== void 0 && oldPath !== newPath) {
816
+ status = "renamed";
817
+ }
818
+ const isBinary = lines.some((l) => l.startsWith("Binary files ") || l === "GIT binary patch");
819
+ let additions = 0;
820
+ let deletions = 0;
821
+ let inChunk = false;
822
+ for (const line of lines) {
823
+ if (line.startsWith("@@")) {
824
+ inChunk = true;
825
+ continue;
826
+ }
827
+ if (!inChunk) continue;
828
+ if (line.startsWith("+")) additions++;
829
+ else if (line.startsWith("-")) deletions++;
830
+ }
831
+ return {
832
+ path,
833
+ oldPath: status === "renamed" ? oldPath : void 0,
834
+ status,
835
+ additions,
836
+ deletions,
837
+ isBinary,
838
+ contentHash: sha256(block)
839
+ };
840
+ }
841
+ function parseDiffHeaderPaths(headerLine) {
842
+ if (!headerLine.startsWith("diff --git ")) return null;
843
+ const raw = headerLine.slice("diff --git ".length);
844
+ const segments = [];
845
+ let current = "";
846
+ let isInQuotes = false;
847
+ for (let i = 0; i < raw.length; i++) {
848
+ const char = raw[i];
849
+ const prev = i > 0 ? raw[i - 1] : null;
850
+ if (char === '"' && prev !== "\\") {
851
+ isInQuotes = !isInQuotes;
852
+ current += char;
853
+ continue;
854
+ }
855
+ if (char === " " && !isInQuotes && prev !== "\\") {
856
+ if (current) {
857
+ segments.push(current);
858
+ current = "";
859
+ }
860
+ continue;
861
+ }
862
+ current += char;
863
+ }
864
+ if (current) segments.push(current);
865
+ if (segments.length !== 2) return null;
866
+ return {
867
+ oldPath: decodeGitPath(segments[0]),
868
+ newPath: decodeGitPath(segments[1])
869
+ };
870
+ }
871
+ function extractPath(line, prefix) {
872
+ if (!line?.startsWith(prefix)) return void 0;
873
+ return decodeGitPath(line.slice(prefix.length));
874
+ }
875
+ function decodeGitPath(rawPath) {
876
+ if (typeof rawPath !== "string") return void 0;
877
+ const trimmed = rawPath.startsWith('"') && rawPath.endsWith('"') ? rawPath.slice(1, -1) : rawPath;
878
+ let path = trimmed;
879
+ for (const prefix of ["a/", "b/", "c/", "i/", "w/"]) {
880
+ if (path.startsWith(prefix)) {
881
+ path = path.slice(prefix.length);
882
+ break;
883
+ }
884
+ }
885
+ const tabIndex = path.indexOf(" ");
886
+ if (tabIndex !== -1) path = path.slice(0, tabIndex);
887
+ if (path === "/dev/null") return void 0;
888
+ const bytes = [];
889
+ for (let i = 0; i < path.length; i++) {
890
+ const char = path[i];
891
+ if (char === "\\" && i + 1 < path.length) {
892
+ const next = path[i + 1];
893
+ if (/[0-7]/.test(next)) {
894
+ let octal = next;
895
+ let read = 1;
896
+ while (read < 3 && i + 1 + read < path.length && /[0-7]/.test(path[i + 1 + read])) {
897
+ octal += path[i + 1 + read];
898
+ read++;
899
+ }
900
+ bytes.push(parseInt(octal, 8));
901
+ i += read;
902
+ continue;
903
+ }
904
+ const escapes = {
905
+ t: 9,
906
+ n: 10,
907
+ r: 13,
908
+ b: 8,
909
+ f: 12,
910
+ v: 11,
911
+ a: 7,
912
+ "\\": 92,
913
+ '"': 34,
914
+ " ": 32
915
+ };
916
+ if (next in escapes) {
917
+ bytes.push(escapes[next]);
918
+ } else {
919
+ bytes.push(...Buffer.from(next, "utf8"));
920
+ }
921
+ i++;
922
+ continue;
923
+ }
924
+ bytes.push(...Buffer.from(char, "utf8"));
925
+ }
926
+ return Buffer.from(bytes).toString("utf8");
927
+ }
928
+
929
+ // src/server/git/refs.ts
930
+ import { simpleGit as simpleGit2 } from "simple-git";
931
+ var GitRefs = class {
932
+ git;
933
+ constructor(repoRoot) {
934
+ this.git = simpleGit2(repoRoot);
935
+ }
936
+ async getRanges() {
937
+ const [branches, log, defaultBranch] = await Promise.all([
938
+ this.getBranches(),
939
+ this.getRecentCommits(),
940
+ this.getDefaultBranch()
941
+ ]);
942
+ return { branches, recentCommits: log, defaultBranch };
943
+ }
944
+ async getBranches() {
945
+ const summary = await this.git.branch(["--sort=-committerdate"]);
946
+ return summary.all.filter((b) => !b.startsWith("remotes/") || b.startsWith("remotes/origin/"));
947
+ }
948
+ async getRecentCommits() {
949
+ const log = await this.git.log({ maxCount: 50 });
950
+ return log.all.map((c) => ({
951
+ sha: c.hash,
952
+ shortSha: c.hash.slice(0, 7),
953
+ subject: c.message,
954
+ date: c.date
955
+ }));
956
+ }
957
+ async getDefaultBranch() {
958
+ try {
959
+ const out = await this.git.raw(["symbolic-ref", "refs/remotes/origin/HEAD"]);
960
+ return out.trim().replace("refs/remotes/", "");
961
+ } catch {
962
+ for (const name of ["main", "master"]) {
963
+ try {
964
+ await this.git.revparse(["--verify", `refs/heads/${name}`]);
965
+ return name;
966
+ } catch {
967
+ }
968
+ }
969
+ return null;
970
+ }
971
+ }
972
+ };
973
+
974
+ // src/server/events.ts
975
+ var EventBus = class {
976
+ constructor(options) {
977
+ this.options = options;
978
+ }
979
+ options;
980
+ clients = /* @__PURE__ */ new Set();
981
+ shutdownTimer = null;
982
+ register(client) {
983
+ this.clients.add(client);
984
+ if (this.shutdownTimer) {
985
+ clearTimeout(this.shutdownTimer);
986
+ this.shutdownTimer = null;
987
+ }
988
+ }
989
+ unregister(client) {
990
+ this.clients.delete(client);
991
+ if (this.clients.size === 0 && !this.options.isKeepAlive) {
992
+ this.shutdownTimer = setTimeout(() => {
993
+ if (this.clients.size === 0) {
994
+ console.log("kaleido: all clients disconnected, shutting down");
995
+ if (this.options.onShutdown) {
996
+ this.options.onShutdown();
997
+ } else {
998
+ process.exit(0);
999
+ }
1000
+ }
1001
+ }, 5e3);
1002
+ }
1003
+ }
1004
+ broadcast(event) {
1005
+ for (const client of this.clients) {
1006
+ void client.write(event.type, JSON.stringify(event)).catch(() => {
1007
+ this.clients.delete(client);
1008
+ });
1009
+ }
1010
+ }
1011
+ };
1012
+
1013
+ // src/server/store/reviewStore.ts
1014
+ import { createHash as createHash2 } from "crypto";
1015
+ import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "fs";
1016
+ import { realpathSync as realpathSync2 } from "fs";
1017
+ import { join as join3 } from "path";
1018
+ import envPaths from "env-paths";
1019
+ import { nanoid } from "nanoid";
1020
+ function computeRepoId(repoRoot) {
1021
+ return createHash2("sha256").update(realpathSync2(repoRoot)).digest("hex").slice(0, 16);
1022
+ }
1023
+ function openReviewStore(repoRoot) {
1024
+ const realRoot = realpathSync2(repoRoot);
1025
+ return new ReviewStore(computeRepoId(realRoot), realRoot);
1026
+ }
1027
+ var STORE_VERSION = 1;
1028
+ var ReviewStore = class {
1029
+ baseDir;
1030
+ constructor(repoId, repoRoot) {
1031
+ this.baseDir = join3(envPaths("kaleido", { suffix: "" }).data, "reviews", repoId);
1032
+ mkdirSync(join3(this.baseDir, "comments"), { recursive: true });
1033
+ this.writeAtomic(join3(this.baseDir, "meta.json"), { repoRoot });
1034
+ }
1035
+ readJson(path) {
1036
+ try {
1037
+ return JSON.parse(readFileSync3(path, "utf8"));
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+ writeAtomic(path, data) {
1043
+ const tmp = `${path}.tmp-${process.pid}`;
1044
+ writeFileSync(tmp, JSON.stringify(data, null, 2));
1045
+ renameSync(tmp, path);
1046
+ }
1047
+ // --- viewed ---
1048
+ get viewedPath() {
1049
+ return join3(this.baseDir, "viewed.json");
1050
+ }
1051
+ getViewed() {
1052
+ const data = this.readJson(this.viewedPath);
1053
+ if (!data || data.version !== STORE_VERSION) return { entries: {} };
1054
+ return { entries: data.entries };
1055
+ }
1056
+ setViewed(path, hash, isViewed) {
1057
+ const state = this.getViewed();
1058
+ if (isViewed) {
1059
+ state.entries[path] = { hash, viewedAt: (/* @__PURE__ */ new Date()).toISOString() };
1060
+ } else {
1061
+ delete state.entries[path];
1062
+ }
1063
+ this.writeAtomic(this.viewedPath, { version: STORE_VERSION, entries: state.entries });
1064
+ }
1065
+ // --- comments ---
1066
+ commentsPath(rangeKey2) {
1067
+ const safe = rangeKey2.replace(/[^a-zA-Z0-9_.~-]/g, "_");
1068
+ return join3(this.baseDir, "comments", `${safe}.json`);
1069
+ }
1070
+ getComments(rangeKey2) {
1071
+ const data = this.readJson(this.commentsPath(rangeKey2));
1072
+ if (!data || data.version !== STORE_VERSION) return [];
1073
+ return data.comments;
1074
+ }
1075
+ saveComments(rangeKey2, comments) {
1076
+ this.writeAtomic(this.commentsPath(rangeKey2), { version: STORE_VERSION, comments });
1077
+ }
1078
+ addComment(rangeKey2, req) {
1079
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1080
+ const comment = { id: nanoid(10), ...req, createdAt: now, updatedAt: now };
1081
+ const comments = this.getComments(rangeKey2);
1082
+ comments.push(comment);
1083
+ this.saveComments(rangeKey2, comments);
1084
+ return comment;
1085
+ }
1086
+ updateComment(rangeKey2, id, body) {
1087
+ const comments = this.getComments(rangeKey2);
1088
+ const target = comments.find((c) => c.id === id);
1089
+ if (!target) return null;
1090
+ target.body = body;
1091
+ target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1092
+ this.saveComments(rangeKey2, comments);
1093
+ return target;
1094
+ }
1095
+ /** 範囲のコメントを全削除し、削除したものを返す (クライアント側 Undo 用) */
1096
+ clearComments(rangeKey2) {
1097
+ const comments = this.getComments(rangeKey2);
1098
+ this.saveComments(rangeKey2, []);
1099
+ return comments;
1100
+ }
1101
+ /** クリアの Undo 用。id 重複は既存を優先して復元する */
1102
+ restoreComments(rangeKey2, comments) {
1103
+ const existing = this.getComments(rangeKey2);
1104
+ const existingIds = new Set(existing.map((c) => c.id));
1105
+ const restored = [...existing, ...comments.filter((c) => !existingIds.has(c.id))];
1106
+ this.saveComments(rangeKey2, restored);
1107
+ }
1108
+ deleteComment(rangeKey2, id) {
1109
+ const comments = this.getComments(rangeKey2);
1110
+ const next = comments.filter((c) => c.id !== id);
1111
+ if (next.length === comments.length) return false;
1112
+ this.saveComments(rangeKey2, next);
1113
+ return true;
1114
+ }
1115
+ };
1116
+
1117
+ // src/server/ts/workerClient.ts
1118
+ import { Worker } from "worker_threads";
1119
+ var CALL_TIMEOUT_MS = 3e4;
1120
+ function resolveWorker() {
1121
+ if (import.meta.url.endsWith(".ts")) {
1122
+ return { url: new URL("./worker.ts", import.meta.url), execArgv: ["--import", "tsx"] };
1123
+ }
1124
+ return { url: new URL("../server/ts/worker.js", import.meta.url), execArgv: [] };
1125
+ }
1126
+ var TsWorkerClient = class {
1127
+ worker;
1128
+ nextId = 1;
1129
+ pending = /* @__PURE__ */ new Map();
1130
+ constructor(repoRoot) {
1131
+ const { url, execArgv } = resolveWorker();
1132
+ this.worker = new Worker(url, { workerData: { repoRoot }, execArgv });
1133
+ this.worker.unref();
1134
+ this.worker.on("message", (message) => {
1135
+ const call = this.pending.get(message.id);
1136
+ if (!call) return;
1137
+ this.pending.delete(message.id);
1138
+ if (message.error !== void 0) {
1139
+ call.reject(new Error(message.error));
1140
+ } else {
1141
+ call.resolve(message.result);
1142
+ }
1143
+ });
1144
+ this.worker.on("error", (error) => {
1145
+ console.error("[kaleido] ts worker error:", error);
1146
+ const wrapped = error instanceof Error ? error : new Error(String(error));
1147
+ for (const call of this.pending.values()) call.reject(wrapped);
1148
+ this.pending.clear();
1149
+ });
1150
+ this.worker.on("exit", (code) => {
1151
+ const error = new Error(`ts worker exited with code ${code}`);
1152
+ for (const call of this.pending.values()) call.reject(error);
1153
+ this.pending.clear();
1154
+ });
1155
+ }
1156
+ call(method, params) {
1157
+ const id = this.nextId++;
1158
+ return new Promise((resolve4, reject) => {
1159
+ const timer = setTimeout(() => {
1160
+ this.pending.delete(id);
1161
+ reject(new Error(`ts worker call timed out: ${method}`));
1162
+ }, CALL_TIMEOUT_MS);
1163
+ this.pending.set(id, {
1164
+ resolve: (value) => {
1165
+ clearTimeout(timer);
1166
+ resolve4(value);
1167
+ },
1168
+ reject: (reason) => {
1169
+ clearTimeout(timer);
1170
+ reject(reason);
1171
+ }
1172
+ });
1173
+ this.worker.postMessage({ id, method, params });
1174
+ });
1175
+ }
1176
+ hover(params) {
1177
+ return this.call("hover", params);
1178
+ }
1179
+ diagnostics(params) {
1180
+ return this.call("diagnostics", params);
1181
+ }
1182
+ definition(params) {
1183
+ return this.call("definition", params);
1184
+ }
1185
+ references(params) {
1186
+ return this.call("references", params);
1187
+ }
1188
+ warmup(paths) {
1189
+ this.worker.postMessage({ method: "warmup", params: { paths } });
1190
+ }
1191
+ invalidateFile(path) {
1192
+ this.worker.postMessage({ method: "invalidateFile", params: { path } });
1193
+ }
1194
+ };
1195
+
1196
+ // src/server/context.ts
1197
+ function createAppContext(rootDir, initialRange, options) {
1198
+ const realRoot = realpathSync3(rootDir);
1199
+ const repoId = computeRepoId(realRoot);
1200
+ return {
1201
+ rootDir: realRoot,
1202
+ repoId,
1203
+ isGitRepo: options.isGitRepo,
1204
+ initialRange,
1205
+ config: loadViewerConfig(realRoot, options.excludes),
1206
+ gitDiff: options.isGitRepo ? new GitDiff(realRoot) : null,
1207
+ gitRefs: options.isGitRepo ? new GitRefs(realRoot) : null,
1208
+ fileContent: new FileContent(realRoot, options.isGitRepo),
1209
+ tsService: new TsWorkerClient(realRoot),
1210
+ reviewStore: new ReviewStore(repoId, realRoot),
1211
+ eventBus: new EventBus({ isKeepAlive: options.isKeepAlive, onShutdown: options.onShutdown })
1212
+ };
1213
+ }
1214
+
1215
+ // src/server/watcher.ts
1216
+ import { relative, sep as sep2 } from "path";
1217
+ import watcher from "@parcel/watcher";
1218
+ var DEBOUNCE_MS = 300;
1219
+ async function startWatcher(rootDir, onChange) {
1220
+ let pending = /* @__PURE__ */ new Set();
1221
+ let timer = null;
1222
+ const flush = () => {
1223
+ timer = null;
1224
+ const paths = [...pending];
1225
+ pending = /* @__PURE__ */ new Set();
1226
+ if (paths.length > 0) onChange(paths);
1227
+ };
1228
+ const subscription = await watcher.subscribe(
1229
+ rootDir,
1230
+ (err, events) => {
1231
+ if (err) return;
1232
+ for (const event of events) {
1233
+ const rel = relative(rootDir, event.path).split(sep2).join("/");
1234
+ if (rel.startsWith(".git/")) {
1235
+ if (rel !== ".git/HEAD" && rel !== ".git/index") continue;
1236
+ }
1237
+ pending.add(rel);
1238
+ }
1239
+ if (pending.size > 0 && !timer) {
1240
+ timer = setTimeout(flush, DEBOUNCE_MS);
1241
+ }
1242
+ },
1243
+ {
1244
+ ignore: [
1245
+ "**/node_modules/**",
1246
+ ".git/objects/**",
1247
+ ".git/logs/**",
1248
+ ".git/refs/**",
1249
+ "**/.DS_Store"
1250
+ ]
1251
+ }
1252
+ );
1253
+ return () => subscription.unsubscribe();
1254
+ }
1255
+
1256
+ // src/server/index.ts
1257
+ async function startServer(options) {
1258
+ const ctx = createAppContext(options.rootDir, options.initialRange, {
1259
+ isGitRepo: options.isGitRepo,
1260
+ isKeepAlive: options.isKeepAlive,
1261
+ excludes: options.excludes,
1262
+ onShutdown: options.onShutdown
1263
+ });
1264
+ const app = createApp(ctx);
1265
+ void startWatcher(ctx.rootDir, (paths) => {
1266
+ for (const path of paths) {
1267
+ ctx.tsService.invalidateFile(path);
1268
+ }
1269
+ ctx.eventBus.broadcast({ type: "files-changed" });
1270
+ }).catch((error) => {
1271
+ console.warn("kaleido: file watcher unavailable:", error);
1272
+ });
1273
+ const clientDir = join4(dirname(fileURLToPath(import.meta.url)), "..", "client");
1274
+ if (existsSync(join4(clientDir, "index.html"))) {
1275
+ app.use("/*", serveStatic({ root: relativizeForServeStatic(clientDir) }));
1276
+ app.get("*", serveStatic({ path: join4(relativizeForServeStatic(clientDir), "index.html") }));
1277
+ }
1278
+ const port = await findAvailablePort(options.port, options.host);
1279
+ const server = serve({ fetch: app.fetch, port, hostname: options.host });
1280
+ const url = `http://${options.host === "0.0.0.0" ? "localhost" : options.host}:${port}`;
1281
+ return { port, url, server };
1282
+ }
1283
+ function relativizeForServeStatic(absDir) {
1284
+ return relative2(process.cwd(), absDir) || ".";
1285
+ }
1286
+ async function findAvailablePort(startPort, host) {
1287
+ const { createServer } = await import("net");
1288
+ for (let port = startPort; port < startPort + 100; port++) {
1289
+ const isFree = await new Promise((resolvePromise) => {
1290
+ const probe = createServer();
1291
+ probe.once("error", () => resolvePromise(false));
1292
+ probe.once("listening", () => probe.close(() => resolvePromise(true)));
1293
+ probe.listen(port, host);
1294
+ });
1295
+ if (isFree) return port;
1296
+ }
1297
+ throw new Error(`No available port found in range ${startPort}-${startPort + 99}`);
1298
+ }
1299
+
1300
+ // src/cli/args.ts
1301
+ import { execFileSync, execSync } from "child_process";
1302
+ import { realpathSync as realpathSync4, statSync } from "fs";
1303
+ import { resolve as resolve3 } from "path";
1304
+ function findGitRoot(cwd) {
1305
+ try {
1306
+ return execSync("git rev-parse --show-toplevel", {
1307
+ cwd,
1308
+ encoding: "utf8",
1309
+ stdio: ["ignore", "pipe", "pipe"]
1310
+ }).trim();
1311
+ } catch {
1312
+ return null;
1313
+ }
1314
+ }
1315
+ function isCommitish(gitRoot, ref) {
1316
+ try {
1317
+ execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
1318
+ cwd: gitRoot,
1319
+ stdio: "ignore"
1320
+ });
1321
+ return true;
1322
+ } catch {
1323
+ return false;
1324
+ }
1325
+ }
1326
+ function isDirectory(path) {
1327
+ try {
1328
+ return statSync(path).isDirectory();
1329
+ } catch {
1330
+ return false;
1331
+ }
1332
+ }
1333
+ function resolveViewerTarget(params) {
1334
+ const { cwd, target, base } = params;
1335
+ if (params.dir !== void 0) {
1336
+ const dir = resolve3(cwd, params.dir);
1337
+ if (!isDirectory(dir)) throw new Error(`Not a directory: ${params.dir}`);
1338
+ return forExplicitRoot(dir, target, base);
1339
+ }
1340
+ const gitRoot = findGitRoot(cwd);
1341
+ if (target !== void 0 && target !== "working" && target !== "staged" && target !== "." && !(gitRoot !== null && isCommitish(gitRoot, target)) && isDirectory(resolve3(cwd, target))) {
1342
+ if (base !== void 0) {
1343
+ throw new Error("A base cannot be given when opening a directory");
1344
+ }
1345
+ return forExplicitRoot(resolve3(cwd, target), void 0, void 0);
1346
+ }
1347
+ if (gitRoot === null) {
1348
+ if (target !== void 0) {
1349
+ throw new Error(
1350
+ `Not a git repository, and no such directory: ${target}
1351
+ Run kaleido without arguments to browse the current directory.`
1352
+ );
1353
+ }
1354
+ return { rootDir: cwd, isGitRepo: false, range: BROWSE_RANGE };
1355
+ }
1356
+ return {
1357
+ rootDir: gitRoot,
1358
+ isGitRepo: true,
1359
+ range: target ? resolveRange(target, base) : resolveDefaultRange(gitRoot)
1360
+ };
1361
+ }
1362
+ function forExplicitRoot(dir, target, base) {
1363
+ const gitRoot = findGitRoot(dir);
1364
+ if (gitRoot === null || realpathSync4(gitRoot) !== realpathSync4(dir)) {
1365
+ if (target !== void 0) {
1366
+ throw new Error(`${dir} is not a git repository root; ranges are unavailable`);
1367
+ }
1368
+ return { rootDir: dir, isGitRepo: false, range: BROWSE_RANGE };
1369
+ }
1370
+ return {
1371
+ rootDir: gitRoot,
1372
+ isGitRepo: true,
1373
+ range: target ? resolveRange(target, base) : resolveDefaultRange(gitRoot)
1374
+ };
1375
+ }
1376
+ function resolveRange(target, base) {
1377
+ if (target === "working") {
1378
+ return { target: "working", base: "HEAD" };
1379
+ }
1380
+ if (target === "staged" || target === ".") {
1381
+ return { target, base: base ?? "HEAD" };
1382
+ }
1383
+ if (base) {
1384
+ return { target, base };
1385
+ }
1386
+ return { target, base: `${target}^` };
1387
+ }
1388
+ function resolveDefaultRange(rootDir) {
1389
+ const succeeds = (command) => {
1390
+ try {
1391
+ execSync(command, { cwd: rootDir, stdio: ["ignore", "pipe", "pipe"] });
1392
+ return true;
1393
+ } catch {
1394
+ return false;
1395
+ }
1396
+ };
1397
+ if (!succeeds("git diff --cached --quiet")) {
1398
+ return { target: "staged", base: "HEAD" };
1399
+ }
1400
+ const hasUnstaged = !succeeds("git diff --quiet");
1401
+ const hasUntracked = execSync("git ls-files --others --exclude-standard", {
1402
+ cwd: rootDir,
1403
+ encoding: "utf8",
1404
+ stdio: ["ignore", "pipe", "pipe"]
1405
+ }).trim().length > 0;
1406
+ if (hasUnstaged || hasUntracked) {
1407
+ return { target: ".", base: "HEAD" };
1408
+ }
1409
+ if (succeeds("git rev-parse --verify HEAD^")) {
1410
+ return { target: "HEAD", base: "HEAD^" };
1411
+ }
1412
+ return { target: ".", base: "HEAD" };
1413
+ }
1414
+
1415
+ // src/cli/index.ts
1416
+ var program = new Command();
1417
+ var collect = (value, previous) => [...previous, value];
1418
+ program.name("kaleido").description("Monaco-based local web file viewer with TypeScript hover, diff and review tools").version("0.1.0");
1419
+ program.command("serve", { isDefault: true }).description("start the viewer (default command)").argument(
1420
+ "[target]",
1421
+ 'directory to browse, commit-ish to view, or "working" / "staged" / "." (default: auto-detect)'
1422
+ ).argument("[base]", "commit-ish to compare against (default: HEAD or <target>^)").option("--port <port>", "preferred port (auto-increments if taken)", "4890").option("--host <host>", "host to bind", "127.0.0.1").option("--dir <path>", "directory to open (default: current directory)").option("--repo <path>", "alias of --dir").option("--exclude <pattern>", "extra path pattern to hide from the file tree", collect, []).option("--no-open", "do not open the browser").option("--no-keep-alive", "shut down the server when all browser tabs are closed").action(async (target, base, options) => {
1423
+ try {
1424
+ const { rootDir, isGitRepo, range } = resolveViewerTarget({
1425
+ cwd: process.cwd(),
1426
+ dir: options.dir ?? options.repo,
1427
+ target,
1428
+ base
1429
+ });
1430
+ const printCommentsAndExit = () => {
1431
+ try {
1432
+ const comments = openReviewStore(rootDir).getComments(rangeKey(range));
1433
+ if (comments.length > 0) {
1434
+ console.log(`
1435
+ ${comments.length} comment(s) for ${describeRange(range)}:
1436
+ `);
1437
+ console.log(formatAllCommentsPrompt(comments));
1438
+ }
1439
+ } catch {
1440
+ }
1441
+ process.exit(0);
1442
+ };
1443
+ const { url } = await startServer({
1444
+ rootDir,
1445
+ isGitRepo,
1446
+ initialRange: range,
1447
+ excludes: options.exclude,
1448
+ port: parseInt(options.port, 10),
1449
+ host: options.host,
1450
+ isKeepAlive: options.keepAlive,
1451
+ onShutdown: printCommentsAndExit
1452
+ });
1453
+ process.on("SIGINT", printCommentsAndExit);
1454
+ process.on("SIGTERM", printCommentsAndExit);
1455
+ console.log(`kaleido: ${describeRange(range)} @ ${rootDir}`);
1456
+ console.log(` ${url}`);
1457
+ if (options.open) {
1458
+ await open(url);
1459
+ }
1460
+ } catch (error) {
1461
+ console.error(error instanceof Error ? error.message : error);
1462
+ process.exit(1);
1463
+ }
1464
+ });
1465
+ program.command("comments").description("print saved comments for a range as an AI prompt").argument("[target]", 'commit-ish, or "working" / "staged" / "." (default: ".")').argument("[base]", "commit-ish to compare against (default: HEAD or <target>^)").option("--dir <path>", "directory to read comments for (default: current directory)").option("--repo <path>", "alias of --dir").option("--json", "output as JSON instead of prompt text").action((target, base, options) => {
1466
+ try {
1467
+ const { rootDir, range } = resolveViewerTarget({
1468
+ cwd: process.cwd(),
1469
+ dir: options.dir ?? options.repo,
1470
+ target,
1471
+ base
1472
+ });
1473
+ const comments = openReviewStore(rootDir).getComments(rangeKey(range));
1474
+ if (options.json) {
1475
+ console.log(JSON.stringify(comments, null, 2));
1476
+ return;
1477
+ }
1478
+ if (comments.length === 0) {
1479
+ console.error(`No comments for ${describeRange(range)}`);
1480
+ process.exitCode = 1;
1481
+ return;
1482
+ }
1483
+ console.log(formatAllCommentsPrompt(comments));
1484
+ } catch (error) {
1485
+ console.error(error instanceof Error ? error.message : error);
1486
+ process.exit(1);
1487
+ }
1488
+ });
1489
+ program.parse();