deel-local-cli 1.20.13 → 2.0.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 (155) hide show
  1. package/README.ko.md +96 -78
  2. package/README.md +119 -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 +300 -0
  58. package/src/commands/model.js +852 -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 +452 -39
  64. package/src/configexplain.js +125 -14
  65. package/src/doctor.js +164 -26
  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 +507 -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 +11 -1
  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 +444 -36
  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 +1029 -145
  118. package/src/tools/jobs.js +325 -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 +366 -56
  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 +312 -43
  127. package/src/tools/webfetch.js +184 -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
  * 정규식으로 찾을 것이 없는 파일들.
@@ -55,6 +60,34 @@ export function 안볼글로브() {
55
60
  return 안볼확장자.flatMap((x) => ['--iglob', `!*.${x}`]);
56
61
  }
57
62
 
63
+ /*
64
+ * ── 두 길이 **같은 자리를 본다** ───────────────────────────────────────
65
+ *
66
+ * 안 볼 확장자는 한 벌로 맞춰 놨는데, 정작 **어느 폴더·어느 파일을 도느냐**
67
+ * 는 두 길이 따로 정하고 있었다.
68
+ *
69
+ * rg : 점으로 시작하는 것을 기본으로 안 본다. 대신 dist·build·venv 는 돈다.
70
+ * walk: 점 규칙이 아예 없다(다 본다). 대신 SKIP_DIRS 를 안 돈다.
71
+ *
72
+ * 그래서 같은 폴더에서 같은 무늬로 찾았는데 —
73
+ *
74
+ * rg 가 있는 PC → dist/bundle.js · build/out.js · venv/lib.js
75
+ * rg 가 없는 PC → .github/workflows/ci.yml · .env.example · .eslintrc.json
76
+ *
77
+ * 겹치는 답이 src/a.js 하나뿐이었다(실제로 재 봤다). 「CI 설정 어디서 고쳐」
78
+ * 를 rg 가 깔린 PC 에서 물으면 한 줄도 안 나오고, 모델은 그 침묵을 사실로
79
+ * 받아 「이 저장소에는 워크플로가 없습니다」 로 답을 맺는다. 오류도 안 나고
80
+ * 꼬리말도 안 붙으니 사람은 PC 차이라는 것을 영영 모른다.
81
+ *
82
+ * 이 파일 머리말이 목록을 한 벌만 두자고 적어 둔 까닭이 그것이다. 이제
83
+ * 점 파일은 rg 도 보고(`--hidden`), 안 볼 폴더는 walk 가 쓰는 그 목록을
84
+ * 그대로 옮겨 적는다. `.git` 도 그 목록에 있어서 같이 빠진다 — 점 파일을
85
+ * 보기 시작하면 제일 먼저 걸리는 것이 저장소 살림이다.
86
+ */
87
+ export function 안볼폴더글로브() {
88
+ return [...SKIP_DIRS].flatMap((d) => ['--iglob', `!${d}/`]);
89
+ }
90
+
58
91
  /*
59
92
  * 있는지 한 번만 본다.
60
93
  *
@@ -72,6 +105,8 @@ export function 엔진찾기({ 다시 = false, env = process.env } = {}) {
72
105
  if (본것 && !다시) return 본것;
73
106
  // 끄고 싶을 때가 있다 — 결과가 다르다고 의심될 때 같은 자리에서 견주려면.
74
107
  if (env.DEEL_GREP === 'js') { 본것 = { rg: false, gitgrep: false, 왜: 'DEEL_GREP=js 로 꺼 두었습니다' }; return 본것; }
108
+ // rg 가 깔린 PC 에서 git grep 길을 재 보려면 rg 만 끈다 — 안 그러면 그 길은 rg 없는 PC 에서만 돈다.
109
+ if (env.DEEL_GREP === 'git') { 본것 = { rg: false, gitgrep: 있나('git', ['--version']), 왜: 'DEEL_GREP=git 로 rg 를 꺼 두었습니다' }; return 본것; }
75
110
  본것 = {
76
111
  rg: 있나('rg', ['--version']),
77
112
  gitgrep: 있나('git', ['--version']),
@@ -100,12 +135,189 @@ export function 저장소인가(폴더) {
100
135
  *
101
136
  * @returns {{ok:true, 줄들:string[], 잘림:boolean} | {ok:false, 왜:string}}
102
137
  */
