deel-local-cli 1.20.13 → 2.0.1

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 (155) hide show
  1. package/README.ko.md +97 -78
  2. package/README.md +120 -81
  3. package/bin/deel.js +398 -32
  4. package/package.json +2 -1
  5. package/src/acp/jsonrpc.js +87 -7
  6. package/src/acp/map.js +23 -3
  7. package/src/acp/serve.js +333 -25
  8. package/src/agent/agents.js +110 -12
  9. package/src/agent/askcheck.js +50 -6
  10. package/src/agent/asks.js +59 -5
  11. package/src/agent/budget.js +9 -2
  12. package/src/agent/card.js +8 -2
  13. package/src/agent/commit.js +168 -16
  14. package/src/agent/compact.js +93 -21
  15. package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
  16. package/src/agent/effort.js +85 -15
  17. package/src/agent/evidence.js +63 -7
  18. package/src/agent/evolve.js +132 -24
  19. package/src/agent/grade.js +12 -3
  20. package/src/agent/loop.js +408 -81
  21. package/src/agent/memory.js +176 -29
  22. package/src/agent/mention.js +22 -5
  23. package/src/agent/models.js +31 -7
  24. package/src/agent/modes.js +25 -10
  25. package/src/agent/outschema.js +381 -56
  26. package/src/agent/pins.js +33 -2
  27. package/src/agent/project.js +73 -8
  28. package/src/agent/recall.js +40 -9
  29. package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
  30. package/src/agent/review.js +143 -13
  31. package/src/agent/route.js +253 -26
  32. package/src/agent/salvage.js +2 -1
  33. package/src/agent/session.js +457 -31
  34. package/src/agent/store.js +232 -22
  35. package/src/agent/threads.js +52 -3
  36. package/src/backend/adapter.js +508 -85
  37. package/src/backend/azure.js +12 -1
  38. package/src/backend/cachemark.js +31 -5
  39. package/src/backend/clientcert.js +23 -1
  40. package/src/backend/ctxsize.js +56 -9
  41. package/src/backend/detect.js +252 -19
  42. package/src/backend/http.js +383 -42
  43. package/src/backend/learn.js +101 -11
  44. package/src/backend/mcp.js +333 -24
  45. package/src/backend/price.js +61 -13
  46. package/src/backend/probe.js +188 -36
  47. package/src/backend/proxy.js +108 -14
  48. package/src/backend/quota.js +226 -30
  49. package/src/backend/retry.js +46 -3
  50. package/src/backend/scan.js +47 -10
  51. package/src/backend/scanui.js +57 -7
  52. package/src/backend/toolfit.js +204 -22
  53. package/src/backend/vision.js +14 -1
  54. package/src/backend/wire.js +58 -10
  55. package/src/cmdnames.js +16 -0
  56. package/src/commands/common.js +70 -0
  57. package/src/commands/extend.js +299 -0
  58. package/src/commands/model.js +851 -0
  59. package/src/commands/view.js +328 -0
  60. package/src/commands/work.js +823 -0
  61. package/src/commands.js +193 -2082
  62. package/src/completion.js +45 -3
  63. package/src/config.js +454 -41
  64. package/src/configexplain.js +127 -15
  65. package/src/doctor.js +164 -27
  66. package/src/i18n/en.js +22 -0
  67. package/src/i18n/index.js +31 -1
  68. package/src/i18n/ja.js +25 -1
  69. package/src/i18n/ko.js +25 -2
  70. package/src/i18n/zh.js +25 -1
  71. package/src/lsp/client.js +111 -9
  72. package/src/lsp/servers.js +86 -22
  73. package/src/oneshot.js +325 -65
  74. package/src/pack/sbom.js +28 -5
  75. package/src/pack/selfpack.js +54 -5
  76. package/src/pack/sheet.en.js +24 -3
  77. package/src/pack/tar.js +45 -8
  78. package/src/pack/zip.js +103 -8
  79. package/src/plugins/manage.js +165 -14
  80. package/src/preview/serve.js +239 -23
  81. package/src/providers/bedrock.js +30 -5
  82. package/src/providers/gemini.js +8 -0
  83. package/src/providers/index.js +17 -1
  84. package/src/repl.js +254 -61
  85. package/src/report.js +7 -2
  86. package/src/reset.js +151 -25
  87. package/src/safety/audit.js +108 -8
  88. package/src/safety/authcmd.js +75 -5
  89. package/src/safety/guard.js +531 -15
  90. package/src/safety/hooks.js +172 -20
  91. package/src/safety/keystore.js +178 -18
  92. package/src/safety/network.js +209 -6
  93. package/src/safety/policy.js +377 -36
  94. package/src/safety/runmode.js +9 -2
  95. package/src/safety/secrets.js +243 -17
  96. package/src/safety/shellenv.js +64 -3
  97. package/src/safety/trust.js +298 -32
  98. package/src/safety/undo.js +409 -17
  99. package/src/setup.js +175 -17
  100. package/src/skills/discover.js +122 -19
  101. package/src/stats.js +57 -16
  102. package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
  103. package/src/tools/clipboard.js +53 -20
  104. package/src/tools/convert.js +118 -37
  105. package/src/tools/desc.en.js +48 -5
  106. package/src/tools/doc2md.js +36 -4
  107. package/src/tools/docs.js +94 -18
  108. package/src/tools/edit-match.js +212 -13
  109. package/src/tools/encoding.js +525 -28
  110. package/src/tools/excel-com.js +63 -15
  111. package/src/tools/excel.js +12 -7
  112. package/src/tools/fastgrep.js +501 -38
  113. package/src/tools/fig.js +21 -4
  114. package/src/tools/fsutil.js +235 -21
  115. package/src/tools/hwpxwrite.js +80 -19
  116. package/src/tools/ignore.js +55 -7
  117. package/src/tools/index.js +1083 -148
  118. package/src/tools/jobs.js +328 -50
  119. package/src/tools/kiwi.js +50 -5
  120. package/src/tools/lsp.js +164 -32
  121. package/src/tools/outline.js +52 -12
  122. package/src/tools/pdf.js +367 -59
  123. package/src/tools/shell.js +12 -4
  124. package/src/tools/spawn.js +203 -18
  125. package/src/tools/todo.js +73 -4
  126. package/src/tools/verify.js +328 -45
  127. package/src/tools/webfetch.js +194 -25
  128. package/src/tools/xlsx.js +232 -31
  129. package/src/ui/ansi.js +209 -15
  130. package/src/ui/banner.js +2 -1
  131. package/src/ui/complete.js +46 -4
  132. package/src/ui/diff.js +4 -2
  133. package/src/ui/export.js +91 -17
  134. package/src/ui/inputbox.js +58 -18
  135. package/src/ui/intro.js +9 -2
  136. package/src/ui/level.js +32 -7
  137. package/src/ui/md.js +101 -15
  138. package/src/ui/motion.js +5 -2
  139. package/src/ui/notify.js +7 -1
  140. package/src/ui/office.js +37 -5
  141. package/src/ui/pastechip.js +3 -2
  142. package/src/ui/pick.js +85 -12
  143. package/src/ui/prompt.js +176 -13
  144. package/src/ui/screen.js +2 -1
  145. package/src/ui/spinner.js +6 -1
  146. package/src/ui/status.js +80 -12
  147. package/src/ui/wrap.js +23 -37
  148. /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
  149. /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
  150. /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
  151. /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
  152. /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
  153. /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
  154. /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
  155. /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
