deel-local-cli 1.12.0 → 1.13.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 (51) hide show
  1. package/README.ko.md +6 -5
  2. package/README.md +6 -5
  3. package/package.json +60 -60
  4. package/src/acp/map.js +30 -25
  5. package/src/acp/serve.js +810 -810
  6. package/src/agent/budget.js +156 -156
  7. package/src/agent/commit.js +32 -9
  8. package/src/agent/compact.js +441 -420
  9. package/src/agent/effort.js +308 -308
  10. package/src/agent/evolve.js +263 -263
  11. package/src/agent/loop.js +1728 -1707
  12. package/src/agent/memory.js +156 -156
  13. package/src/agent/mention.js +210 -210
  14. package/src/agent/recall.js +22 -9
  15. package/src/agent/session.js +1056 -1049
  16. package/src/backend/adapter.js +1355 -1159
  17. package/src/backend/cachemark.js +223 -150
  18. package/src/backend/detect.js +327 -327
  19. package/src/backend/http.js +3 -1
  20. package/src/backend/mcp.js +15 -3
  21. package/src/backend/price.js +69 -6
  22. package/src/backend/probe.js +17 -3
  23. package/src/backend/quota.js +274 -250
  24. package/src/backend/tokens.js +25 -3
  25. package/src/backend/toolfit.js +352 -352
  26. package/src/backend/wire.js +715 -600
  27. package/src/commands.js +3184 -3135
  28. package/src/config.js +39 -0
  29. package/src/i18n/en.js +545 -544
  30. package/src/i18n/ja.js +498 -497
  31. package/src/i18n/ko.js +592 -591
  32. package/src/i18n/zh.js +498 -497
  33. package/src/oneshot.js +625 -620
  34. package/src/pack/tar.js +23 -1
  35. package/src/pack/zip.js +19 -1
  36. package/src/plugins/manage.js +85 -5
  37. package/src/repl.js +2616 -2602
  38. package/src/safety/audit.js +22 -3
  39. package/src/safety/guard.js +124 -9
  40. package/src/safety/network.js +43 -0
  41. package/src/tools/fastgrep.js +15 -0
  42. package/src/tools/fsutil.js +291 -265
  43. package/src/tools/index.js +40 -3
  44. package/src/tools/jobs.js +87 -6
  45. package/src/tools/label.js +52 -0
  46. package/src/tools/spawn.js +114 -0
  47. package/src/tools/verify.js +358 -358
  48. package/src/tools/webfetch.js +28 -3
  49. package/src/ui/export.js +26 -10
  50. package/src/ui/pick.js +115 -115
  51. package/src/ui/status.js +617 -613
@@ -2,7 +2,7 @@
2
2
  // 그래야 그 관례로 쓰인 스킬·명령이 그대로 먹는다.
3
3
  import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, statSync, renameSync, cpSync, rmSync } from 'node:fs';
4
4
  import { dirname, extname, join, relative, sep } from 'node:path';
5
- import { execFile } from 'node:child_process';
5
+ import { 무리로돌리기 } from './spawn.js';
6
6
  import { globToRegex, walk, readText, readTextFull, 내부살림 } from './fsutil.js';
7
7
  import { 건너뜀말 } from './ignore.js';
8
8
  import { encode, label as encLabel, decode as decodeBytes, consoleCodepage, looksBinary } from './encoding.js';