103
- export async function rg로찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, timeout = 20000, signal = null }) {
138
+ export async function rg로찾기({ 무늬, 자리, 뿌리 = null, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, timeout = 20000, signal = null }) {
104
139
  const 인자 = [
105
140
  '--line-number',
106
141
  '--no-heading',
107
142
  '--with-filename',
143
+ // 경로 끝을 콜론 대신 NUL 로 찍는다 — 리눅스·맥의 파일 이름에는 `:숫자:` 가 들 수 있다 (아래 줄가르기).
144
+ '--null',
145
+ '--color', 'never',
146
+ /*
147
+ * CRLF 파일의 `$` 를 줄 끝으로 본다.
148
+ *
149
+ * 이게 없으면 rg 는 `\n` 만 줄 끝으로 알아서 `end$` 가 `end\r\n` 에 안 맞았다.
150
+ * 자바스크립트 길도 줄마다 `\r` 을 달고 있어서 두 길 다 윈도우 파일에서
151
+ * 「일치 없음」 이었다. 두 길을 같이 고친다 (tools/index.js 의 한파일에서찾기).
152
+ */
153
+ '--crlf',
154
+ ...rg거르는인자({ glob, 무시파일, 자리, 뿌리 }),
155
+ ];
156
+ if (대소문자무시) 인자.push('--ignore-case');
157
+ // `--` 뒤로 넘겨서 무늬가 옵션으로 안 읽히게 한다. `-foo` 같은 무늬가 실제로 있다.
158
+ 인자.push('--regexp', 무늬, '--', 자리);
159
+
160
+ const r = await 돌려보기('rg', 인자, { timeout, signal, 덤: rg작업폴더(뿌리, { glob, 무시파일 }) });
161
+ const 못준까닭 = rg가못준까닭(r);
162
+ if (못준까닭) return { ok: false, 왜: 못준까닭 };
163
+ const 줄들 = String(r.stdout ?? '').split('\n').filter(Boolean);
164
+ return { ok: true, 줄들: 줄들.slice(0, 최대), 잘림: 줄들.length > 최대 };
165
+ }
166
+
167
+ /**
168
+ * rg 가 **답을 준 것인가.** 아니면 왜 못 줬는지.
169
+ *
170
+ * rg 의 끝맺음은 셋뿐이다 — 0(찾음) · 1(못 찾음) · 2(무늬·자리가 틀림).
171
+ * 1 은 성공(빈 결과)이고, **그 밖은 전부 「우리가 못 물어본 것」** 이다.
172
+ *
173
+ * ── 왜 `status === 2` 로는 모자랐나 ────────────────────────────────────
174
+ *
175
+ * 여기가 그 한 줄이었다. 그런데 **신호로 죽은** 자식은 status 가 숫자가
176
+ * 아니라 `null` 이다 — spawn.js 의 'close' 가 종료 코드를 그대로 주는데,
177
+ * 신호로 끊긴 프로세스에는 그 코드가 없다. error 도 비어 있다(우리가 죽인
178
+ * 것이 아니니까). OOM killer 가 걷어가거나, 사람이 kill 하거나, 컨테이너가
179
+ * 메모리 상한으로 끊는 자리가 다 그렇다.
180
+ *
181
+ * 그 자리에서 이 자는 `ok: true` 를 올려 보냈다. stdout 은 반 토막이거나
182
+ * 비어 있는데 부르는 쪽은 그걸 **다 찾아본 결과**로 받는다 — 화면에는
183
+ * 「일치 없음」. 이 파일 머리말 3번이 「결과가 없는 것이 아니라 우리가 못
184
+ * 물어본 것」 이라고 적어 둔 바로 그 자리에서, 못 물어본 것이 없는 것으로
185
+ * 나갔다. 실패로 두면 부르는 쪽이 자바스크립트 길로 내려가 다시 찾는다.
186
+ *
187
+ * @returns {string|null} 못 준 까닭. 답을 줬으면 null.
188
+ */
189
+ export function rg가못준까닭(r) {
190
+ if (r?.error) return r.error.message;
191
+ if (r?.status === 0 || r?.status === 1) return null;
192
+ const 첫줄 = String(r?.stderr ?? '').split('\n')[0];
193
+ if (r?.status === 2) return 첫줄 || 'rg 가 무늬를 못 읽었습니다';
194
+ if (r?.status == null) return 첫줄 || 'rg 가 끝맺지 못하고 죽었습니다 (신호로 끊겼습니다)';
195
+ return 첫줄 || `rg 가 ${r.status} 로 끝났습니다`;
196
+ }
197
+
198
+ /*
199
+ * ── rg 가 **글자로 못 푸는** 파일 ─────────────────────────────────────────
200
+ *
201
+ * rg 는 UTF-8 과 표식 있는 UTF-16 만 글자로 푼다. 그 밖은 바이트 그대로 보거나
202
+ * (CP949 · Shift_JIS · 표식 없는 UTF-16) NUL 을 만나는 순간 그 파일을 버린다.
203
+ * 자바스크립트 길은 그 셋을 다 읽는다(encoding.js 가 알아보고 풀고, 바이너리
204
+ * 판정은 앞 8,000바이트만 본다). 그래서 같은 폴더에서 「결재」 를 찾으면 —
205
+ *
206
+ * rg 가 있는 PC → src/a.js
207
+ * rg 가 없는 PC → src/a.js · 사내문서.txt(CP949) · out.txt(UTF-16) · 로그.txt
208
+ *
209
+ * 이 파일 머리말이 「같은 명령이 PC 마다 다른 답을 내면 빠른 게 아니다」 라고
210
+ * 세 번 적어 둔 바로 그 고장이, 하필 사내 문서에서 났다.
211
+ *
212
+ * rg 에 그 인코딩을 가르칠 수는 없다(내용을 보고 고르는 것은 우리 쪽 일이다).
213
+ * 대신 **어느 파일이 그런지**는 rg 가 빨리 안다 — NUL 이나 UTF-8 에 없는
214
+ * 바이트열이 하나라도 든 파일을 한 번 더 훑어 이름만 받는다. 그 파일들은 rg 의
215
+ * 답을 버리고 자바스크립트 길과 **똑같은 방법으로** 다시 읽는다(Grep 도구가 한다).
216
+ *
217
+ * 무늬는 바이트로 본다(`(?-u)`). 한 줄 안에서 이런 것이 보이면 UTF-8 이 아니다:
218
+ * NUL · UTF-8 에 절대 안 나오는 바이트 · 앞 바이트 없는 이음 바이트 ·
219
+ * 이음이 모자란 앞 바이트 · 이음이 남는 글자 · 너무 길게 쓴 것과 서로게이트.
220
+ * 표식 있는 UTF-16 은 rg 가 먼저 UTF-8 로 풀어서 여기 안 걸린다 — 맞다, rg 가
221
+ * 그건 제대로 찾는다.
222
+ */
223
+ const 이음 = '[\\x80-\\xBF]';
224
+ const 이음아님 = '(?:[^\\x80-\\xBF]|$)';
225
+ export const 못푸는바이트무늬 = '(?-u:'
226
+ + [
227
+ '\\x00',
228
+ '[\\xC0\\xC1\\xF5-\\xFF]',
229
+ `(?:^|[\\x00-\\x7F])${이음}`,
230
+ `[\\xC2-\\xDF]${이음아님}`,
231
+ `[\\xE0-\\xEF](?:${이음아님}|${이음}${이음아님})`,
232
+ `[\\xF0-\\xF4](?:${이음아님}|${이음}${이음아님}|${이음}${이음}${이음아님})`,
233
+ `[\\xC2-\\xDF]${이음}{2}`,
234
+ `[\\xE0-\\xEF]${이음}{3}`,
235
+ `[\\xF0-\\xF4]${이음}{4}`,
236
+ '\\xE0[\\x80-\\x9F]', '\\xED[\\xA0-\\xBF]', '\\xF0[\\x80-\\x8F]', '\\xF4[\\x90-\\xBF]',
237
+ ].join('|')
238
+ + ')';
239
+
240
+ /** rg 가 글자로 못 푸는 파일 이름들. 거르는 규칙은 rg로찾기 와 **같은 것**을 쓴다. */
241
+ /*
242
+ * `부르기` 를 밖에서 받는 까닭은 **검사 때문**이다 (tools/clipboard.js 와 같은 규칙).
243
+ * 신호로 죽은 rg · 2 가 아닌 종료코드는 진짜 rg 로는 만들 수가 없어서, 그 갈래가
244
+ * 영영 안 재진다. 여기가 조용히 빈 목록을 올리면 사내 문서가 검색에서 통째로
245
+ * 사라지는 자리라, 재지는 조각을 내놓는 것이 맞다.
246
+ */
247
+ export async function rg못푸는파일({ 자리, 뿌리 = null, glob = null, 무시파일 = null, timeout = 20000, signal = null, 부르기 = 돌려보기 }) {
248
+ const 인자 = [
249
+ '--files-with-matches',
108
250
  '--color', 'never',
251
+ // NUL 이 든 파일도 끝까지 본다. 안 그러면 rg 가 그 파일을 버려서 이름도 안 나온다.
252
+ '--text',
253
+ ...rg거르는인자({ glob, 무시파일, 자리, 뿌리 }),
254
+ '--regexp', 못푸는바이트무늬, '--', 자리,
255
+ ];
256
+ const r = await 부르기('rg', 인자, { timeout, signal, 덤: rg작업폴더(뿌리, { glob, 무시파일 }) });
257
+ /*
258
+ * 실패는 **rg가못준까닭 한 자로** 가른다. 여기는 `error` 와 `status === 2` 두 줄뿐이라,
259
+ * 신호로 죽거나(`status === null`) 2 가 아닌 값으로 끝나면 **빈 목록이 `ok:true`** 로
260
+ * 올라갔다. 그러면 빠르게찾기 가 「따로 볼 파일 없음」 으로 받아, CP949·Shift_JIS·
261
+ * 표식 없는 UTF-16 파일을 한 개도 되읽지 않는다 — rg 가 그 파일에서 낸 답은 버려졌는데
262
+ * 다시 읽지도 않으니 결과가 조용히 「일치 없음」 이 된다. 이 함수 머리말이 「거르는
263
+ * 규칙은 rg로찾기 와 같은 것을 쓴다」 고 적어 두고 여기만 옛 두 줄이었다.
264
+ */
265
+ const 못준까닭 = rg가못준까닭(r);
266
+ if (못준까닭) return { ok: false, 왜: 못준까닭 };
267
+ const 파일들 = String(r.stdout ?? '').split('\n').map((l) => l.replace(/\r$/, '')).filter(Boolean);
268
+ return { ok: true, 파일들 };
269
+ }
270
+
271
+ /**
272
+ * 어느 파일·폴더를 보나 — rg 를 부르는 **모든** 자리가 이 한 벌을 쓴다.
273
+ * 두 번 부르는 rg(찾기 · 못 푸는 파일 세기)가 서로 다른 파일을 보면
274
+ * 거기서 또 답이 갈린다.
275
+ */
276
+ /*
277
+ * ── rg 는 glob 을 **제 작업 폴더** 기준으로 맞춘다 ──────────────────────────
278
+ *
279
+ * `--glob src/*.js` 와 `--ignore-file` 의 무늬는 rg 를 띄운 작업 폴더에서부터
280
+ * 읽힌다 — 찾을 자리가 아니다. deel 은 작업 폴더를 안 정하고 띄워서, 사람이
281
+ * 다른 폴더에서 deel 을 켰거나 검사처럼 뿌리와 다른 자리에서 돌면 빗금 든
282
+ * glob 이 한 파일에도 안 맞았다(오류 없이 「일치 없음」). .deelignore 의
283
+ * `src/secret.txt` 같은 줄도 같은 까닭으로 안 먹었다.
284
+ *
285
+ * 작업 폴더를 작업 범위의 뿌리로 정한다. .deelignore 가 사는 자리도 거기다.
286
+ *
287
+ * glob 도 .deelignore 도 없으면 **안 바꾼다.** 윈도우는 떠 있는 프로세스의
288
+ * 작업 폴더를 못 지운다. 멈추라고 해서 죽인 rg 는 몇 ms 더 살아 있고, 그동안
289
+ * 그 폴더가 잠긴다 — test/abort-tools.test.js 가 바로 그 자리에서 EPERM 으로
290
+ * 잡았다. 작업 폴더가 뜻을 갖는 것은 위 두 무늬뿐이라 그때만 옮긴다.
291
+ */
292
+ function rg작업폴더(뿌리, { glob = null, 무시파일 = null } = {}) {
293
+ return 뿌리 && (glob || 무시파일) ? { cwd: 뿌리 } : {};
294
+ }
295
+
296
+ /*
297
+ * glob 하나를 rg 인자로. 규칙은 fsutil.js 의 glob거르개 와 **같다**.
298
+ *
299
+ * 작업 폴더가 뿌리이므로 빗금 든 무늬는 뿌리 기준으로 맞는다. 찾는 폴더가
300
+ * 뿌리 아래(path=src)면 「찾는 폴더 기준」 으로도 맞아야 해서 그 앞머리를 붙인
301
+ * 무늬를 하나 더 준다 — rg 는 여러 --glob 을 「하나라도 맞으면」 으로 읽고,
302
+ * `!` 빼기는 「하나라도 맞으면 뺀다」 로 읽는다. 자바스크립트 쪽과 같은 뜻이다.
303
+ * `/` 로 시작하는 무늬는 뿌리에 묶인 것이라 더 안 붙인다.
304
+ */
305
+ export function rg글로브들(glob, { 자리 = null, 뿌리 = null } = {}) {
306
+ if (!glob) return [];
307
+ const 빼기 = glob.startsWith('!');
308
+ const 몸 = 빼기 ? glob.slice(1) : glob;
309
+ const 무늬들 = [몸];
310
+ if (!몸.startsWith('/') && 몸.includes('/') && 뿌리 && 자리) {
311
+ const 앞 = relative(뿌리, 자리);
312
+ if (앞 && 앞 !== '..' && !앞.startsWith(`..${sep}`) && !isAbsolute(앞)) {
313
+ 무늬들.push(`${앞.split(sep).join('/')}/${몸}`);
314
+ }
315
+ }
316
+ return 무늬들.map((x) => (빼기 ? `!${x}` : x));
317
+ }
318
+
319
+ function rg거르는인자({ glob = null, 무시파일 = null, 자리 = null, 뿌리 = null } = {}) {
320
+ const 인자 = [
109
321
  // 아주 큰 파일은 안 본다(JS 길과 같은 규칙). 글이 아닌 파일은 rg 가 기본으로 건너뛴다.
110
322
  '--max-filesize', '2M',
111
323
  /*
@@ -130,13 +342,23 @@ export async function rg로찾기({ 무늬, 자리, glob = null, 대소문자무
130
342
  * 같은 명령이 PC 에 따라 다른 답을 내고, 하필 **가리라고 적은 것을 더
131
343
  * 보는 쪽**으로 갈린다. 이 파일 머리말이 없애려던 바로 그 고장이다.
132
344
  * 빠른 길이 다른 답을 내면 그건 빠른 게 아니다.
345
+ *
346
+ * ── 두 자를 **둘 다** 줘야 한다 ────────────────────────────────────
347
+ *
348
+ * `--glob-case-insensitive` 는 **`--glob` 으로 우리가 준 무늬**에만 먹는다.
349
+ * `.gitignore`·`.deelignore` 규칙에는 안 먹는다. 그래서 고친 줄 알고 넘어간
350
+ * 자리에서 그 고장이 그대로 남아 있었다 — 재 보니 `*.tmpx` 를 적어 둔
351
+ * 폴더에서 `A.TMPX` 를 rg 길만 검색했다(자바스크립트 길은 안 봤다).
352
+ * 규칙 파일 쪽 대소문자는 이 자가 따로 정한다.
133
353
  */
134
- ...(process.platform === 'win32' ? ['--glob-case-insensitive'] : []),
354
+ ...(process.platform === 'win32' ? ['--glob-case-insensitive', '--ignore-file-case-insensitive'] : []),
355
+ // 점으로 시작하는 것도 본다 — 자바스크립트 길은 처음부터 다 보고 있었다.
356
+ // 무엇이 빠졌었는지는 위 안볼폴더글로브() 에 적어 두었다.
357
+ '--hidden',
135
358
  ];
136
- if (대소문자무시) 인자.push('--ignore-case');
137
- if (glob) 인자.push('--glob', glob);
138
- // 자바스크립트 길이 안 여는 파일은 rg 도 안 열게 한다. 같은 목록에서 나온다.
139
- 인자.push(...안볼글로브());
359
+ for (const g of rg글로브들(glob, { 자리, 뿌리 })) 인자.push('--glob', g);
360
+ // 자바스크립트 길이 안 여는 파일·폴더는 rg 도 안 열게 한다. 같은 목록에서 나온다.
361
+ 인자.push(...안볼글로브(), ...안볼폴더글로브());
140
362
  /*
141
363
  * .deelignore 도 지켜야 한다.
142
364
  *
@@ -145,36 +367,179 @@ export async function rg로찾기({ 무늬, 자리, glob = null, 대소문자무
145
367
  * 같은 명령이 PC 마다 다른 답을 내는 셈이라, 이건 빠른 것보다 나쁘다.
146
368
  */
147
369
  if (무시파일) 인자.push('--ignore-file', 무시파일);
148
- // `--` 뒤로 넘겨서 무늬가 옵션으로 안 읽히게 한다. `-foo` 같은 무늬가 실제로 있다.
149
- 인자.push('--regexp', 무늬, '--', 자리);
370
+ return 인자;
371
+ }
150
372
 
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 > 최대 };
373
+ /*
374
+ * ── git grep 에는 **파일만 추리게** 한다 ────────────────────────────────
375
+ *
376
+ * 여기서 사람의 정규식을 그대로 `git grep -E` 에 넘기고 있었다. 네 가지가 샜다:
377
+ *
378
+ * · ERE 에는 `\d` 가 없다 — `foo\d+` 가 조용히 다른 무늬로 읽혔다
379
+ * · 기본은 **git 이 아는 파일만** 본다 — 방금 만든 파일은 한 줄도 안 나왔다
380
+ * · CP949 · 표식 없는 UTF-16 · 8KB 뒤 NUL 파일은 바이트로 보거나(-I) 버렸다
381
+ * · glob 을 아예 안 넘겼다 — `*.js` 로 좁혀 달라는 말이 무시됐다
382
+ *
383
+ * `-P`(PCRE) 로 바꿔도 못 맞춘다. 재 보니 윈도우의 git 은 로케일을 UTF-8 로
384
+ * 줘도 PCRE 를 **바이트로** 돌려서 `[가-힣]` 이 한 줄도 안 맞았고, 리눅스의
385
+ * git 은 글자로 돌린다. 같은 무늬가 PC 마다 다른 답을 낸다.
386
+ *
387
+ * 그래서 git grep 에게는 **맞는 줄이라면 반드시 품고 있는 글자**(꼭있는글자)만
388
+ * 고정 문자열로 주어 파일을 추리게 하고, 추린 파일은 자바스크립트 길과 같은
389
+ * 함수로 연다(tools/index.js 의 한파일에서찾기). 추린 목록은 답을 품는 쪽으로만
390
+ * 넓으니 답이 자바스크립트 길과 같다. 거기에 git 이 UTF-8 로 못 푸는 파일
391
+ * (git못푸는파일)을 더한다. 꼭 있는 글자를 못 뽑는 무늬는 예전 길로 간다.
392
+ */
393
+
394
+ /**
395
+ * 정규식이 맞는 줄이면 **반드시** 들어 있는 글자 조각 중 가장 긴 것. 모르면 null.
396
+ *
397
+ * 넓게 잡으면 틀린다(그 글자 없이도 맞는 줄을 놓친다). 그래서 조금이라도
398
+ * 모르겠으면 조각을 끊는다 — 짧아질 뿐 틀리지는 않는다.
399
+ *
400
+ * · 맨 바깥의 `|` — 어느 갈래가 맞을지 모른다 → null
401
+ * · 괄호 안에 `|` 가 있거나 괄호 뒤가 `?` `*` `{0,…}` 이면, 앞뒤 보기면 그 괄호는 버린다
402
+ * · 글자 무리 `[…]` · `.` · `\d` 같은 무리 · `\x41` 같은 번호 글자 → 끊는다
403
+ * · 대소문자 무시에서 `k`·`s` 는 끊는다 — 유니코드로 접으면 K(켈빈)·ſ 에도 맞는다
404
+ */
405
+ export function 꼭있는글자(무늬, { 대소문자무시 = false } = {}) {
406
+ const s = String(무늬 ?? '');
407
+ const 쌓 = [{ 조각들: [], 지금: '', 버림: false, 갈래: false }];
408
+ const 위 = () => 쌓[쌓.length - 1];
409
+ const 끊기 = () => { const f = 위(); if (f.지금) f.조각들.push(f.지금); f.지금 = ''; };
410
+ const 되풀이 = (i) => {
411
+ const c = s[i];
412
+ let 길이 = 0; let 없어도됨 = false; let 여럿 = false;
413
+ if (c === '?' || c === '*') { 길이 = 1; 없어도됨 = true; }
414
+ else if (c === '+') { 길이 = 1; 여럿 = true; }
415
+ else if (c === '{') {
416
+ const m = /^\{(\d+)(,\d*)?\}/.exec(s.slice(i));
417
+ if (m) { 길이 = m[0].length; 없어도됨 = Number(m[1]) === 0; 여럿 = true; }
418
+ }
419
+ if (길이 && s[i + 길이] === '?') 길이 += 1; // 게으른 표시
420
+ return { 길이, 없어도됨, 여럿 };
421
+ };
422
+ const 못믿을글자 = (ch) => 대소문자무시
423
+ && (/^[ks]$/i.test(ch) || (ch.codePointAt(0) > 0x7F && ch.toLowerCase() !== ch.toUpperCase()));
424
+ let i = 0;
425
+ while (i < s.length) {
426
+ const c = s[i];
427
+ if (c === '\\') {
428
+ const d = s[i + 1];
429
+ if (d === undefined) return null;
430
+ if (/[A-Za-z0-9]/.test(d)) {
431
+ // 무리(\d \w \s …) · 자리 표시(\b) · 번호 글자(\x41 \u0041 \u{…} \cJ) · 되부르기(\1 \k<이름>)
432
+ 끊기();
433
+ let j = i + 2;
434
+ if (s[j] === '{' && /[pPux]/.test(d)) { const e = s.indexOf('}', j); if (e < 0) return null; j = e + 1; }
435
+ else if (d === 'k' && s[j] === '<') { const e = s.indexOf('>', j); if (e < 0) return null; j = e + 1; }
436
+ else if (d === 'x') j += 2;
437
+ else if (d === 'u') j += 4;
438
+ else if (d === 'c') j += 1;
439
+ else if (/[0-9]/.test(d)) while (/[0-9]/.test(s[j] ?? '')) j++;
440
+ i = j + 되풀이(j).길이;
441
+ continue;
442
+ }
443
+ // 빗금 뒤 문장부호는 그 글자 그대로다 (`\.` `\$` `\(`).
444
+ const q = 되풀이(i + 2);
445
+ if (q.없어도됨 || 못믿을글자(d)) 끊기();
446
+ else { 위().지금 += d; if (q.여럿) 끊기(); }
447
+ i = i + 2 + q.길이;
448
+ } else if (c === '[') {
449
+ let j = i + 1;
450
+ while (j < s.length && s[j] !== ']') { if (s[j] === '\\') j++; j++; }
451
+ if (j >= s.length) return null;
452
+ 끊기();
453
+ i = j + 1 + 되풀이(j + 1).길이;
454
+ } else if (c === '(') {
455
+ 끊기();
456
+ let j = i + 1; let 버림 = false;
457
+ if (s[j] === '?') {
458
+ if (s[j + 1] === ':') j += 2;
459
+ else if (s[j + 1] === '<' && s[j + 2] !== '=' && s[j + 2] !== '!') {
460
+ const e = s.indexOf('>', j); if (e < 0) return null; j = e + 1;
461
+ } else {
462
+ // 앞뒤 보기 `(?=…)` `(?!…)` `(?<=…)` · 깃발 `(?i:…)` — 안의 글자를 믿지 않는다
463
+ 버림 = true;
464
+ j += 2;
465
+ }
466
+ }
467
+ 쌓.push({ 조각들: [], 지금: '', 버림, 갈래: false });
468
+ i = j;
469
+ } else if (c === ')') {
470
+ if (쌓.length === 1) return null;
471
+ 끊기();
472
+ const f = 쌓.pop();
473
+ const q = 되풀이(i + 1);
474
+ if (!f.버림 && !f.갈래 && !q.없어도됨) 위().조각들.push(...f.조각들);
475
+ i = i + 1 + q.길이;
476
+ } else if (c === '|') {
477
+ if (쌓.length === 1) return null;
478
+ 끊기(); 위().갈래 = true; i += 1;
479
+ } else if (c === '.' || c === '^' || c === '$') {
480
+ 끊기();
481
+ i = i + 1 + 되풀이(i + 1).길이;
482
+ } else if ('*+?{}]'.includes(c)) {
483
+ return null; // 되풀이할 것이 없는 되풀이 표시 · 짝 없는 괄호 — 낯선 무늬다
484
+ } else {
485
+ const ch = String.fromCodePoint(s.codePointAt(i));
486
+ const q = 되풀이(i + ch.length);
487
+ if (q.없어도됨 || 못믿을글자(ch)) 끊기();
488
+ else { 위().지금 += ch; if (q.여럿) 끊기(); }
489
+ i = i + ch.length + q.길이;
490
+ }
491
+ }
492
+ if (쌓.length !== 1) return null;
493
+ 끊기();
494
+ const 조각들 = 쌓[0].조각들;
495
+ if (!조각들.length) return null;
496
+ return 조각들.reduce((a, b) => (Buffer.byteLength(b) > Buffer.byteLength(a) ? b : a));
157
497
  }