@@ -24,8 +24,13 @@
24
24
  // 결과가 없는 것이 아니라 우리가 못 물어본 것이다. 그때 '일치 없음' 을
25
25
  // 돌려주면 사람은 없는 줄 안다. 그래서 실패는 실패로 두고 JS 로 다시 찾는다.
26
26
  import { spawnSync } from 'node:child_process';
27
+ import { statSync } from 'node:fs';
27
28
  import { 돌려보기 } from './spawn.js';
28
- import { join } from 'node:path';
29
+ import { SKIP_DIRS, glob거르개 } from './fsutil.js';
30
+ import { join, relative, sep, isAbsolute } from 'node:path';
31
+
32
+ // 이보다 큰 파일은 안 본다 — 자바스크립트 길(GREP_MAX_FILE)과 rg(--max-filesize 2M)와 같은 값.
33
+ const 큰파일 = 2 * 1024 * 1024;
29
34
 
30
35
  /*
31
36
  * 정규식으로 찾을 것이 없는 파일들.
@@ -47,14 +52,49 @@ export const 안볼확장자 = [
47
52
  'map', 'min.js', 'min.css', 'lock',
48
53
  ];
49
54
 
50
- /** 위 목록으로 만든 정규식. 자바스크립트 길이 쓴다. */
51
- export const 안볼정규식 = new RegExp(`\\.(${안볼확장자.map((x) => x.replace(/\./g, '\\.')).join('|')})$`, 'i');
55
+ /**
56
+ * 위 목록으로 만든 정규식. 자바스크립트 길이 쓴다.
57
+ *
58
+ * 점만 벗기고 있었다. 지금 목록에는 점 말고 특수한 글자가 없어서 탈이 안 났지만,
59
+ * `c++` 같은 갈래 이름을 한 줄 더하는 날 그 줄이 조용히 **다른 무늬**가 된다.
60
+ * 목록은 사람이 늘리는 자리니 벗기는 쪽을 온전하게 둔다.
61
+ */
62
+ const 정규식글자벗기기 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
63
+ export const 안볼정규식 = new RegExp(`\\.(${안볼확장자.map(정규식글자벗기기).join('|')})$`, 'i');
52
64
 
53
65
  /** 위 목록으로 만든 rg 옵션. 대소문자를 안 가린다(정규식의 `i` 와 맞춘다). */
54
66
  export function 안볼글로브() {
55
67
  return 안볼확장자.flatMap((x) => ['--iglob', `!*.${x}`]);
56
68
  }
57
69
 
70
+ /*
71
+ * ── 두 길이 **같은 자리를 본다** ───────────────────────────────────────
72
+ *
73
+ * 안 볼 확장자는 한 벌로 맞춰 놨는데, 정작 **어느 폴더·어느 파일을 도느냐**
74
+ * 는 두 길이 따로 정하고 있었다.
75
+ *
76
+ * rg : 점으로 시작하는 것을 기본으로 안 본다. 대신 dist·build·venv 는 돈다.
77
+ * walk: 점 규칙이 아예 없다(다 본다). 대신 SKIP_DIRS 를 안 돈다.
78
+ *
79
+ * 그래서 같은 폴더에서 같은 무늬로 찾았는데 —
80
+ *
81
+ * rg 가 있는 PC → dist/bundle.js · build/out.js · venv/lib.js
82
+ * rg 가 없는 PC → .github/workflows/ci.yml · .env.example · .eslintrc.json
83
+ *
84
+ * 겹치는 답이 src/a.js 하나뿐이었다(실제로 재 봤다). 「CI 설정 어디서 고쳐」
85
+ * 를 rg 가 깔린 PC 에서 물으면 한 줄도 안 나오고, 모델은 그 침묵을 사실로
86
+ * 받아 「이 저장소에는 워크플로가 없습니다」 로 답을 맺는다. 오류도 안 나고
87
+ * 꼬리말도 안 붙으니 사람은 PC 차이라는 것을 영영 모른다.
88
+ *
89
+ * 이 파일 머리말이 목록을 한 벌만 두자고 적어 둔 까닭이 그것이다. 이제
90
+ * 점 파일은 rg 도 보고(`--hidden`), 안 볼 폴더는 walk 가 쓰는 그 목록을
91
+ * 그대로 옮겨 적는다. `.git` 도 그 목록에 있어서 같이 빠진다 — 점 파일을
92
+ * 보기 시작하면 제일 먼저 걸리는 것이 저장소 살림이다.
93
+ */
94
+ export function 안볼폴더글로브() {
95
+ return [...SKIP_DIRS].flatMap((d) => ['--iglob', `!${d}/`]);
96
+ }
97
+
58
98
  /*
59
99
  * 있는지 한 번만 본다.
60
100
  *
@@ -72,6 +112,8 @@ export function 엔진찾기({ 다시 = false, env = process.env } = {}) {
72
112
  if (본것 && !다시) return 본것;
73
113
  // 끄고 싶을 때가 있다 — 결과가 다르다고 의심될 때 같은 자리에서 견주려면.
74
114
  if (env.DEEL_GREP === 'js') { 본것 = { rg: false, gitgrep: false, 왜: 'DEEL_GREP=js 로 꺼 두었습니다' }; return 본것; }
115
+ // rg 가 깔린 PC 에서 git grep 길을 재 보려면 rg 만 끈다 — 안 그러면 그 길은 rg 없는 PC 에서만 돈다.
116
+ if (env.DEEL_GREP === 'git') { 본것 = { rg: false, gitgrep: 있나('git', ['--version']), 왜: 'DEEL_GREP=git 로 rg 를 꺼 두었습니다' }; return 본것; }
75
117
  본것 = {
76
118
  rg: 있나('rg', ['--version']),
77
119
  gitgrep: 있나('git', ['--version']),
@@ -100,12 +142,237 @@ export function 저장소인가(폴더) {
100
142
  *
101
143
  * @returns {{ok:true, 줄들:string[], 잘림:boolean} | {ok:false, 왜:string}}
102
144
  */