@@ -1518,9 +1518,34 @@ export const TOOLS = {
1518
1518
  */
1519
1519
  let 건너뛴것 = 0;
1520
1520
  let 멈춤 = null;
1521
+ /*
1522
+ * ── 이 길에서도 숨을 쉬어야 한다 ────────────────────────────────────
1523
+ *
1524
+ * 바로 위 머리말이 「그동안 화면은 멈춰 있고 Ctrl+C 도 안 먹는다」 고
1525
+ * 적어 뒀다. rg 를 비동기로 부르는 것으로 그걸 고쳤는데, **rg 가 없는
1526
+ * 기계는 여기로 내려온다** — 그리고 여기는 그대로 한 덩어리였다.
1527
+ *
1528
+ * rg 없는 기계가 드물지 않다. 갓 띄운 리눅스 컨테이너, 회사 이미지,
1529
+ * CI 러너가 대개 그렇다(우리 CI 도 그래서 이 자리를 재는 검사가 빨간불
1530
+ * 이었다). 그런 곳에서 큰 폴더를 한 번 찾으면 그 몇 초 동안 상태줄도
1531
+ * 안 돌고 ESC 도 안 먹는다. 오류가 아니라서 아무 데도 안 남고, 사람 눈에는
1532
+ * 「가끔 멈춘다」 로만 보인다.
1533
+ *
1534
+ * 그래서 파일을 몇 개 볼 때마다 한 번씩 자리를 내준다. 밀린 타이머와
1535
+ * 눌린 키가 그 틈에 처리된다. 한 번 내주는 값은 마이크로초 단위라
1536
+ * 찾는 시간에는 거의 안 보태지고, 그 사이 파일 목록이 바뀌지도 않는다
1537
+ * (목록은 위에서 이미 다 떠 놨다).
1538
+ */
1539
+ const 숨돌릴때마다 = 200;
1540
+ let 본것 = 0;
1521
1541
  for (const f of files) {
1522
1542
  // 도중에 Ctrl+C 를 눌렀으면 여기서 그만둔다. 찾은 데까지는 준다.
1523
1543
  if (ctx.signal?.aborted) { 멈춤 = '중단'; break; }
1544
+ if ((본것 += 1) % 숨돌릴때마다 === 0) {
1545
+ await new Promise((r) => setImmediate(r));
1546
+ // 자리를 내준 사이에 눌렸을 수 있다. 내주고 나면 다시 본다.
1547
+ if (ctx.signal?.aborted) { 멈춤 = '중단'; break; }
1548
+ }
1524
1549
  if ((f.size ?? 0) > GREP_MAX_FILE) { 건너뛴것++; continue; }
1525
1550
  if (안읽을확장자.test(f.rel)) { 건너뛴것++; continue; }
1526
1551
  let text;
@@ -1686,7 +1711,7 @@ export const TOOLS = {
1686
1711
  // 윈도우 명령창은 UTF-8 이 아니다. 한국어 윈도우는 CP949 로 뱉는다.
1687
1712
  // 이걸 utf8 이라고 하고 받으면 한글이 통째로 깨진다 — '파싱 성공' 이
1688
1713
  // '�Ľ� ����' 이 된다. 바이트로 받아 이 컴퓨터가 쓰는 것으로 해독한다.
1689
- const kid = execFile(shell.file, shell.args, {
1714
+ const kid = 무리로돌리기(shell.file, shell.args, {
1690
1715
  cwd: ctx.scope.root,
1691
1716
  // 열쇠만 빼고 나머지는 그대로 물려준다 (backend/mcp.js 열쇠뺀환경 머리말).
1692
1717
  // 안 빼면 `env` 한 줄이 열쇠를 화면과 대화 기록에 그대로 싣는다.
@@ -1699,6 +1724,10 @@ export const TOOLS = {
1699
1724
  /*
1700
1725
  * 유닉스에서는 **무리를 만들어 둔다.**
1701
1726
  *
1727
+ * 이 한 줄 때문에 execFile 을 안 쓴다 — 그쪽은 detached 를 spawn 에
1728
+ * 안 넘기고 조용히 버린다(tools/spawn.js 의 무리로돌리기 머리말).
1729
+ * 여기 이 줄이 열두 판 동안 아무 일도 안 하고 있었다.
1730
+ *
1702
1731
  * 나중에 만들어 줄 방법이 없어서 띄우는 순간에 정해야 한다. 안 만들면
1703
1732
  * 나중에 손자를 가리킬 길이 아예 없다 — `sh -c "cd app && npm start"`
1704
1733
  * 처럼 복합 명령이면 sh 가 exec 로 갈아치우지 않고 fork 하므로,
@@ -1833,7 +1862,15 @@ export const TOOLS = {
1833
1862
  * 있다 — 검사가 쓰는 명령이 `node ticker.cjs` 하나뿐이라
1834
1863
  * 유닉스 sh 가 그걸 exec 로 갈아치웠고, 애초에 손자가 안 생겼다.
1835
1864
  */
1836
- 무리끊기(kid.pid);
1865
+ /*
1866
+ * 올려치는 시간도 **부르는 쪽이 정한 것**을 쓴다.
1867
+ *
1868
+ * 윗 갈래(윈도우)는 더줄까 를 받아 쓰는데 여기는 안 받고 있었다.
1869
+ * 그래서 시간 초과에서 넉넉히 주라고 4초를 넘겨도 유닉스에서는
1870
+ * 늘 기본값 그대로였다 — 같은 판단이 두 벌로 있으면 늘 한쪽만
1871
+ * 고쳐진다는 이야기가 이 함수 안에서 또 한 번 났다.
1872
+ */
1873
+ 무리끊기(kid.pid, { 늦게: 더줄까 > 0 ? Math.min(더줄까, 2000) : 800 });
1837
1874
  }
1838
1875
  try { kid.kill(); } catch {}
1839
1876
  // 죽이라고 시켰다고 곧바로 죽는 것은 아니다. 그동안 이 프로세스가
package/src/tools/jobs.js CHANGED
@@ -157,17 +157,98 @@ export function 잘렸나(err) {
157
157
  * 만들어 줄 방법은 없다 — 그래서 이 판단이 띄우는 자리와 짝을 이뤄야 한다.
158
158
  * (Bash 도구가 한동안 이 짝을 안 맞춰서, 윈도우만 고쳐지고 유닉스는 그대로였다.)
159
159
  */
160
+ /**
161
+ * 그 무리가 아직 살아 있나. 죽었으면 false.
162
+ *
163
+ * 무리로 못 물어보면 **그 하나에게** 물어본다 — 위 신호주기() 와 같은 까닭이다.
164
+ * 무리가 없는 판에서 「무리가 없다 = 다 죽었다」 로 읽으면, 살아 있는 자식을
165
+ * 죽은 것으로 세고 그냥 지나간다.
166
+ */
167
+ function 무리살아있나(pid) {
168
+ try { process.kill(-pid, 0); return true; } catch { /* 무리가 없다 */ }
169
+ try { process.kill(pid, 0); return true; } catch { return false; }
170
+ }
171
+
172
+ /**
173
+ * 동기로 잠깐 쉰다.
174
+ *
175
+ * `setTimeout` 이 아니라 이걸 쓰는 까닭은 아래 무리끊기 머리말에 있다 —
176
+ * 이 자리는 프로그램이 끝나는 길에서도 지나가고, 그 자리에서는 비동기
177
+ * 일감이 하나도 안 돈다.
178
+ */
179
+ function 동기로쉬기(ms) {
180
+ try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
181
+ catch { /* SharedArrayBuffer 가 막힌 판이면 그냥 지나간다 */ }
182
+ }
183
+
184
+ /**
185
+ * 신호를 보낸다. **무리째 못 보내면 그 하나에게라도 보낸다.**
186
+ *
187
+ * ── 왜 두 번 시도하나 ──────────────────────────────────────────────────
188
+ *
189
+ * `-pid` 는 「그 무리 전부」 라는 뜻이고, 그 무리는 띄울 때 `detached` 로
190
+ * 만들어 둔 것이다. 그런데 그게 늘 있으리라 믿으면 안 된다 —
191
+ *
192
+ * · `detached` 가 안 먹은 판 (컨테이너 안, 이상한 실행 환경)
193
+ * · 이미 무리 우두머리가 죽고 남은 놈이 딴 무리로 넘어간 자리
194
+ *
195
+ * 이런 자리에서 `kill(-pid, …)` 는 ESRCH 로 튕긴다. 그러면 여태는 그
196
+ * 자리에서 **아무것도 안 하고 돌아갔다.** 무리로 못 보냈다는 것이 「보낼
197
+ * 것이 없다」 는 뜻은 아닌데, 그렇게 다뤘다. 무리로 못 보내면 적어도 우리가
198
+ * 아는 그 하나(자식)에게는 보낸다 — 그게 손자를 못 잡더라도, 아무것도 안
199
+ * 하는 것보다는 낫다.
200
+ *
201
+ * @returns {boolean} 하나라도 닿았으면 true
202
+ */
203
+ function 신호주기(pid, 신호) {
204
+ let 닿았나 = false;
205
+ try { process.kill(-pid, 신호); 닿았나 = true; } catch { /* 무리가 없다 */ }
206
+ if (!닿았나) {
207
+ try { process.kill(pid, 신호); 닿았나 = true; } catch { /* 그마저 죽었다 */ }
208
+ }
209
+ return 닿았나;
210
+ }
211
+
160
212
  export function 무리끊기(pid, { 곧장 = false, 늦게 = 800 } = {}) {
161
213
  if (process.platform === 'win32' || !pid) return;
162
214
  if (곧장) {
163
- try { process.kill(-pid, 'SIGKILL'); } catch { /* 이미 죽음 */ }
215
+ 신호주기(pid, 'SIGKILL');
164
216
  return;
165
217
  }
166
- try { process.kill(-pid, 'SIGTERM'); } catch { /* 무리가 없거나 이미 죽음 */ }
167
- try {
168
- const 시계 = setTimeout(() => { try { process.kill(-pid, 'SIGKILL'); } catch { /* 죽었다 */ } }, 늦게);
169
- 시계.unref?.();
170
- } catch { /* 끝나는 중이면 타이머를 못 건다 — 부르는 쪽이 곧장 으로 다시 온다 */ }
218
+ 신호주기(pid, 'SIGTERM');
219
+
220
+ /*
221
+ * ── 올려치기를 **동기로** 한다 ────────────────────────────────────────
222
+ *
223
+ * 여기가 `setTimeout(…, 늦게).unref()` 였다. 두 가지가 어긋났다.
224
+ *
225
+ * 1. `unref()` 한 타이머는 **프로세스를 안 붙잡는다.** 시간 초과로
226
+ * 끊고 곧바로 끝나는 길에서는 그 타이머가 아예 안 뜬다. 즉 SIGTERM
227
+ * 을 무시하는 손자(제 신호 처리기를 단 dev 서버·watch 프로세스)는
228
+ * 그대로 살아남는다. 「죽이라고 말은 했는데 안 죽는」 자리다.
229
+ * 2. 부르는 쪽은 이 함수가 **돌아오면 끝난 줄로 안다.** 실제로는 아직
230
+ * TERM 만 보낸 상태라, 바로 뒤에서 「진짜 멈췄나」 를 보면 살아 있다.
231
+ * 이 비대칭이 윈도우와 유닉스 사이에도 있었다 — 윈도우 쪽은
232
+ * taskkill 을 동기로 기다린다(나무죽이기 머리말).
233
+ *
234
+ * 그래서 곱게 말한 뒤 **짧게 나눠 지켜보다가**, 그래도 살아 있으면 그
235
+ * 자리에서 끊는다. 얌전히 끝나는 무리는 첫 20ms 에 사라지므로 보통은
236
+ * 거의 안 기다린다. 안 죽는 놈에게만 `늦게` 만큼 쓴다.
237
+ */
238
+ const 조각 = 20;
239
+ for (let 쓴시간 = 0; 쓴시간 < 늦게; 쓴시간 += 조각) {
240
+ if (!무리살아있나(pid)) return;
241
+ 동기로쉬기(조각);
242
+ }
243
+ /*
244
+ * 마지막 한 방은 **묻지 않고** 보낸다.
245
+ *
246
+ * 여기가 `if (무리살아있나(pid))` 였다. 그 물음은 틀릴 수 있다 — 권한이
247
+ * 없으면(EPERM) 살아 있는데도 「없다」 가 나오고, 그러면 안 죽이고 지나간다.
248
+ * 이미 죽은 것에 SIGKILL 을 보내는 값은 예외 하나뿐이고 그건 여기서 삼킨다.
249
+ * 안 죽은 것을 살려 보내는 값은 포트를 문 프로세스가 남는 것이다. 값이 다르다.
250
+ */
251
+ 신호주기(pid, 'SIGKILL');
171
252
  }
172
253
 
173
254
  /**
@@ -0,0 +1,52 @@
1
+ // 도구 부름 한 줄을 적을 때 **무엇으로 그 부름을 알아보나.**
2
+ //
3
+ // ── 왜 파일 하나를 따로 두나 ────────────────────────────────────────────
4
+ //
5
+ // `Read` 만 적으면 열 줄이 전부 `Read` 다. 무엇을 읽었는지가 빠지면 목록을
6
+ // 훑어보는 뜻이 없어진다. 그래서 인자에서 「이 부름을 대표하는 값」 하나를
7
+ // 뽑아 괄호에 넣는다 — `Read(src/a.js)`.
8
+ //
9
+ // 그 「대표하는 값」 을 고르는 **차례**가 여태 다섯 곳에 따로 적혀 있었다:
10
+ //
11
+ // src/repl.js · src/oneshot.js · src/acp/map.js · src/ui/export.js ·
12
+ // src/safety/audit.js
13
+ //
14
+ // 다섯이 조금씩 달랐다. 어떤 데는 `url` 이 있고 어떤 데는 없다. 새 도구가
15
+ // 새 인자 이름을 쓰면 그중 한두 곳만 고쳐지고, 나머지에서는 그 도구가 **빈
16
+ // 괄호**로 남는다 — 화면에도, 내보낸 보고서에도, 감사기록에도. 그 셋이
17
+ // 서로 다른 말을 하기 시작하면 무엇이 맞는지 알 길이 없다.
18
+ //
19
+ // 목록은 여기 하나다. 부르는 쪽은 제 사정(길이 자르기·일감 번호 같은 것)만
20
+ // 따로 얹는다.
21
+
22
+ /**
23
+ * 이 인자가 무엇에 관한 부름인지 말해 주는 이름들. **차례가 뜻이다** —
24
+ * 앞엣것이 더 또렷하다.
25
+ *
26
+ * file_path 파일을 다루는 도구 (Read·Write·Edit)
27
+ * pattern 찾는 도구 (Grep·Glob)
28
+ * path 폴더를 다루는 도구
29
+ * url 웹을 읽는 도구
30
+ * name 이름으로 부르는 것 (스킬·플러그인·MCP)
31
+ * purpose/목적 하위 작업 — 무엇을 시켰는지가 곧 그 부름이다
32
+ * command 셸 — 제일 뒤다. 길고, 앞엣것이 있으면 그쪽이 더 또렷하다.
33
+ */
34
+ export const 이름칸들 = ['file_path', 'pattern', 'path', 'url', 'name', 'purpose', '목적'];
35
+
36
+ /**
37
+ * 인자에서 대표하는 값 하나. 없으면 null.
38
+ *
39
+ * `command` 는 **여기서 안 본다.** 글이 길어서 부르는 쪽마다 자르는 길이가
40
+ * 다르고(화면은 52자, 기록은 통째로), 그 판단까지 여기서 하면 한쪽이 반드시
41
+ * 어색해진다. 부르는 쪽이 이 자를 먼저 물어보고, 없으면 제 방식으로 command
42
+ * 를 다룬다.
43
+ */
44
+ export function 첫이름(인자) {
45
+ const a = 인자 ?? {};
46
+ for (const 칸 of 이름칸들) {
47
+ const v = a[칸];
48
+ if (typeof v === 'string' && v) return v;
49
+ if (typeof v === 'number') return String(v);
50
+ }
51
+ return null;
52
+ }
@@ -97,3 +97,117 @@ export function 돌려보기(이름, 인자, { timeout = 20000, maxBuffer = 32 *
97
97
  아이.on('close', (코드) => 끝내기({ error: null, status: 코드, stdout: 밖, stderr: 탈 }));
98
98
  });
99
99
  }
100
+
101
+
102
+ /*
103
+ * ── `execFile` 은 `detached` 를 **버린다** ──────────────────────────────
104
+ *
105
+ * Bash 도구가 이렇게 부르고 있었다.
106
+ *
107
+ * execFile(셸, 인자, { cwd, env, timeout, maxBuffer, detached: true, … })
108
+ *
109
+ * 그리고 그 옆에 「유닉스에서는 무리를 만들어 둔다」 는 머리말이 열두 줄
110
+ * 붙어 있었다. 그 줄은 **아무 일도 안 하고 있었다.** Node 의 execFile 은
111
+ * 받은 옵션을 spawn 에 통째로 넘기지 않는다. 제 소스에 목록이 박혀 있다 —
112
+ *
113
+ * cwd · env · gid · shell · signal · uid ·
114
+ * windowsHide · windowsVerbatimArguments
115
+ *
116
+ * `detached` 가 없다. 오타도 아니고 오류도 아니라서, 적어 둔 사람도 읽는
117
+ * 사람도 그게 안 걸린 줄 모른다. 리눅스 CI 가 열 판 넘게 이 자리에서
118
+ * 빨갰는데, 무엇이 살아남았는지를 검사가 적기 시작하고서야 보였다.
119
+ *
120
+ * pid 6791 · pgid 1938 · ppid 1 node ticker.cjs
121
+ *
122
+ * 무리(pgid)가 제 pid 가 아니라 **검사 프로세스의 무리**였다. 즉 애초에
123
+ * 무리가 안 만들어졌고, `kill(-pid, …)` 는 죽일 것을 못 찾고 있었다.
124
+ * 죽이는 쪽을 아무리 고쳐도 안 고쳐질 자리였다.
125
+ *
126
+ * 그래서 spawn 을 바로 쓴다. execFile 이 해 주던 것(모아 담기·상한·시한·
127
+ * 오류 모양)은 여기서 **같은 모양으로** 해 준다 — 부르는 쪽의 판단 코드를
128
+ * 한 줄도 안 건드리려는 것이다. 이 파일 머리말과 같은 자세다.
129
+ */
130
+
131
+ /**
132
+ * execFile 과 같은 약속으로 부르되, **무리를 진짜로 만든다.**
133
+ *
134
+ * 콜백은 execFile 의 것과 같다 — `(탈, 밖Buffer, 탈Buffer)`.
135
+ * 탈의 모양도 맞춘다:
136
+ *
137
+ * 못 돌렸다 탈.code = 'ENOENT' 같은 글자
138
+ * 출력이 넘쳤다 탈.code = 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER', 탈.killed = true
139
+ * 시한을 넘겼다 탈.killed = true, 탈.signal = 보낸 신호
140
+ * 시그널로 죽었다 탈.signal = 그 신호
141
+ * 0 이 아니게 끝남 탈.code = 그 숫자
142
+ *
143
+ * @returns {import('node:child_process').ChildProcess}
144
+ */
145
+ export function 무리로돌리기(파일, 인자, 옵션 = {}, 끝나면 = () => {}) {
146
+ const {
147
+ cwd = undefined, env = undefined, timeout = 0, maxBuffer = 32 * 1024 * 1024,
148
+ windowsHide = true, windowsVerbatimArguments = false, detached = false,
149
+ killSignal = 'SIGTERM',
150
+ } = 옵션;
151
+
152
+ const 아이 = spawn(파일, 인자, {
153
+ cwd, env, windowsHide, windowsVerbatimArguments, detached,
154
+ });
155
+
156
+ const 밖조각 = [];
157
+ const 탈조각 = [];
158
+ let 밖크기 = 0;
159
+ let 탈크기 = 0;
160
+ let 끝났나 = false;
161
+ let 미리잡은탈 = null;
162
+
163
+ const 모으기 = (스트림, 조각들, 더할것) => {
164
+ 조각들.push(더할것);
165
+ return 스트림 + 더할것.length;
166
+ };
167
+
168
+ const 끝내기 = (탈) => {
169
+ if (끝났나) return;
170
+ 끝났나 = true;
171
+ clearTimeout(시계);
172
+ 끝나면(탈 ?? null, Buffer.concat(밖조각), Buffer.concat(탈조각));
173
+ };
174
+
175
+ /** execFile 이 만드는 것과 같은 모양의 탈. */
176
+ const 탈만들기 = (글, 더할것) => Object.assign(new Error(글), 더할것);
177
+
178
+ const 넘쳤다 = () => {
179
+ 미리잡은탈 = 탈만들기('stdout maxBuffer length exceeded',
180
+ { code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER', killed: true });
181
+ try { 아이.kill(killSignal); } catch { /* 이미 죽었다 */ }
182
+ };
183
+
184
+ 아이.stdout?.on('data', (조각) => {
185
+ 밖크기 = 모으기(밖크기, 밖조각, 조각);
186
+ if (밖크기 > maxBuffer && !미리잡은탈) 넘쳤다();
187
+ });
188
+ 아이.stderr?.on('data', (조각) => {
189
+ 탈크기 = 모으기(탈크기, 탈조각, 조각);
190
+ if (탈크기 > maxBuffer && !미리잡은탈) 넘쳤다();
191
+ });
192
+
193
+ /*
194
+ * 시한은 **마지막 그물**이다. 부르는 쪽이 제 시계로 먼저 끊는 것이 보통이고,
195
+ * 이건 그것마저 못 돌았을 때 선다. unref 하지 않는다 — 여기서 놓아 버리면
196
+ * 그물이 아니게 된다.
197
+ */
198
+ const 시계 = timeout > 0 ? setTimeout(() => {
199
+ 미리잡은탈 = 탈만들기(`명령이 ${timeout}ms 안에 안 끝났습니다`,
200
+ { killed: true, signal: killSignal });
201
+ try { 아이.kill(killSignal); } catch { /* 이미 죽었다 */ }
202
+ }, timeout) : null;
203
+
204
+ 아이.on('error', (탈) => 끝내기(탈));
205
+ 아이.on('close', (코드, 신호) => {
206
+ if (미리잡은탈) return 끝내기(미리잡은탈);
207
+ if (신호) return 끝내기(탈만들기(`${신호} 시그널로 죽었습니다`, { killed: true, signal: 신호 }));
208
+ if (코드 !== 0) return 끝내기(탈만들기(`종료코드 ${코드}`, { code: 코드, killed: false }));
209
+ 끝내기(null);
210
+ });
211
+
212
+ return 아이;
213
+ }