158
498
 
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 });
499
+ // 자바스크립트 길이 안 여는 파일·폴더는 git 도 안 연다. 같은 목록에서 나온다.
500
+ function git경로빼기() {
501
+ return [
502
+ ...안볼확장자.map((x) => `:(exclude,icase)*.${x}`),
503
+ ...[...SKIP_DIRS].map((d) => `:(exclude,glob,icase)**/${d}/**`),
504
+ ];
505
+ }
506
+
507
+ async function git파일목록(자리, 인자, { timeout, signal, env = null }) {
508
+ const r = await 돌려보기('git', ['-C', 자리, '-c', 'core.quotePath=false', '--no-pager', 'grep', ...인자], {
509
+ timeout, signal, 덤: env ? { env: { ...process.env, ...env } } : {},
510
+ });
166
511
  if (r.error) return { ok: false, 왜: r.error.message };
512
+ // git grep 은 못 찾으면 1 이다. 그 밖(128 — PCRE 없이 빌드한 git 등)은 못 물어본 것이다.
167
513
  if (r.status !== 0 && r.status !== 1) {
168
514
  return { ok: false, 왜: (r.stderr ?? '').split('\n')[0] || 'git grep 이 무늬를 못 읽었습니다' };
169
515
  }
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 > 최대 };
516
+ // -z 라 이름 사이가 NUL 이다. 이름에 빈칸·따옴표가 있어도 안 깨진다. 찾는 자리 기준 상대경로로 온다.
517
+ const 파일들 = String(r.stdout ?? '').split('\0').map((x) => x.replace(/^\n/, '')).filter(Boolean).map((x) => join(자리, x));
518
+ return { ok: true, 파일들 };
519
+ }
520
+
521
+ /**
522
+ * 꼭 있는 글자를 품은 파일들. 커밋 안 한 새 파일도 본다(`--untracked` — .gitignore 는 지킨다).
523
+ * 글이 아닌 파일(-I)은 뺀다 — 그중 글인 것(표식 없는 UTF-16)은 git못푸는파일 이 잡는다.
524
+ */
525
+ export async function git후보파일({ 글자, 자리, 대소문자무시 = false, timeout = 20000, signal = null }) {
526
+ const 인자 = ['-l', '-z', '-I', '--untracked', '-F'];
527
+ if (대소문자무시) 인자.push('-i');
528
+ 인자.push('-e', 글자, '--', '.', ...git경로빼기());
529
+ return git파일목록(자리, 인자, { timeout, signal });
530
+ }
531
+
532
+ /**
533
+ * git 이 UTF-8 로 못 읽는 파일들 — rg못푸는파일 과 같은 바이트 무늬.
534
+ *
535
+ * 로케일을 C 로 못박는다. UTF-8 로케일의 리눅스 git 은 PCRE 를 글자로 돌려서
536
+ * `[\x80-\xBF]` 가 바이트가 아니라 U+0080~U+00BF 가 되고, CP949 파일이 한 개도
537
+ * 안 걸린다. `-a` 로 NUL 든 파일도 끝까지 본다.
538
+ */
539
+ export async function git못푸는파일({ 자리, timeout = 20000, signal = null }) {
540
+ const 바이트무늬 = 못푸는바이트무늬.replace(/^\(\?-u:/, '(?:');
541
+ const 인자 = ['-l', '-z', '-a', '--untracked', '-P', '-e', 바이트무늬, '--', '.', ...git경로빼기()];
542
+ return git파일목록(자리, 인자, { timeout, signal, env: { LC_ALL: 'C' } });
178
543
  }
179
544
 
180
545
  /*
@@ -192,7 +557,21 @@ export async function git로찾기({ 무늬, 자리, 대소문자무시 = false,
192
557
  * 막고 있는 척만 하는 코드는 없느니만 못해서 지웠다. 대신 윈도우 경로를
193
558
  * 재는 검사를 test/fastgrep.test.js 에 남겨 둔다.
194
559
  */
560
+ /*
561
+ * ── 경로 끝이 NUL 이면 그 자리로 가른다 (2.0.0 6회차 Gemini 빠른찾기6) ─────────
562
+ *
563
+ * 위 정규식은 `:숫자:` 가 **처음** 나오는 자리를 경로 끝으로 본다. 윈도우는 파일 이름에 콜론을
564
+ * 못 쓰니 그걸로 됐지만, 리눅스·맥은 쓸 수 있다 — `backup-12:30:45.txt:7:hello` 가 파일
565
+ * `backup-12` · 줄 30 으로 갈렸다. 시각을 붙인 백업·로그 이름에서 그대로 생긴다.
566
+ * rg 를 `--null` 로 불러 경로 뒤를 NUL 로 받는다. 파일 이름에는 NUL 이 못 드니 모호함이 없다.
567
+ * NUL 없는 줄(예전 꼴)은 여전히 아래 정규식으로 본다.
568
+ */
195
569
  export function 줄가르기(줄) {
570
+ const 끝 = 줄.indexOf('\0');
571
+ if (끝 >= 0) {
572
+ const n = /^(\d+):([\s\S]*)$/.exec(줄.slice(끝 + 1));
573
+ return n ? { 파일: 줄.slice(0, 끝), 줄: Number(n[1]), 내용: n[2] } : null;
574
+ }
196
575
  const m = /^(.*?):(\d+):([\s\S]*)$/.exec(줄);
197
576
  if (!m) return null;
198
577
  return { 파일: m[1], 줄: Number(m[2]), 내용: m[3] };
@@ -203,11 +582,29 @@ export function 줄가르기(줄) {
203
582
  *
204
583
  * @returns {{엔진:string, 줄들:Array<{파일,줄,내용}>, 잘림:boolean} | null}
205
584
  */
206
- export async function 빠르게찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, signal = null }) {
585
+ export async function 빠르게찾기({ 무늬, 자리, 뿌리 = null, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, signal = null }) {
207
586
  const 것 = 엔진찾기();
208
587
  if (것.rg) {
209
- const r = await rg로찾기({ 무늬, 자리, glob, 대소문자무시, 무시파일, 최대, signal });
210
- if (r.ok) return { 엔진: 'rg', 줄들: r.줄들.map(줄가르기).filter(Boolean), 잘림: r.잘림 };
588
+ /*
589
+ * 찾기와 「rg 가 못 푸는 파일 세기」 를 같이 돌린다 (rg못푸는파일 머리말).
590
+ * 못 푼 파일에서 나온 rg 의 줄은 버린다 — 바이트 그대로 찍힌 깨진 글이거나
591
+ * NUL 앞에서 끊긴 반쪽이다. 그 파일들은 `따로볼파일` 로 넘겨 부르는 쪽이
592
+ * 예전 길과 같은 방법으로 읽는다. 세기가 실패하면 어느 답을 믿을지 모르니
593
+ * 통째로 예전 길로 간다.
594
+ */
595
+ const [r, 못푼것] = await Promise.all([
596
+ rg로찾기({ 무늬, 자리, 뿌리, glob, 대소문자무시, 무시파일, 최대, signal }),
597
+ rg못푸는파일({ 자리, 뿌리, glob, 무시파일, signal }),
598
+ ]);
599
+ if (r.ok && 못푼것.ok) {
600
+ const 따로 = new Set(못푼것.파일들);
601
+ return {
602
+ 엔진: 'rg',
603
+ 줄들: r.줄들.map(줄가르기).filter(Boolean).filter((x) => !따로.has(x.파일)),
604
+ 잘림: r.잘림,
605
+ 따로볼파일: 못푼것.파일들,
606
+ };
607
+ }
211
608
  // rg 가 무늬를 못 읽은 것일 수 있다(Rust regex 에는 되돌아보기가 없다).
212
609
  // 그건 '없다' 가 아니라 '못 물어봤다' 이므로 예전 길로 내려간다.
213
610
  return null;
@@ -215,9 +612,20 @@ export async function 빠르게찾기({ 무늬, 자리, glob = null, 대소문
215
612
  // git grep 은 .deelignore 를 시킬 방법이 없다. 그 파일이 있으면 예전 길로 간다 —
216
613
  // 사람이 "deel 은 여기 보지 마라" 고 적어 둔 것을 못 지키면 빠른 것이 뜻이 없다.
217
614
  if (것.gitgrep && !무시파일 && 저장소인가(자리)) {
218
- const r = await git로찾기({ 무늬, 자리, 대소문자무시, 최대, signal });
219
- if (r.ok) return { 엔진: 'git grep', 줄들: r.줄들.map(줄가르기).filter(Boolean), 잘림: r.잘림 };
220
- return null;
615
+ // 파일만 추리고 읽기는 자바스크립트 길과 같은 함수로 한다 (꼭있는글자 위 머리말).
616
+ const 글자 = 꼭있는글자(무늬, { 대소문자무시 });
617
+ if (!글자) return null;
618
+ const [후보, 못푼것] = await Promise.all([
619
+ git후보파일({ 글자, 자리, 대소문자무시, signal }),
620
+ git못푸는파일({ 자리, signal }),
621
+ ]);
622
+ if (!후보.ok || !못푼것.ok) return null;
623
+ const 맞나 = glob거르개(glob, { 뿌리, 자리 });
624
+ const 크기괜찮나 = (f) => { try { return statSync(f).size <= 큰파일; } catch { return false; } };
625
+ const 볼것 = [...new Set([...후보.파일들, ...못푼것.파일들])]
626
+ .filter((f) => 맞나(f) && !안볼정규식.test(f) && 크기괜찮나(f))
627
+ .sort();
628
+ return { 엔진: 'git grep', 줄들: [], 잘림: false, 따로볼파일: 볼것, 따로볼까닭: '추림' };
221
629
  }
222
630
  return null;
223
631
  }
package/src/tools/fig.js CHANGED
@@ -170,7 +170,12 @@ function 열쇠(g) {
170
170
  return `${g.sessionID ?? 0}:${g.localID ?? 0}`;
171
171
  }
172
172
 
173
- /** 크기를 `375×812` 로. 소수점은 버린다 — 시안 치수는 정수로 읽는 게 맞다. */
173
+ /**
174
+ * 크기를 `375×812` 로. 소수점은 **반올림**한다 — 시안 치수는 정수로 읽는 게 맞다.
175
+ *
176
+ * 버리면 `375.8` 이 375 가 되어, 사람이 Figma 에서 보는 376 과 한 칸 어긋난다.
177
+ * 시안 치수는 자를 대는 값이라 가까운 쪽으로 붙이는 것이 맞다.
178
+ */
174
179
  function 크기말(size) {
175
180
  if (!size || typeof size !== 'object') return '';
176
181
  const w = Number(size.x);
@@ -217,9 +222,15 @@ function 나무엮기(노드들) {
217
222
  return { 자리표, 자식표, 뿌리들 };
218
223
  }
219
224
 
220
- /** 한 갈래를 줄들로 편다. 깊이는 두 칸씩. */
225
+ /**
226
+ * 한 갈래를 줄들로 편다. 깊이는 두 칸씩.
227
+ *
228
+ * 상한에 걸려 **못 편 것이 실제로 있을 때만** `셈.잘림` 을 켠다. 「남은 것이
229
+ * 0 인가」 로 물으면 상한과 도형 수가 딱 같을 때 — 한 줄도 안 잘린 판에 —
230
+ * 「뒷부분은 안 실렸습니다」 가 나간다. 거짓 경고는 진짜 경고를 죽인다.
231
+ */
221
232
  function 가지풀기(뿌리, 자식표, 줄들, 깊이, 셈) {
222
- if (셈.남은 <= 0) return;
233
+ if (셈.남은 <= 0) { 셈.잘림 = true; return; }
223
234
  const 들여 = ' '.repeat(Math.min(깊이, 12));
224
235
  const 갈래 = typeof 뿌리.type === 'string' ? 뿌리.type : String(뿌리.type ?? '?');
225
236
  const 이름 = String(뿌리.name ?? '').trim();
@@ -270,6 +281,11 @@ export function readFig(경로또는버퍼, { 최대노드 = 노드최대 } = {}
270
281
  const 메타몸 = 꾸러미.files.get('meta.json');
271
282
  if (메타몸) { try { 메타 = JSON.parse(메타몸.toString('utf8')); } catch { 메타 = null; } }
272
283
  알맹이 = 꾸러미.files.get('canvas.fig');
284
+ // **있는데 못 푼** 것은 「없다」 가 아니다 — 사람이 파일 안을 뒤지다 헛걸음한다(2.0.0 3회차).
285
+ const 못푼알맹이 = !알맹이 && 꾸러미.skipped.find((s) => s.name === 'canvas.fig');
286
+ if (못푼알맹이) {
287
+ return { ok: false, 끝났다: true, error: `.fig 꾸러미 안의 canvas.fig 를 풀지 못했습니다 — ${못푼알맹이.why}` };
288
+ }
273
289
  if (!알맹이) {
274
290
  return {
275
291
  ok: false,
@@ -383,7 +399,8 @@ export function readFig(경로또는버퍼, { 최대노드 = 노드최대 } = {}
383
399
  덩이들.push({ 이름: '도형', 문단들: 줄들 });
384
400
  }
385
401
 
386
- if (셈.남은 <= 0) {
402
+ // 못 편 것이 정말 있을 때만 말한다 (가지풀기 머리말).
403
+ if (셈.잘림) {
387
404
  말.push(`도형이 ${상한노드.toLocaleString('en-US')}개를 넘어 거기까지만 폈습니다 — 뒷부분은 안 실렸습니다`);
388
405
  }
389
406