103
- export async function rg로찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, timeout = 20000, signal = null }) {
145
+ export async function rg로찾기({ 무늬, 자리, 뿌리 = null, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, timeout = 20000, signal = null, 부르기 = 돌려보기 }) {
104
146
  const 인자 = [
105
147
  '--line-number',
106
148
  '--no-heading',
107
149
  '--with-filename',
150
+ // 경로 끝을 콜론 대신 NUL 로 찍는다 — 리눅스·맥의 파일 이름에는 `:숫자:` 가 들 수 있다 (아래 줄가르기).
151
+ '--null',
108
152
  '--color', 'never',
153
+ /*
154
+ * CRLF 파일의 `$` 를 줄 끝으로 본다.
155
+ *
156
+ * 이게 없으면 rg 는 `\n` 만 줄 끝으로 알아서 `end$` 가 `end\r\n` 에 안 맞았다.
157
+ * 자바스크립트 길도 줄마다 `\r` 을 달고 있어서 두 길 다 윈도우 파일에서
158
+ * 「일치 없음」 이었다. 두 길을 같이 고친다 (tools/index.js 의 한파일에서찾기).
159
+ */
160
+ '--crlf',
161
+ ...rg거르는인자({ glob, 무시파일, 자리, 뿌리 }),
162
+ ];
163
+ if (대소문자무시) 인자.push('--ignore-case');
164
+ // `--` 뒤로 넘겨서 무늬가 옵션으로 안 읽히게 한다. `-foo` 같은 무늬가 실제로 있다.
165
+ const 덤 = rg작업폴더(뿌리, { glob, 무시파일 });
166
+ const { 찾을, 되붙이기 } = rg찾을자리(뿌리, 자리, 덤);
167
+ 인자.push('--regexp', 무늬, '--', 찾을);
168
+
169
+ const r = await 부르기('rg', 인자, { timeout, signal, 덤 });
170
+ const 못준까닭 = rg가못준까닭(r);
171
+ if (못준까닭) return { ok: false, 왜: 못준까닭 };
172
+ // 경로는 첫 NUL 앞이다(--null). 그 앞만 원래 뿌리에 되붙인다.
173
+ const 줄들 = String(r.stdout ?? '').split('\n').filter(Boolean).map((줄) => {
174
+ const 경로끝 = 줄.indexOf('\0');
175
+ return 경로끝 < 0 ? 줄 : 되붙이기(줄.slice(0, 경로끝)) + 줄.slice(경로끝);
176
+ });
177
+ return { ok: true, 줄들: 줄들.slice(0, 최대), 잘림: 줄들.length > 최대 };
178
+ }
179
+
180
+ /**
181
+ * rg 가 **답을 준 것인가.** 아니면 왜 못 줬는지.
182
+ *
183
+ * rg 의 끝맺음은 셋뿐이다 — 0(찾음) · 1(못 찾음) · 2(무늬·자리가 틀림).
184
+ * 1 은 성공(빈 결과)이고, **그 밖은 전부 「우리가 못 물어본 것」** 이다.
185
+ *
186
+ * ── 왜 `status === 2` 로는 모자랐나 ────────────────────────────────────
187
+ *
188
+ * 여기가 그 한 줄이었다. 그런데 **신호로 죽은** 자식은 status 가 숫자가
189
+ * 아니라 `null` 이다 — spawn.js 의 'close' 가 종료 코드를 그대로 주는데,
190
+ * 신호로 끊긴 프로세스에는 그 코드가 없다. error 도 비어 있다(우리가 죽인
191
+ * 것이 아니니까). OOM killer 가 걷어가거나, 사람이 kill 하거나, 컨테이너가
192
+ * 메모리 상한으로 끊는 자리가 다 그렇다.
193
+ *
194
+ * 그 자리에서 이 자는 `ok: true` 를 올려 보냈다. stdout 은 반 토막이거나
195
+ * 비어 있는데 부르는 쪽은 그걸 **다 찾아본 결과**로 받는다 — 화면에는
196
+ * 「일치 없음」. 이 파일 머리말 3번이 「결과가 없는 것이 아니라 우리가 못
197
+ * 물어본 것」 이라고 적어 둔 바로 그 자리에서, 못 물어본 것이 없는 것으로
198
+ * 나갔다. 실패로 두면 부르는 쪽이 자바스크립트 길로 내려가 다시 찾는다.
199
+ *
200
+ * @returns {string|null} 못 준 까닭. 답을 줬으면 null.
201
+ */
202
+ export function rg가못준까닭(r) {
203
+ if (r?.error) return r.error.message;
204
+ if (r?.status === 0 || r?.status === 1) return null;
205
+ const 첫줄 = String(r?.stderr ?? '').split('\n')[0];
206
+ if (r?.status === 2) return 첫줄 || 'rg 가 무늬를 못 읽었습니다';
207
+ if (r?.status == null) return 첫줄 || 'rg 가 끝맺지 못하고 죽었습니다 (신호로 끊겼습니다)';
208
+ return 첫줄 || `rg 가 ${r.status} 로 끝났습니다`;
209
+ }
210
+
211
+ /*
212
+ * ── rg 가 **글자로 못 푸는** 파일 ─────────────────────────────────────────
213
+ *
214
+ * rg 는 UTF-8 과 표식 있는 UTF-16 만 글자로 푼다. 그 밖은 바이트 그대로 보거나
215
+ * (CP949 · Shift_JIS · 표식 없는 UTF-16) NUL 을 만나는 순간 그 파일을 버린다.
216
+ * 자바스크립트 길은 그 셋을 다 읽는다(encoding.js 가 알아보고 풀고, 바이너리
217
+ * 판정은 앞 8,000바이트만 본다). 그래서 같은 폴더에서 「결재」 를 찾으면 —
218
+ *
219
+ * rg 가 있는 PC → src/a.js
220
+ * rg 가 없는 PC → src/a.js · 사내문서.txt(CP949) · out.txt(UTF-16) · 로그.txt
221
+ *
222
+ * 이 파일 머리말이 「같은 명령이 PC 마다 다른 답을 내면 빠른 게 아니다」 라고
223
+ * 세 번 적어 둔 바로 그 고장이, 하필 사내 문서에서 났다.
224
+ *
225
+ * rg 에 그 인코딩을 가르칠 수는 없다(내용을 보고 고르는 것은 우리 쪽 일이다).
226
+ * 대신 **어느 파일이 그런지**는 rg 가 빨리 안다 — NUL 이나 UTF-8 에 없는
227
+ * 바이트열이 하나라도 든 파일을 한 번 더 훑어 이름만 받는다. 그 파일들은 rg 의
228
+ * 답을 버리고 자바스크립트 길과 **똑같은 방법으로** 다시 읽는다(Grep 도구가 한다).
229
+ *
230
+ * 무늬는 바이트로 본다(`(?-u)`). 한 줄 안에서 이런 것이 보이면 UTF-8 이 아니다:
231
+ * NUL · UTF-8 에 절대 안 나오는 바이트 · 앞 바이트 없는 이음 바이트 ·
232
+ * 이음이 모자란 앞 바이트 · 이음이 남는 글자 · 너무 길게 쓴 것과 서로게이트.
233
+ * 표식 있는 UTF-16 은 rg 가 먼저 UTF-8 로 풀어서 여기 안 걸린다 — 맞다, rg 가
234
+ * 그건 제대로 찾는다.
235
+ */
236
+ const 이음 = '[\\x80-\\xBF]';
237
+ const 이음아님 = '(?:[^\\x80-\\xBF]|$)';
238
+ export const 못푸는바이트무늬 = '(?-u:'
239
+ + [
240
+ '\\x00',
241
+ '[\\xC0\\xC1\\xF5-\\xFF]',
242
+ `(?:^|[\\x00-\\x7F])${이음}`,
243
+ `[\\xC2-\\xDF]${이음아님}`,
244
+ `[\\xE0-\\xEF](?:${이음아님}|${이음}${이음아님})`,
245
+ `[\\xF0-\\xF4](?:${이음아님}|${이음}${이음아님}|${이음}${이음}${이음아님})`,
246
+ `[\\xC2-\\xDF]${이음}{2}`,
247
+ `[\\xE0-\\xEF]${이음}{3}`,
248
+ `[\\xF0-\\xF4]${이음}{4}`,
249
+ '\\xE0[\\x80-\\x9F]', '\\xED[\\xA0-\\xBF]', '\\xF0[\\x80-\\x8F]', '\\xF4[\\x90-\\xBF]',
250
+ ].join('|')
251
+ + ')';
252
+
253
+ /** rg 가 글자로 못 푸는 파일 이름들. 거르는 규칙은 rg로찾기 와 **같은 것**을 쓴다. */
254
+ /*
255
+ * `부르기` 를 밖에서 받는 까닭은 **검사 때문**이다 (tools/clipboard.js 와 같은 규칙).
256
+ * 신호로 죽은 rg · 2 가 아닌 종료코드는 진짜 rg 로는 만들 수가 없어서, 그 갈래가
257
+ * 영영 안 재진다. 여기가 조용히 빈 목록을 올리면 사내 문서가 검색에서 통째로
258
+ * 사라지는 자리라, 재지는 조각을 내놓는 것이 맞다.
259
+ */
260
+ export async function rg못푸는파일({ 자리, 뿌리 = null, glob = null, 무시파일 = null, timeout = 20000, signal = null, 부르기 = 돌려보기 }) {
261
+ const 인자 = [
262
+ '--files-with-matches',
263
+ '--color', 'never',
264
+ // NUL 이 든 파일도 끝까지 본다. 안 그러면 rg 가 그 파일을 버려서 이름도 안 나온다.
265
+ '--text',
266
+ ...rg거르는인자({ glob, 무시파일, 자리, 뿌리 }),
267
+ ];
268
+ const 덤 = rg작업폴더(뿌리, { glob, 무시파일 });
269
+ const { 찾을, 되붙이기 } = rg찾을자리(뿌리, 자리, 덤);
270
+ 인자.push('--regexp', 못푸는바이트무늬, '--', 찾을);
271
+ const r = await 부르기('rg', 인자, { timeout, signal, 덤 });
272
+ /*
273
+ * 실패는 **rg가못준까닭 한 자로** 가른다. 여기는 `error` 와 `status === 2` 두 줄뿐이라,
274
+ * 신호로 죽거나(`status === null`) 2 가 아닌 값으로 끝나면 **빈 목록이 `ok:true`** 로
275
+ * 올라갔다. 그러면 빠르게찾기 가 「따로 볼 파일 없음」 으로 받아, CP949·Shift_JIS·
276
+ * 표식 없는 UTF-16 파일을 한 개도 되읽지 않는다 — rg 가 그 파일에서 낸 답은 버려졌는데
277
+ * 다시 읽지도 않으니 결과가 조용히 「일치 없음」 이 된다. 이 함수 머리말이 「거르는
278
+ * 규칙은 rg로찾기 와 같은 것을 쓴다」 고 적어 두고 여기만 옛 두 줄이었다.
279
+ */
280
+ const 못준까닭 = rg가못준까닭(r);
281
+ if (못준까닭) return { ok: false, 왜: 못준까닭 };
282
+ const 파일들 = String(r.stdout ?? '').split('\n').map((l) => l.replace(/\r$/, '')).filter(Boolean).map(되붙이기);
283
+ return { ok: true, 파일들 };
284
+ }
285
+
286
+ /**
287
+ * 어느 파일·폴더를 보나 — rg 를 부르는 **모든** 자리가 이 한 벌을 쓴다.
288
+ * 두 번 부르는 rg(찾기 · 못 푸는 파일 세기)가 서로 다른 파일을 보면
289
+ * 거기서 또 답이 갈린다.
290
+ */
291
+ /*
292
+ * ── rg 는 glob 을 **제 작업 폴더** 기준으로 맞춘다 ──────────────────────────
293
+ *
294
+ * `--glob src/*.js` 와 `--ignore-file` 의 무늬는 rg 를 띄운 작업 폴더에서부터
295
+ * 읽힌다 — 찾을 자리가 아니다. deel 은 작업 폴더를 안 정하고 띄워서, 사람이
296
+ * 다른 폴더에서 deel 을 켰거나 검사처럼 뿌리와 다른 자리에서 돌면 빗금 든
297
+ * glob 이 한 파일에도 안 맞았다(오류 없이 「일치 없음」). .deelignore 의
298
+ * `src/secret.txt` 같은 줄도 같은 까닭으로 안 먹었다.
299
+ *
300
+ * 작업 폴더를 작업 범위의 뿌리로 정한다. .deelignore 가 사는 자리도 거기다.
301
+ *
302
+ * glob 도 .deelignore 도 없으면 **안 바꾼다.** 윈도우는 떠 있는 프로세스의
303
+ * 작업 폴더를 못 지운다. 멈추라고 해서 죽인 rg 는 몇 ms 더 살아 있고, 그동안
304
+ * 그 폴더가 잠긴다 — test/abort-tools.test.js 가 바로 그 자리에서 EPERM 으로
305
+ * 잡았다. 작업 폴더가 뜻을 갖는 것은 위 두 무늬뿐이라 그때만 옮긴다.
306
+ */
307
+ function rg작업폴더(뿌리, { glob = null, 무시파일 = null } = {}) {
308
+ return 뿌리 && (glob || 무시파일) ? { cwd: 뿌리 } : {};
309
+ }
310
+
311
+ /*
312
+ * ── 작업 폴더를 정했으면 찾을 자리도 **그 기준 상대경로로** 넘긴다 (2.0.1) ──
313
+ *
314
+ * rg 는 glob 을 작업 폴더 기준으로 맞춘다. 그러려면 찾을 자리가 작업 폴더
315
+ * **밑으로 보여야** 한다 — 그 판단은 글자로 한다. 그런데 윈도우에서는 같은
316
+ * 폴더를 두 가지로 적을 수 있다:
317
+ *
318
+ * C:\Users\RUNNER~1\AppData\Local\Temp (8.3 짧은 이름 — os.tmpdir() 가 준다)
319
+ * C:\Users\runneradmin\AppData\Local\Temp (긴 이름 — 같은 폴더)
320
+ *
321
+ * Chocolatey·Scoop 은 rg 를 **심(shim)** 으로 깐다. 재 보니 심을 거치면 작업
322
+ * 폴더가 긴 이름으로 바뀌어 진짜 rg 가 뜬다. 우리는 찾을 자리를 짧은 이름으로
323
+ * 넘겼으니 rg 눈에는 그 자리가 작업 폴더 밑이 아니고, 빗금 든 glob 이 한 파일에도
324
+ * 안 맞았다 — 오류 없이 「일치 없음」, `!` 빼기는 아무것도 안 뺐다.
325
+ *
326
+ * 둘 중 하나만 있어서는 안 깨진다(윈도 러너에서 갈라 봤다):
327
+ *
328
+ * 심 + 짧은 이름 → 5개 빨강
329
+ * 진짜 rg.exe + 짧은 이름 → 121 통과
330
+ * 심 + 긴 이름 → 121 통과
331
+ *
332
+ * 윈도우에서 rg 는 대개 choco·scoop 으로 깔고, 짧은 이름은 사용자 이름이 여덟
333
+ * 글자를 넘거나 빈칸이 들면 %TEMP% 에 그대로 온다. 흔한 짝이다.
334
+ *
335
+ * 그래서 이름 적는 방식에 기대지 않는다. 찾을 자리를 **뿌리 기준 상대경로**로
336
+ * 넘기면 rg 는 제 작업 폴더에서부터 걸어 들어가고, 작업 폴더를 어떻게 적었든
337
+ * 상관이 없다. 나온 경로는 **원래 뿌리에 되붙여** 돌려준다 — 아래로 흐르는
338
+ * 경로 꼴은 예전과 똑같다.
339
+ *
340
+ * 작업 폴더를 안 정하는 판(glob·무시파일 없음)과, 찾을 자리가 뿌리 밖인 판은
341
+ * 예전대로 절대경로를 넘긴다.
342
+ */
343
+ function rg찾을자리(뿌리, 자리, 덤) {
344
+ const 그대로 = { 찾을: 자리, 되붙이기: (f) => f };
345
+ if (!덤?.cwd || !뿌리 || !자리) return 그대로;
346
+ const 상대 = relative(뿌리, 자리);
347
+ if (isAbsolute(상대) || 상대 === '..' || 상대.startsWith(`..${sep}`)) return 그대로;
348
+ return { 찾을: 상대 || '.', 되붙이기: (f) => (isAbsolute(f) ? f : join(뿌리, f)) };
349
+ }
350
+
351
+ /*
352
+ * glob 하나를 rg 인자로. 규칙은 fsutil.js 의 glob거르개 와 **같다**.
353
+ *
354
+ * 작업 폴더가 뿌리이므로 빗금 든 무늬는 뿌리 기준으로 맞는다. 찾는 폴더가
355
+ * 뿌리 아래(path=src)면 「찾는 폴더 기준」 으로도 맞아야 해서 그 앞머리를 붙인
356
+ * 무늬를 하나 더 준다 — rg 는 여러 --glob 을 「하나라도 맞으면」 으로 읽고,
357
+ * `!` 빼기는 「하나라도 맞으면 뺀다」 로 읽는다. 자바스크립트 쪽과 같은 뜻이다.
358
+ * `/` 로 시작하는 무늬는 뿌리에 묶인 것이라 더 안 붙인다.
359
+ */
360
+ export function rg글로브들(glob, { 자리 = null, 뿌리 = null } = {}) {
361
+ if (!glob) return [];
362
+ const 빼기 = glob.startsWith('!');
363
+ const 몸 = 빼기 ? glob.slice(1) : glob;
364
+ const 무늬들 = [몸];
365
+ if (!몸.startsWith('/') && 몸.includes('/') && 뿌리 && 자리) {
366
+ const 앞 = relative(뿌리, 자리);
367
+ if (앞 && 앞 !== '..' && !앞.startsWith(`..${sep}`) && !isAbsolute(앞)) {
368
+ 무늬들.push(`${앞.split(sep).join('/')}/${몸}`);
369
+ }
370
+ }
371
+ return 무늬들.map((x) => (빼기 ? `!${x}` : x));
372
+ }
373
+
374
+ function rg거르는인자({ glob = null, 무시파일 = null, 자리 = null, 뿌리 = null } = {}) {
375
+ const 인자 = [
109
376
  // 아주 큰 파일은 안 본다(JS 길과 같은 규칙). 글이 아닌 파일은 rg 가 기본으로 건너뛴다.
110
377
  '--max-filesize', '2M',
111
378
  /*
@@ -130,13 +397,23 @@ export async function rg로찾기({ 무늬, 자리, glob = null, 대소문자무
130
397
  * 같은 명령이 PC 에 따라 다른 답을 내고, 하필 **가리라고 적은 것을 더
131
398
  * 보는 쪽**으로 갈린다. 이 파일 머리말이 없애려던 바로 그 고장이다.
132
399
  * 빠른 길이 다른 답을 내면 그건 빠른 게 아니다.
400
+ *
401
+ * ── 두 자를 **둘 다** 줘야 한다 ────────────────────────────────────
402
+ *
403
+ * `--glob-case-insensitive` 는 **`--glob` 으로 우리가 준 무늬**에만 먹는다.
404
+ * `.gitignore`·`.deelignore` 규칙에는 안 먹는다. 그래서 고친 줄 알고 넘어간
405
+ * 자리에서 그 고장이 그대로 남아 있었다 — 재 보니 `*.tmpx` 를 적어 둔
406
+ * 폴더에서 `A.TMPX` 를 rg 길만 검색했다(자바스크립트 길은 안 봤다).
407
+ * 규칙 파일 쪽 대소문자는 이 자가 따로 정한다.
133
408
  */
134
- ...(process.platform === 'win32' ? ['--glob-case-insensitive'] : []),
409
+ ...(process.platform === 'win32' ? ['--glob-case-insensitive', '--ignore-file-case-insensitive'] : []),
410
+ // 점으로 시작하는 것도 본다 — 자바스크립트 길은 처음부터 다 보고 있었다.
411
+ // 무엇이 빠졌었는지는 위 안볼폴더글로브() 에 적어 두었다.
412
+ '--hidden',
135
413
  ];
136
- if (대소문자무시) 인자.push('--ignore-case');
137
- if (glob) 인자.push('--glob', glob);
138
- // 자바스크립트 길이 안 여는 파일은 rg 도 안 열게 한다. 같은 목록에서 나온다.
139
- 인자.push(...안볼글로브());
414
+ for (const g of rg글로브들(glob, { 자리, 뿌리 })) 인자.push('--glob', g);
415
+ // 자바스크립트 길이 안 여는 파일·폴더는 rg 도 안 열게 한다. 같은 목록에서 나온다.
416
+ 인자.push(...안볼글로브(), ...안볼폴더글로브());
140
417
  /*
141
418
  * .deelignore 도 지켜야 한다.
142
419
  *
@@ -145,36 +422,179 @@ export async function rg로찾기({ 무늬, 자리, glob = null, 대소문자무
145
422
  * 같은 명령이 PC 마다 다른 답을 내는 셈이라, 이건 빠른 것보다 나쁘다.
146
423
  */
147
424
  if (무시파일) 인자.push('--ignore-file', 무시파일);
148
- // `--` 뒤로 넘겨서 무늬가 옵션으로 안 읽히게 한다. `-foo` 같은 무늬가 실제로 있다.
149
- 인자.push('--regexp', 무늬, '--', 자리);
425
+ return 인자;
426
+ }
150
427
 
151
- const r = await 돌려보기('rg', 인자, { timeout, signal });
152
- // rg 는 못 찾으면 1, 진짜 탈이 나면 2 를 준다. 1 은 성공(빈 결과)이다.
153
- if (r.error) return { ok: false, 왜: r.error.message };
154
- if (r.status === 2) return { ok: false, 왜: (r.stderr ?? '').split('\n')[0] || 'rg 가 무늬를 못 읽었습니다' };
155
- const 줄들 = String(r.stdout ?? '').split('\n').filter(Boolean);
156
- return { ok: true, 줄들: 줄들.slice(0, 최대), 잘림: 줄들.length > 최대 };
428
+ /*
429
+ * ── git grep 에는 **파일만 추리게** 한다 ────────────────────────────────
430
+ *
431
+ * 여기서 사람의 정규식을 그대로 `git grep -E` 에 넘기고 있었다. 네 가지가 샜다:
432
+ *
433
+ * · ERE 에는 `\d` 가 없다 — `foo\d+` 가 조용히 다른 무늬로 읽혔다
434
+ * · 기본은 **git 이 아는 파일만** 본다 — 방금 만든 파일은 한 줄도 안 나왔다
435
+ * · CP949 · 표식 없는 UTF-16 · 8KB 뒤 NUL 파일은 바이트로 보거나(-I) 버렸다
436
+ * · glob 을 아예 안 넘겼다 — `*.js` 로 좁혀 달라는 말이 무시됐다
437
+ *
438
+ * `-P`(PCRE) 로 바꿔도 못 맞춘다. 재 보니 윈도우의 git 은 로케일을 UTF-8 로
439
+ * 줘도 PCRE 를 **바이트로** 돌려서 `[가-힣]` 이 한 줄도 안 맞았고, 리눅스의
440
+ * git 은 글자로 돌린다. 같은 무늬가 PC 마다 다른 답을 낸다.
441
+ *
442
+ * 그래서 git grep 에게는 **맞는 줄이라면 반드시 품고 있는 글자**(꼭있는글자)만
443
+ * 고정 문자열로 주어 파일을 추리게 하고, 추린 파일은 자바스크립트 길과 같은
444
+ * 함수로 연다(tools/index.js 의 한파일에서찾기). 추린 목록은 답을 품는 쪽으로만
445
+ * 넓으니 답이 자바스크립트 길과 같다. 거기에 git 이 UTF-8 로 못 푸는 파일
446
+ * (git못푸는파일)을 더한다. 꼭 있는 글자를 못 뽑는 무늬는 예전 길로 간다.
447
+ */
448
+
449
+ /**
450
+ * 정규식이 맞는 줄이면 **반드시** 들어 있는 글자 조각 중 가장 긴 것. 모르면 null.
451
+ *
452
+ * 넓게 잡으면 틀린다(그 글자 없이도 맞는 줄을 놓친다). 그래서 조금이라도
453
+ * 모르겠으면 조각을 끊는다 — 짧아질 뿐 틀리지는 않는다.
454
+ *
455
+ * · 맨 바깥의 `|` — 어느 갈래가 맞을지 모른다 → null
456
+ * · 괄호 안에 `|` 가 있거나 괄호 뒤가 `?` `*` `{0,…}` 이면, 앞뒤 보기면 그 괄호는 버린다
457
+ * · 글자 무리 `[…]` · `.` · `\d` 같은 무리 · `\x41` 같은 번호 글자 → 끊는다
458
+ * · 대소문자 무시에서 `k`·`s` 는 끊는다 — 유니코드로 접으면 K(켈빈)·ſ 에도 맞는다
459
+ */
460
+ export function 꼭있는글자(무늬, { 대소문자무시 = false } = {}) {
461
+ const s = String(무늬 ?? '');
462
+ const 쌓 = [{ 조각들: [], 지금: '', 버림: false, 갈래: false }];
463
+ const 위 = () => 쌓[쌓.length - 1];
464
+ const 끊기 = () => { const f = 위(); if (f.지금) f.조각들.push(f.지금); f.지금 = ''; };
465
+ const 되풀이 = (i) => {
466
+ const c = s[i];
467
+ let 길이 = 0; let 없어도됨 = false; let 여럿 = false;
468
+ if (c === '?' || c === '*') { 길이 = 1; 없어도됨 = true; }
469
+ else if (c === '+') { 길이 = 1; 여럿 = true; }
470
+ else if (c === '{') {
471
+ const m = /^\{(\d+)(,\d*)?\}/.exec(s.slice(i));
472
+ if (m) { 길이 = m[0].length; 없어도됨 = Number(m[1]) === 0; 여럿 = true; }
473
+ }
474
+ if (길이 && s[i + 길이] === '?') 길이 += 1; // 게으른 표시
475
+ return { 길이, 없어도됨, 여럿 };
476
+ };
477
+ const 못믿을글자 = (ch) => 대소문자무시
478
+ && (/^[ks]$/i.test(ch) || (ch.codePointAt(0) > 0x7F && ch.toLowerCase() !== ch.toUpperCase()));
479
+ let i = 0;
480
+ while (i < s.length) {
481
+ const c = s[i];
482
+ if (c === '\\') {
483
+ const d = s[i + 1];
484
+ if (d === undefined) return null;
485
+ if (/[A-Za-z0-9]/.test(d)) {
486
+ // 무리(\d \w \s …) · 자리 표시(\b) · 번호 글자(\x41 \u0041 \u{…} \cJ) · 되부르기(\1 \k<이름>)
487
+ 끊기();
488
+ let j = i + 2;
489
+ if (s[j] === '{' && /[pPux]/.test(d)) { const e = s.indexOf('}', j); if (e < 0) return null; j = e + 1; }
490
+ else if (d === 'k' && s[j] === '<') { const e = s.indexOf('>', j); if (e < 0) return null; j = e + 1; }
491
+ else if (d === 'x') j += 2;
492
+ else if (d === 'u') j += 4;
493
+ else if (d === 'c') j += 1;
494
+ else if (/[0-9]/.test(d)) while (/[0-9]/.test(s[j] ?? '')) j++;
495
+ i = j + 되풀이(j).길이;
496
+ continue;
497
+ }
498
+ // 빗금 뒤 문장부호는 그 글자 그대로다 (`\.` `\$` `\(`).
499
+ const q = 되풀이(i + 2);
500
+ if (q.없어도됨 || 못믿을글자(d)) 끊기();
501
+ else { 위().지금 += d; if (q.여럿) 끊기(); }
502
+ i = i + 2 + q.길이;
503
+ } else if (c === '[') {
504
+ let j = i + 1;
505
+ while (j < s.length && s[j] !== ']') { if (s[j] === '\\') j++; j++; }
506
+ if (j >= s.length) return null;
507
+ 끊기();
508
+ i = j + 1 + 되풀이(j + 1).길이;
509
+ } else if (c === '(') {
510
+ 끊기();
511
+ let j = i + 1; let 버림 = false;
512
+ if (s[j] === '?') {
513
+ if (s[j + 1] === ':') j += 2;
514
+ else if (s[j + 1] === '<' && s[j + 2] !== '=' && s[j + 2] !== '!') {
515
+ const e = s.indexOf('>', j); if (e < 0) return null; j = e + 1;
516
+ } else {
517
+ // 앞뒤 보기 `(?=…)` `(?!…)` `(?<=…)` · 깃발 `(?i:…)` — 안의 글자를 믿지 않는다
518
+ 버림 = true;
519
+ j += 2;
520
+ }
521
+ }
522
+ 쌓.push({ 조각들: [], 지금: '', 버림, 갈래: false });
523
+ i = j;
524
+ } else if (c === ')') {
525
+ if (쌓.length === 1) return null;
526
+ 끊기();
527
+ const f = 쌓.pop();
528
+ const q = 되풀이(i + 1);
529
+ if (!f.버림 && !f.갈래 && !q.없어도됨) 위().조각들.push(...f.조각들);
530
+ i = i + 1 + q.길이;
531
+ } else if (c === '|') {
532
+ if (쌓.length === 1) return null;
533
+ 끊기(); 위().갈래 = true; i += 1;
534
+ } else if (c === '.' || c === '^' || c === '$') {
535
+ 끊기();
536
+ i = i + 1 + 되풀이(i + 1).길이;
537
+ } else if ('*+?{}]'.includes(c)) {
538
+ return null; // 되풀이할 것이 없는 되풀이 표시 · 짝 없는 괄호 — 낯선 무늬다
539
+ } else {
540
+ const ch = String.fromCodePoint(s.codePointAt(i));
541
+ const q = 되풀이(i + ch.length);
542
+ if (q.없어도됨 || 못믿을글자(ch)) 끊기();
543
+ else { 위().지금 += ch; if (q.여럿) 끊기(); }
544
+ i = i + ch.length + q.길이;
545
+ }
546
+ }
547
+ if (쌓.length !== 1) return null;
548
+ 끊기();
549
+ const 조각들 = 쌓[0].조각들;
550
+ if (!조각들.length) return null;
551
+ return 조각들.reduce((a, b) => (Buffer.byteLength(b) > Buffer.byteLength(a) ? b : a));
157
552
  }
158
553
 
159
- /** `git grep` 으로 찾는다. rg 가 없고 여기가 저장소일 때. */
160
- export async function git로찾기({ 무늬, 자리, 대소문자무시 = false, 최대 = 5000, timeout = 20000, signal = null }) {
161
- const 인자 = ['-C', 자리, '--no-pager', 'grep', '--line-number', '--no-color', '-I', '-E'];
162
- if (대소문자무시) 인자.push('-i');
163
- // 여기도 같은 목록으로 뺀다. git 은 경로무늬 앞머리에 마법을 붙여 빼낸다.
164
- 인자.push('-e', 무늬, '--', '.', ...안볼확장자.map((x) => `:(exclude,icase)*.${x}`));
165
- const r = await 돌려보기('git', 인자, { timeout, signal });
554
+ // 자바스크립트 길이 안 여는 파일·폴더는 git 도 안 연다. 같은 목록에서 나온다.
555
+ function git경로빼기() {
556
+ return [
557
+ ...안볼확장자.map((x) => `:(exclude,icase)*.${x}`),
558
+ ...[...SKIP_DIRS].map((d) => `:(exclude,glob,icase)**/${d}/**`),
559
+ ];
560
+ }
561
+
562
+ async function git파일목록(자리, 인자, { timeout, signal, env = null }) {
563
+ const r = await 돌려보기('git', ['-C', 자리, '-c', 'core.quotePath=false', '--no-pager', 'grep', ...인자], {
564
+ timeout, signal, 덤: env ? { env: { ...process.env, ...env } } : {},
565
+ });
166
566
  if (r.error) return { ok: false, 왜: r.error.message };
567
+ // git grep 은 못 찾으면 1 이다. 그 밖(128 — PCRE 없이 빌드한 git 등)은 못 물어본 것이다.
167
568
  if (r.status !== 0 && r.status !== 1) {
168
569
  return { ok: false, 왜: (r.stderr ?? '').split('\n')[0] || 'git grep 이 무늬를 못 읽었습니다' };
169
570
  }
170
- const 줄들 = String(r.stdout ?? '').split('\n').filter(Boolean);
171
- // git grep 은 저장소 기준 상대경로를 준다. 절대경로로 맞춰 준다 — 아래에서 한 모양으로 쓴다.
172
- const 고친것 = 줄들.map((l) => {
173
- const i = l.indexOf(':');
174
- if (i < 0) return l;
175
- return join(자리, l.slice(0, i)) + l.slice(i);
176
- });
177
- return { ok: true, 줄들: 고친것.slice(0, 최대), 잘림: 고친것.length > 최대 };
571
+ // -z 라 이름 사이가 NUL 이다. 이름에 빈칸·따옴표가 있어도 안 깨진다. 찾는 자리 기준 상대경로로 온다.
572
+ const 파일들 = String(r.stdout ?? '').split('\0').map((x) => x.replace(/^\n/, '')).filter(Boolean).map((x) => join(자리, x));
573
+ return { ok: true, 파일들 };
574
+ }
575
+
576
+ /**
577
+ * 꼭 있는 글자를 품은 파일들. 커밋 안 한 새 파일도 본다(`--untracked` — .gitignore 는 지킨다).
578
+ * 글이 아닌 파일(-I)은 뺀다 — 그중 글인 것(표식 없는 UTF-16)은 git못푸는파일 이 잡는다.
579
+ */
580
+ export async function git후보파일({ 글자, 자리, 대소문자무시 = false, timeout = 20000, signal = null }) {
581
+ const 인자 = ['-l', '-z', '-I', '--untracked', '-F'];
582
+ if (대소문자무시) 인자.push('-i');
583
+ 인자.push('-e', 글자, '--', '.', ...git경로빼기());
584
+ return git파일목록(자리, 인자, { timeout, signal });
585
+ }
586
+
587
+ /**
588
+ * git 이 UTF-8 로 못 읽는 파일들 — rg못푸는파일 과 같은 바이트 무늬.
589
+ *
590
+ * 로케일을 C 로 못박는다. UTF-8 로케일의 리눅스 git 은 PCRE 를 글자로 돌려서
591
+ * `[\x80-\xBF]` 가 바이트가 아니라 U+0080~U+00BF 가 되고, CP949 파일이 한 개도
592
+ * 안 걸린다. `-a` 로 NUL 든 파일도 끝까지 본다.
593
+ */
594
+ export async function git못푸는파일({ 자리, timeout = 20000, signal = null }) {
595
+ const 바이트무늬 = 못푸는바이트무늬.replace(/^\(\?-u:/, '(?:');
596
+ const 인자 = ['-l', '-z', '-a', '--untracked', '-P', '-e', 바이트무늬, '--', '.', ...git경로빼기()];
597
+ return git파일목록(자리, 인자, { timeout, signal, env: { LC_ALL: 'C' } });
178
598
  }
179
599
 
180
600
  /*
@@ -192,7 +612,21 @@ export async function git로찾기({ 무늬, 자리, 대소문자무시 = false,
192
612
  * 막고 있는 척만 하는 코드는 없느니만 못해서 지웠다. 대신 윈도우 경로를
193
613
  * 재는 검사를 test/fastgrep.test.js 에 남겨 둔다.
194
614
  */
615
+ /*
616
+ * ── 경로 끝이 NUL 이면 그 자리로 가른다 (2.0.0 6회차 Gemini 빠른찾기6) ─────────
617
+ *
618
+ * 위 정규식은 `:숫자:` 가 **처음** 나오는 자리를 경로 끝으로 본다. 윈도우는 파일 이름에 콜론을
619
+ * 못 쓰니 그걸로 됐지만, 리눅스·맥은 쓸 수 있다 — `backup-12:30:45.txt:7:hello` 가 파일
620
+ * `backup-12` · 줄 30 으로 갈렸다. 시각을 붙인 백업·로그 이름에서 그대로 생긴다.
621
+ * rg 를 `--null` 로 불러 경로 뒤를 NUL 로 받는다. 파일 이름에는 NUL 이 못 드니 모호함이 없다.
622
+ * NUL 없는 줄(예전 꼴)은 여전히 아래 정규식으로 본다.
623
+ */
195
624
  export function 줄가르기(줄) {
625
+ const 끝 = 줄.indexOf('\0');
626
+ if (끝 >= 0) {
627
+ const n = /^(\d+):([\s\S]*)$/.exec(줄.slice(끝 + 1));
628
+ return n ? { 파일: 줄.slice(0, 끝), 줄: Number(n[1]), 내용: n[2] } : null;
629
+ }
196
630
  const m = /^(.*?):(\d+):([\s\S]*)$/.exec(줄);
197
631
  if (!m) return null;
198
632
  return { 파일: m[1], 줄: Number(m[2]), 내용: m[3] };
@@ -203,11 +637,29 @@ export function 줄가르기(줄) {
203
637
  *
204
638
  * @returns {{엔진:string, 줄들:Array<{파일,줄,내용}>, 잘림:boolean} | null}
205
639
  */
206
- export async function 빠르게찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, signal = null }) {
640
+ export async function 빠르게찾기({ 무늬, 자리, 뿌리 = null, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, signal = null }) {
207
641
  const 것 = 엔진찾기();
208
642
  if (것.rg) {
209
- const r = await rg로찾기({ 무늬, 자리, glob, 대소문자무시, 무시파일, 최대, signal });
210
- if (r.ok) return { 엔진: 'rg', 줄들: r.줄들.map(줄가르기).filter(Boolean), 잘림: r.잘림 };
643
+ /*
644
+ * 찾기와 「rg 가 못 푸는 파일 세기」 를 같이 돌린다 (rg못푸는파일 머리말).
645
+ * 못 푼 파일에서 나온 rg 의 줄은 버린다 — 바이트 그대로 찍힌 깨진 글이거나
646
+ * NUL 앞에서 끊긴 반쪽이다. 그 파일들은 `따로볼파일` 로 넘겨 부르는 쪽이
647
+ * 예전 길과 같은 방법으로 읽는다. 세기가 실패하면 어느 답을 믿을지 모르니
648
+ * 통째로 예전 길로 간다.
649
+ */
650
+ const [r, 못푼것] = await Promise.all([
651
+ rg로찾기({ 무늬, 자리, 뿌리, glob, 대소문자무시, 무시파일, 최대, signal }),
652
+ rg못푸는파일({ 자리, 뿌리, glob, 무시파일, signal }),
653
+ ]);
654
+ if (r.ok && 못푼것.ok) {
655
+ const 따로 = new Set(못푼것.파일들);
656
+ return {
657
+ 엔진: 'rg',
658
+ 줄들: r.줄들.map(줄가르기).filter(Boolean).filter((x) => !따로.has(x.파일)),
659
+ 잘림: r.잘림,
660
+ 따로볼파일: 못푼것.파일들,
661
+ };
662
+ }
211
663
  // rg 가 무늬를 못 읽은 것일 수 있다(Rust regex 에는 되돌아보기가 없다).
212
664
  // 그건 '없다' 가 아니라 '못 물어봤다' 이므로 예전 길로 내려간다.
213
665
  return null;
@@ -215,9 +667,20 @@ export async function 빠르게찾기({ 무늬, 자리, glob = null, 대소문
215
667
  // git grep 은 .deelignore 를 시킬 방법이 없다. 그 파일이 있으면 예전 길로 간다 —
216
668
  // 사람이 "deel 은 여기 보지 마라" 고 적어 둔 것을 못 지키면 빠른 것이 뜻이 없다.
217
669
  if (것.gitgrep && !무시파일 && 저장소인가(자리)) {
218
- const r = await git로찾기({ 무늬, 자리, 대소문자무시, 최대, signal });
219
- if (r.ok) return { 엔진: 'git grep', 줄들: r.줄들.map(줄가르기).filter(Boolean), 잘림: r.잘림 };
220
- return null;
670
+ // 파일만 추리고 읽기는 자바스크립트 길과 같은 함수로 한다 (꼭있는글자 위 머리말).
671
+ const 글자 = 꼭있는글자(무늬, { 대소문자무시 });
672
+ if (!글자) return null;
673
+ const [후보, 못푼것] = await Promise.all([
674
+ git후보파일({ 글자, 자리, 대소문자무시, signal }),
675
+ git못푸는파일({ 자리, signal }),
676
+ ]);
677
+ if (!후보.ok || !못푼것.ok) return null;
678
+ const 맞나 = glob거르개(glob, { 뿌리, 자리 });
679
+ const 크기괜찮나 = (f) => { try { return statSync(f).size <= 큰파일; } catch { return false; } };
680
+ const 볼것 = [...new Set([...후보.파일들, ...못푼것.파일들])]
681
+ .filter((f) => 맞나(f) && !안볼정규식.test(f) && 크기괜찮나(f))
682
+ .sort();
683
+ return { 엔진: 'git grep', 줄들: [], 잘림: false, 따로볼파일: 볼것, 따로볼까닭: '추림' };
221
684
  }
222
685
  return null;
223
686
  }