deel-local-cli 2.0.1 → 2.0.3

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 (58) hide show
  1. package/README.ko.md +13 -5
  2. package/README.md +13 -5
  3. package/package.json +1 -1
  4. package/src/acp/serve.js +49 -12
  5. package/src/agent/commit.js +2 -2
  6. package/src/agent/compact.js +4 -4
  7. package/src/agent/grade.js +1 -1
  8. package/src/agent/loop.js +2 -2
  9. package/src/agent/memory.js +1 -1
  10. package/src/agent/models.js +1 -1
  11. package/src/agent/outschema.js +1 -1
  12. package/src/agent/pins.js +7 -2
  13. package/src/agent/project.js +1 -1
  14. package/src/agent/recall.js +1 -1
  15. package/src/agent/store.js +13 -26
  16. package/src/backend/ctxsize.js +2 -2
  17. package/src/backend/http.js +1 -1
  18. package/src/backend/learn.js +27 -1
  19. package/src/backend/mcp.js +97 -16
  20. package/src/backend/proxy.js +1 -1
  21. package/src/backend/quota.js +2 -2
  22. package/src/commands.js +4 -2
  23. package/src/completion.js +13 -3
  24. package/src/config.js +33 -11
  25. package/src/configexplain.js +1 -1
  26. package/src/jsonl.js +42 -0
  27. package/src/lsp/client.js +6 -46
  28. package/src/oneshot.js +22 -0
  29. package/src/pack/selfpack.js +2 -2
  30. package/src/pack/zip.js +18 -1
  31. package/src/plugins/manage.js +81 -14
  32. package/src/reap.js +70 -0
  33. package/src/report.js +1 -1
  34. package/src/safety/audit.js +4 -19
  35. package/src/safety/authcmd.js +1 -1
  36. package/src/safety/guard.js +56 -6
  37. package/src/safety/hooks.js +1 -1
  38. package/src/safety/keystore.js +18 -3
  39. package/src/safety/shellenv.js +1 -1
  40. package/src/safety/undo.js +44 -20
  41. package/src/skills/discover.js +1 -1
  42. package/src/tools/docs.js +3 -3
  43. package/src/tools/edit-match.js +1 -1
  44. package/src/tools/encoding.js +1 -1
  45. package/src/tools/excel-com.js +56 -30
  46. package/src/tools/fastgrep.js +1 -1
  47. package/src/tools/fsutil.js +8 -4
  48. package/src/tools/ignore.js +21 -1
  49. package/src/tools/index.js +21 -2
  50. package/src/tools/jobs.js +23 -4
  51. package/src/tools/shell.js +27 -0
  52. package/src/tools/todo.js +1 -1
  53. package/src/tools/webfetch.js +1 -1
  54. package/src/tools/xlsx.js +4 -4
  55. package/src/ui/banner.js +1 -1
  56. package/src/ui/inputbox.js +1 -1
  57. package/src/ui/md.js +1 -1
  58. package/src/ui/motion.js +1 -1
@@ -53,6 +53,10 @@ $dst = $env:DEEL_XL_OUT
53
53
  # 그러면 맞는 암호를 넣은 사람이 「암호가 맞지 않습니다」 를 세 번 받고 끝난다. 나가는 쪽은
54
54
  # 이미 decode() 로 콘솔 인코딩을 풀고 있는데(아래 받은글), 들어오는 쪽만 빠져 있었다.
55
55
  try { [Console]::InputEncoding = New-Object System.Text.UTF8Encoding $false } catch { }
56
+ # 나가는 쪽도 UTF-8 로 (2.0.2 · XL3). 콘솔 코드페이지로 두면 그 판에 없는 글자(일본어 엑셀의 「パスワード」,
57
+ # 중국어 엑셀의 「密码」, 한글 판에 없는 시트 이름)가 「?」 가 되고, 받는 쪽 decode() 가 인코딩을 잘못 짚어
58
+ # 한 줄이 통째로 딴 글자가 됐다 — 그러면 암호를 틀렸다는 말도 못 알아본다.
59
+ try { [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false } catch { }
56
60
  $pw = [Console]::In.ReadLine()
57
61
  if ($null -eq $pw) { $pw = '' }
58
62
 
@@ -89,8 +93,8 @@ try {
89
93
  catch {
90
94
  $m = $_.Exception.Message
91
95
  # 작은따옴표는 PowerShell 에서 이스케이프를 안 푼다. 탭을 넣으려면 큰따옴표여야 한다.
92
- if ($m -match 'password|암호|protected') { Write-Output "ERR\`tPASSWORD" }
93
- elseif ($m -eq 'BUSY') { Write-Output "ERR\`tBUSY" }
96
+ # 「암호가 틀렸다」 는 말은 이 PC 언어로 온다 — 가르는 일은 JS 쪽 암호탓인가 가 한다 (2.0.2 · XL3).
97
+ if ($m -eq 'BUSY') { Write-Output "ERR\`tBUSY" }
94
98
  elseif ($m -eq 'OPENFAIL') { Write-Output "ERR\`tPASSWORD" }
95
99
  else { Write-Output ("ERR\`tOTHER\`t" + $m) }
96
100
  }
@@ -191,6 +195,53 @@ export async function excelToTables(경로, { password = '', timeout = 90000 } =
191
195
  }
192
196
  }
193
197
 
198
+ /**
199
+ * 엑셀이 「암호가 틀렸다」 고 한 말인가 (2.0.2 · XL3).
200
+ *
201
+ * 그 말은 **이 컴퓨터 언어로** 온다. 여기가 파워셸 안의 `'password|암호|protected'` 였는데,
202
+ * 일본어 엑셀은 「パスワード」, 중국어는 「密码」, 독일어는 「Kennwort」 라고 말한다 — 그러면 암호를
203
+ * 틀린 사람이 「암호가 맞지 않습니다」 대신 「엑셀이 열지 못했습니다: …」 를 받고, 모델은 암호를
204
+ * 다시 물을 줄 모른다. 판정은 이쪽에서 한 번만 한다 — 엑셀 없는 판에서도 잴 수 있게.
205
+ */
206
+ const 암호말 = /password|protected|암호|パスワード|密码|密碼|kennwort|mot de passe|contraseña|senha|palavra-passe|пароль|wachtwoord|hasło|parola|lösenord|adgangskode|passord|salasana|jelszó|heslo|mật khẩu/i;
207
+ export function 암호탓인가(말) {
208
+ return 암호말.test(String(말 ?? ''));
209
+ }
210
+
211
+ /**
212
+ * 파워셸이 뱉은 글(이미 푼 것)을 답으로 (2.0.2 · XL3).
213
+ *
214
+ * 돌리기 의 close 안에 있던 것을 떼어 냈다. 그 자리는 엑셀이 깔린 PC 에서만 닿아서,
215
+ * 여기 갈래가 틀려도 잴 길이 없었다.
216
+ */
217
+ export function 엑셀답읽기(out, err = '') {
218
+ const 줄들 = String(out ?? '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
219
+ const 오류 = 줄들.find((l) => l.startsWith('ERR\t'));
220
+ if (오류) {
221
+ const [, 갈래, 말] = 오류.split('\t');
222
+ if (갈래 === 'PASSWORD' || 암호탓인가(말)) return { ok: false, reason: 'password', message: '암호가 맞지 않거나, 이 파일에는 암호가 필요합니다' };
223
+ if (갈래 === 'BUSY') return { ok: false, reason: 'busy', message: '엑셀이 계속 바쁘다고 합니다 — 열려 있는 엑셀 창을 닫고 다시 해보세요' };
224
+ // COM 클래스를 못 찾는다 = 이 컴퓨터에 엑셀이 없다.
225
+ // 이걸 그대로 내보내면 사용자는 알아볼 수 없는 오류 코드만 받는다.
226
+ if (없는엑셀(말)) {
227
+ return { ok: false, reason: 'no-excel', message: '이 컴퓨터에 엑셀이 설치되어 있지 않습니다 — 암호가 걸린 파일과 옛 .xls 는 엑셀이 있어야 읽을 수 있습니다' };
228
+ }
229
+ return { ok: false, reason: 'other', message: `엑셀이 열지 못했습니다: ${말 ?? ''}`.trim() };
230
+ }
231
+ if (!줄들.includes('OK')) {
232
+ const 첫줄 = String(err ?? '').split('\n')[0] ?? '';
233
+ if (없는엑셀(err)) {
234
+ return { ok: false, reason: 'no-excel', message: '이 컴퓨터에 엑셀이 설치되어 있지 않습니다' };
235
+ }
236
+ return { ok: false, reason: 'other', message: `엑셀이 끝내지 못했습니다${첫줄 ? ` — ${첫줄}` : ''}` };
237
+ }
238
+ const 시트들 = 줄들.filter((l) => l.startsWith('SHEET\t')).map((l) => {
239
+ const [, i, ...name] = l.split('\t');
240
+ return { i: Number(i), name: name.join('\t') || `시트${i}` };
241
+ });
242
+ return { ok: true, 시트들 };
243
+ }
244
+
194
245
  function 돌리기(경로, 밖, password, timeout) {
195
246
  return new Promise((done) => {
196
247
  // 스크립트는 -EncodedCommand 로 넘긴다. 임시 .ps1 파일을 안 만들려는 것이다 —
@@ -202,7 +253,8 @@ function 돌리기(경로, 밖, password, timeout) {
202
253
  env: { ...process.env, DEEL_XL_IN: 경로, DEEL_XL_OUT: 밖 },
203
254
  });
204
255
 
205
- // PowerShell 은 UTF-8 이 아니라 이 컴퓨터 콘솔 인코딩으로 뱉는다.
256
+ // PowerShell 은 UTF-8 이 아니라 이 컴퓨터 콘솔 인코딩으로 뱉는다(스크립트가 UTF-8 로 돌려
257
+ // 놓지만, 그게 안 먹는 판도 있다).
206
258
  // utf8 이라고 하고 받으면 오류 메시지가 통째로 깨져서, 무엇이 잘못됐는지
207
259
  // 알아볼 수 없는 글자가 사용자에게 그대로 간다. 바이트로 모아 뒤에 푼다.
208
260
  const 밖조각 = [];
@@ -232,33 +284,7 @@ function 돌리기(경로, 밖, password, timeout) {
232
284
  if (끝남) return;
233
285
  끝남 = true;
234
286
  clearTimeout(시계);
235
- const out = 풀기(밖조각);
236
- const err = 풀기(오류조각);
237
- const 줄들 = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
238
- const 오류 = 줄들.find((l) => l.startsWith('ERR\t'));
239
- if (오류) {
240
- const [, 갈래, 말] = 오류.split('\t');
241
- if (갈래 === 'PASSWORD') return done({ ok: false, reason: 'password', message: '암호가 맞지 않거나, 이 파일에는 암호가 필요합니다' });
242
- if (갈래 === 'BUSY') return done({ ok: false, reason: 'busy', message: '엑셀이 계속 바쁘다고 합니다 — 열려 있는 엑셀 창을 닫고 다시 해보세요' });
243
- // COM 클래스를 못 찾는다 = 이 컴퓨터에 엑셀이 없다.
244
- // 이걸 그대로 내보내면 사용자는 알아볼 수 없는 오류 코드만 받는다.
245
- if (없는엑셀(말)) {
246
- return done({ ok: false, reason: 'no-excel', message: '이 컴퓨터에 엑셀이 설치되어 있지 않습니다 — 암호가 걸린 파일과 옛 .xls 는 엑셀이 있어야 읽을 수 있습니다' });
247
- }
248
- return done({ ok: false, reason: 'other', message: `엑셀이 열지 못했습니다: ${말 ?? ''}`.trim() });
249
- }
250
- if (!줄들.includes('OK')) {
251
- const 첫줄 = err.split('\n')[0] ?? '';
252
- if (없는엑셀(err)) {
253
- return done({ ok: false, reason: 'no-excel', message: '이 컴퓨터에 엑셀이 설치되어 있지 않습니다' });
254
- }
255
- return done({ ok: false, reason: 'other', message: `엑셀이 끝내지 못했습니다${첫줄 ? ` — ${첫줄}` : ''}` });
256
- }
257
- const 시트들 = 줄들.filter((l) => l.startsWith('SHEET\t')).map((l) => {
258
- const [, i, ...name] = l.split('\t');
259
- return { i: Number(i), name: name.join('\t') || `시트${i}` };
260
- });
261
- done({ ok: true, 시트들 });
287
+ done(엑셀답읽기(풀기(밖조각), 풀기(오류조각)));
262
288
  });
263
289
 
264
290
  // 암호는 여기로만 나간다. 줄바꿈까지 보내고 바로 닫는다.
@@ -235,7 +235,7 @@ export function rg가못준까닭(r) {
235
235
  */
236
236
  const 이음 = '[\\x80-\\xBF]';
237
237
  const 이음아님 = '(?:[^\\x80-\\xBF]|$)';
238
- export const 못푸는바이트무늬 = '(?-u:'
238
+ const 못푸는바이트무늬 = '(?-u:'
239
239
  + [
240
240
  '\\x00',
241
241
  '[\\xC0\\xC1\\xF5-\\xFF]',
@@ -109,10 +109,14 @@ export function 내부살림(abs) {
109
109
  * Write .deel/config.json::$DATA **설정 본체를 덮어썼다**
110
110
  * cat .deel/audit.jsonl::$DATA checkPaths 통과
111
111
  *
112
- * 드라이브 조각(`C:`)만 빼고 첫 `:` 뒤를 버린다. 유닉스에서 `:` 는 그냥 글자라 막는 쪽으로
113
- * 넓어질 뿐이다(`history:x` 라는 살림 이름을 따로 쓸 까닭이 없다).
112
+ * 드라이브 조각(`C:`)만 빼고 첫 `:` 뒤를 버린다.
113
+ *
114
+ * **윈도에서만** 그렇게 한다 (2.0.2 · 480). 유닉스·맥에서 `:` 는 그냥 글자이고 스트림이 없다 —
115
+ * `.deel/config.json:v2` 는 설정 파일이 아니라 딴 파일이다. 모든 판에서 벗겼더니 리눅스에서 그런
116
+ * 이름을 살림으로 잘못 알고 막았다(안전 쪽이지만 까닭 없는 막힘이다).
114
117
  */
115
- const 조각 = 편.split('/').map((x, 몇째) => (몇째 === 0 && /^[A-Za-z]:$/.test(x) ? x : x.replace(/:.*$/s, '')));
118
+ const 스트림꼴 = process.platform === 'win32';
119
+ const 조각 = 편.split('/').map((x, 몇째) => (!스트림꼴 || (몇째 === 0 && /^[A-Za-z]:$/.test(x)) ? x : x.replace(/:.*$/s, '')));
116
120
  const 이름 = (조각[조각.length - 1] ?? '').toLowerCase();
117
121
  /*
118
122
  * 살림 자리를 `.deel` 이라는 **글자**로 찾고 있었다.
@@ -405,7 +409,7 @@ export function 훑기상한(env = process.env) {
405
409
  * 자주 쉬면 훑기 자체가 느려지고, 드물게 쉬면 그만큼 귀를 닫는다. 200개면
406
410
  * 사내망 드라이브에서도 한 숨 사이가 10ms 언저리다.
407
411
  */
408
- export const 숨쉴간격 = 200;
412
+ const 숨쉴간격 = 200;
409
413
  const 한숨 = () => new Promise((풀기) => setImmediate(풀기));
410
414
 
411
415
  /**
@@ -30,11 +30,31 @@ export function 규칙읽기(text, 기준 = '') {
30
30
  return out;
31
31
  }
32
32
 
33
+ /*
34
+ * ── 끝 빈칸 떼기는 git 과 **똑같이** (2.0.2 · G4) ─────────────────────────────
35
+ *
36
+ * 「역슬래시 바로 뒤 빈칸은 살린다」 로 쟀더니 `foo\\ ` 가 틀렸다. 앞의 `\\` 는 역슬래시 **글자**
37
+ * 하나이고 빈칸은 안 살린 것이라 git 은 `foo\` 로 읽는데, 우리는 빈칸까지 남겨 `foo\ ` 를 찾았다.
38
+ * git(dir.c 의 trim_trailing_spaces)을 그대로 옮긴다 — 역슬래시는 다음 글자 하나를 먹고, 떼는 것은
39
+ * 끝에 이어진 **빈칸**(' ')뿐이다. 탭은 git 도 안 뗀다.
40
+ */
41
+ export function 끝빈칸떼기(줄) {
42
+ const p = String(줄 ?? '');
43
+ let 빈칸자리 = -1;
44
+ for (let i = 0; i < p.length; i++) {
45
+ const ch = p[i];
46
+ if (ch === ' ') { if (빈칸자리 < 0) 빈칸자리 = i; continue; }
47
+ if (ch === '\\') { i++; if (i >= p.length) return p; }
48
+ 빈칸자리 = -1;
49
+ }
50
+ return 빈칸자리 >= 0 ? p.slice(0, 빈칸자리) : p;
51
+ }
52
+
33
53
  /** 줄 하나 → 규칙. 빈 줄·주석이면 null. */
34
54
  export function 패턴규칙(줄, 기준 = '') {
35
55
  let p = String(줄 ?? '');
36
56
  // 끝의 빈칸은 뗀다 — 역슬래시로 살린 것만 남긴다.
37
- p = p.replace(/(?<!\\)\s+$/, '');
57
+ p = 끝빈칸떼기(p);
38
58
  if (!p || p.startsWith('#')) return null;
39
59
  let 부정 = false;
40
60
  if (p.startsWith('!')) { 부정 = true; p = p.slice(1); }
@@ -10,7 +10,7 @@ import { 건너뜀말 } from './ignore.js';
10
10
  import { encode, label as encLabel, decode as decodeBytes, consoleCodepage, looksBinary, 바꾼데만쓰기 } from './encoding.js';
11
11
  import { checkCommand, checkPaths, isMutating, 셸이파일에쓰나 } from '../safety/guard.js';
12
12
  import { 띄우기, 나무끊기, 무리끊기, 남은무리끊기, JOBS_TOOL } from './jobs.js';
13
- import { 셸명령 } from './shell.js';
13
+ import { 셸명령, 셸한계넘나 } from './shell.js';
14
14
  import { findMatch, applySpans, reindent, TIER_LABELS, CRLF뿐인가, CRLF로, 꼴맞추기 } from './edit-match.js';
15
15
  import { loadSkill } from '../skills/discover.js';
16
16
  import { WEB_FETCH_TOOL } from './webfetch.js';
@@ -187,7 +187,7 @@ export function 붙박이그림줄이기(줄들) {
187
187
  * 줄을 통째로 지워 버리는 경우가 더 흔하고, 그때는 표시도 같이 사라진다.
188
188
  * 그래서 **개수를 견준다** — 있던 것보다 줄었으면 잃은 것이다.
189
189
  */
190
- export function 박힌그림수(글) {
190
+ function 박힌그림수(글) {
191
191
  return (String(글 ?? '').match(그림자리) ?? []).length;
192
192
  }
193
193
 
@@ -274,6 +274,19 @@ function 파일크기(abs) {
274
274
  try { return statSync(abs).size; } catch { return -1; }
275
275
  }
276
276
 
277
+ /*
278
+ * 쓴 자리에 **파일이 생겼나** (2.0.2 · 379). 안 생겼으면 그 말을 돌려준다.
279
+ *
280
+ * 윈도 10 까지는 `NUL`·`CON`·`aux.txt` 같은 이름이 파일이 아니라 장치다 — 쓰기는 성공하고
281
+ * 아무것도 안 남는다. 그런데 화면은 `새로 만듦: NUL (1줄)` 이었다. 윈도 11 은 진짜 파일을
282
+ * 만들어서 판마다 다르다 — 이름을 외우지 않고 **쓴 결과**를 본다. Write·Append·한글 문서가
283
+ * 같이 쓴다.
284
+ */
285
+ export function 파일안생김(ctx, abs) {
286
+ try { if (statSync(abs).isFile()) return null; } catch { /* 없다 — 아래 말 */ }
287
+ return { error: `${ctx.scope.show(abs)} 에 썼는데 파일이 안 생겼습니다 — 윈도가 이 이름(NUL·CON·AUX·COM1 따위)을 장치로 읽습니다. 다른 이름을 쓰세요.` };
288
+ }
289
+
277
290
  /*
278
291
  * ── 크기만으로는 「그 파일 그대로」 를 모른다 ─────────────────────────────
279
292
  *
@@ -1439,6 +1452,7 @@ function hwpx새로만들기(args, ctx, abs) {
1439
1452
  writeFileSync(abs, 만듦.buf);
1440
1453
  }, () => !existsSync(abs));
1441
1454
  if (못씀) return 못씀;
1455
+ { const 탈 = 파일안생김(ctx, abs); if (탈) return 탈; }
1442
1456
  /*
1443
1457
  * `seen` 에 안 올린다.
1444
1458
  *
@@ -1608,6 +1622,7 @@ function 한파일쓰기(args, ctx) {
1608
1622
  writeFileSync(abs, 만든것.buf);
1609
1623
  }, () => (existed ? !!읽음 && readFileSync(abs).equals(읽음.buf) : !existsSync(abs)));
1610
1624
  if (못씀) return 못씀;
1625
+ { const 탈 = 파일안생김(ctx, abs); if (탈) return 탈; }
1611
1626
  ctx.seen.add(abs);
1612
1627
  const n = args.content.split('\n').length;
1613
1628
  const 표기 = 원래 !== 'utf-8' ? ` · ${encLabel(원래)}${짐작이다 ? ' (짐작)' : ''}` : '';
@@ -2452,6 +2467,7 @@ export const TOOLS = {
2452
2467
  else writeFileSync(abs, 만든것.buf);
2453
2468
  }, () => (existed ? 파일크기(abs) === 지금크기 : !existsSync(abs)));
2454
2469
  if (못씀) return 못씀;
2470
+ { const 탈 = 파일안생김(ctx, abs); if (탈) return 탈; }
2455
2471
  // 붙인 **뒤** 앞머리로 적어 둔다. 64KB 보다 작던 파일은 붙인 조각이 앞머리에 든다.
2456
2472
  잰것캐시.set(abs, {
2457
2473
  지문: 지금크기 >= 인코딩볼바이트 ? 앞지문 : 표본지문(앞머리(abs, 인코딩볼바이트)),
@@ -3055,6 +3071,9 @@ export const TOOLS = {
3055
3071
  // 어느 셸로 넘기나 — tools/shell.js 가 정한다. Jobs 와 같은 답이어야 하므로 한 군데다.
3056
3072
  // (윈도우 cmd 의 따옴표 문제와 그 해법도 거기 적혀 있다.)
3057
3073
  const shell = 셸명령(cmd);
3074
+ // cmd 의 한 줄 한계를 넘으면 돌리기 전에 말한다 (tools/shell.js 셸한계넘나 · 2.0.2 S6).
3075
+ const 한계 = 셸한계넘나(cmd, shell);
3076
+ if (한계) return { error: 한계 };
3058
3077
 
3059
3078
  const 제한 = bash제한시간(args.timeout);
3060
3079
  // 무엇을 빼고 넘길지 여기서 한 번 정한다. 뺀 이름은 아래에서 명령이
package/src/tools/jobs.js CHANGED
@@ -25,9 +25,10 @@
25
25
  */
26
26
  import { spawn } from 'node:child_process';
27
27
  import { decode as decodeBytes, consoleCodepage } from './encoding.js';
28
- import { 셸명령 } from './shell.js';
28
+ import { 셸명령, 셸한계넘나 } from './shell.js';
29
29
  import { 셸환경 } from '../safety/shellenv.js';
30
30
  import { 말, 세말 } from '../i18n/index.js';
31
+ import { 신호에거두기 } from '../reap.js';
31
32
 
32
33
  /* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
33
34
  const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
@@ -319,7 +320,7 @@ export function 무리끊기(pid, { 곧장 = false, 늦게 = 800 } = {}) {
319
320
  export const 못재고기다릴최대 = 400;
320
321
 
321
322
  /** 무리끊기 의 **기다릴 수 있는** 판. 까닭은 바로 위 머리말에. */
322
- export async function 무리끊기기다려(pid, { 곧장 = false, 늦게 = 800 } = {}) {
323
+ async function 무리끊기기다려(pid, { 곧장 = false, 늦게 = 800 } = {}) {
323
324
  if (process.platform === 'win32' || !pid) return;
324
325
  if (곧장) { 신호주기(pid, 'SIGKILL'); return; }
325
326
  신호주기(pid, 'SIGTERM');
@@ -350,6 +351,19 @@ export async function 무리끊기기다려(pid, { 곧장 = false, 늦게 = 800
350
351
  export function 나무죽이기(kid, { 파이프끊기: 끊을까 = true, 곧장 = false, 상한 = 1500, 뿌리살리기 = true } = {}) {
351
352
  if (!kid) return true;
352
353
  let 나무끊었나 = true;
354
+ /*
355
+ * ── 끝난 것으로 본 아이에게는 taskkill 을 안 보낸다 (2.0.2 · 28회차 flake) ──────────
356
+ *
357
+ * Node 는 아이가 끝났다고 알리는 그 자리에서 프로세스 손잡이를 닫는다. 손잡이가 닫히면 윈도는
358
+ * 그 번호를 **곧바로 딴 프로세스에게 준다.** 그 뒤의 `taskkill /pid N /t` 는 우리 아이가 아니라
359
+ * 그 번호를 새로 받은 **남의 나무**를 끊는다 — 윈도 CI 에서 옆 대조군이 말없이 멈춘 것(28회차)이
360
+ * 이 모양이다. 상한에 잘려 다시 오는 길(뿌리살리기)에서 특히 그렇다: 첫 taskkill 이 뿌리를
361
+ * 죽이고 나서 두 번째가 같은 번호로 간다.
362
+ *
363
+ * 잃는 것은 없다. 뿌리가 끝난 뒤에는 /t 가 어차피 그 아래를 못 훑는다(남은무리끊기 머리말).
364
+ * 아직 안 끝난 아이는 손잡이가 열려 있어 번호가 안 바뀐다 — 그때만 보낸다.
365
+ */
366
+ const 끝난아이 = typeof kid.exitCode === 'number' || typeof kid.signalCode === 'string';
353
367
  if (process.platform === 'win32' && kid.pid) {
354
368
  /*
355
369
  * 상한은 부르는 쪽이 정한다. 여기는 끝날 때도 지나가는 자리라
@@ -357,7 +371,7 @@ export function 나무죽이기(kid, { 파이프끊기: 끊을까 = true, 곧장
357
371
  * 그래서 기본은 짧다. 사람이 「끝내」 라고 시킨 자리에서는 길게 준다 —
358
372
  * 거기서는 몇백 ms 늦는 것보다 안 죽는 것이 훨씬 나쁘다.
359
373
  */
360
- 나무끊었나 = 나무끊기(kid.pid, 상한);
374
+ if (!끝난아이) 나무끊었나 = 나무끊기(kid.pid, 상한);
361
375
  /*
362
376
  * 나무를 다 못 훑었으면 뿌리를 살려 둔다. 부르는 쪽이 다시 온다.
363
377
  *
@@ -675,8 +689,11 @@ export async function 띄우기(명령, { cwd, 설명 = null, 기다림 = 1500,
675
689
  return { error: `뒤에서 도는 명령이 이미 ${도는것.length}개입니다. Jobs 로 안 쓰는 것을 끝내고 다시 하세요.` };
676
690
  }
677
691
 
678
- const j = new 일감(다음번호++, 명령, 설명);
679
692
  const shell = 셸명령(명령);
693
+ // cmd 의 한 줄 한계를 넘으면 띄우기 전에 말한다 (tools/shell.js 셸한계넘나 · 2.0.2 S6). 번호를 받기 전이다.
694
+ const 한계 = 셸한계넘나(명령, shell);
695
+ if (한계) return { error: 한계 };
696
+ const j = new 일감(다음번호++, 명령, 설명);
680
697
  let kid;
681
698
  try {
682
699
  kid = spawn(shell.file, shell.args, {
@@ -918,6 +935,8 @@ export function 비우기() { 모두끝내기(); 다음번호 = 1; 치운것 = 0
918
935
 
919
936
  // 어떤 길로 끝나든 남기지 않는다. repl·oneshot 이 부르는 것과 겹쳐도 무해하다.
920
937
  process.once('exit', () => { try { 모두끝내기(); } catch { /* 끝나는 중이라 할 수 있는 게 없다 */ } });
938
+ // 신호(SIGINT·SIGTERM)로 끝나면 'exit' 이 안 돈다 — 그 길도 거둔다 (reap.js 머리말 · 2.0.2 M3).
939
+ 신호에거두기(모두끝내기);
921
940
 
922
941
  /*
923
942
  * ── 도구 ──────────────────────────────────────────────────────────────
@@ -100,6 +100,33 @@ export function 정한셸() {
100
100
  /** 검사용 — 다음 정한셸() 이 다시 고르게. */
101
101
  export function 셸지우기() { 지금 = null; }
102
102
 
103
+ /*
104
+ * ── cmd.exe 는 한 줄에 8,191자까지다 (2.0.2 · S6) ─────────────────────────────
105
+ *
106
+ * cmd 자체의 한계라 우리가 늘릴 수 없다. 넘기면 cmd 가 「The command line is too long.」 한 줄을
107
+ * 내고 **아무것도 안 돌린다** — 그런데 한국어 윈도에서는 그 줄이 코드페이지째 깨져 오고, 모델은
108
+ * 제 명령이 왜 안 돌았는지 모른 채 같은 긴 명령을 또 낸다. 넘길 것이면 넘기기 전에 사람 말로
109
+ * 멈추고, 어떻게 하면 되는지(파일로 써서 돌리기)를 같이 말한다. Git Bash·파워셸은 이 한계가 없다.
110
+ *
111
+ * 8,191자는 명령만이 아니라 **cmd 가 받은 한 줄 전체**다 — `"C:\WINDOWS\system32\cmd.exe" /d /s /c "…"`.
112
+ * 재 보니 이 PC 에서 명령에 쓸 수 있는 것은 8,152자였다(cmd 자리 27자 + 앞뒤 붙이는 것). 그래서 cmd 자리와
113
+ * 붙이는 글자만큼 빼고 잰다. 붙이는 것은 ` /d /s /c ` 10자 · 명령을 싸는 따옴표 2자에 2자를 더 둔다.
114
+ */
115
+ const cmd한줄최대 = 8191;
116
+
117
+ /** 이 cmd 로 명령에 쓸 수 있는 글자 수. */
118
+ export function cmd명령최대(file = 'cmd.exe') { return cmd한줄최대 - (String(file).length + 14); }
119
+
120
+ /** cmd 로 넘기면 한 줄 한계를 넘나. 넘으면 모델에게 줄 말, 아니면 null. */
121
+ export function 셸한계넘나(cmd, { id, file } = 정한셸()) {
122
+ if (id !== 'cmd') return null;
123
+ const n = String(cmd ?? '').length;
124
+ const 최대 = cmd명령최대(file);
125
+ if (n <= 최대) return null;
126
+ return `명령이 ${n.toLocaleString('en-US')}자입니다 — cmd.exe 는 한 줄에 ${cmd한줄최대.toLocaleString('en-US')}자까지만 받습니다(cmd 자리를 빼면 명령에는 ${최대.toLocaleString('en-US')}자 · cmd 자체 한계라 돌리지 않았습니다).`
127
+ + ' 긴 내용은 Write 로 파일(.bat · .ps1 · .js 등)에 쓰고 그 파일을 돌리세요. Git Bash 를 깔면 이 한계가 없습니다.';
128
+ }
129
+
103
130
  /** 명령 하나를 셸에 넘길 모양. Bash 도구와 Jobs 가 둘 다 이걸 쓴다. */
104
131
  export function 셸명령(cmd) {
105
132
  const s = 정한셸();
package/src/tools/todo.js CHANGED
@@ -13,7 +13,7 @@ import { 말 } from '../i18n/index.js';
13
13
 
14
14
  /* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
15
15
  const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
16
- export const STATES = ['todo', 'doing', 'done'];
16
+ const STATES = ['todo', 'doing', 'done'];
17
17
 
18
18
  const 표시 = { todo: '☐', doing: '▶', done: '☑' };
19
19
 
@@ -303,7 +303,7 @@ export function 웹되돌림(다음, { allowPrivate = false } = {}) {
303
303
  * 첫 주소만 이름을 풀어 보면 소용이 없다. 공개 주소로 시작해 302 한 번으로
304
304
  * 사내로 들어가는 것이 가장 흔한 길이라, 홉마다 같은 것을 물어야 한다.
305
305
  */
306
- export async function 웹되돌림검사(다음, { allowPrivate = false } = {}) {
306
+ async function 웹되돌림검사(다음, { allowPrivate = false } = {}) {
307
307
  웹되돌림(다음, { allowPrivate });
308
308
  if (allowPrivate) return;
309
309
  const 푼것 = await 사설로풀리나(다음.hostname);
package/src/tools/xlsx.js CHANGED
@@ -232,12 +232,12 @@ function 날짜로(n) {
232
232
  * 이 밖을 가리키는 주소는 엑셀이 만든 파일에서 나올 수 없다. 나왔다면 누가
233
233
  * 손으로 적은 바이트다.
234
234
  */
235
- export const 엑셀최대행 = 1048576;
236
- export const 엑셀최대열 = 16384;
235
+ const 엑셀최대행 = 1048576;
236
+ const 엑셀최대열 = 16384;
237
237
  /** 빈 줄·빈 열이 이만큼 넘게 이어지면 자리를 안 지키고 건너뛰었다고 말한다. */
238
- export const 빈틈상한 = 1000;
238
+ const 빈틈상한 = 1000;
239
239
  /** 펼친 표(줄 × 폭)의 칸 수 상한. 넘는 아래쪽 줄은 빼고 뺐다고 말한다. */
240
- export const 칸수상한 = 1_000_000;
240
+ const 칸수상한 = 1_000_000;
241
241
 
242
242
  const 천단위 = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
243
243
 
package/src/ui/banner.js CHANGED
@@ -95,7 +95,7 @@ const 들여 = ' ';
95
95
  const 채움 = '█';
96
96
 
97
97
  /** 글자 하나가 몇 칸인가 (한 배 기준). */
98
- export const 글자폭 = (ch) => (글꼴[ch]?.[0]?.length ?? 0);
98
+ const 글자폭 = (ch) => (글꼴[ch]?.[0]?.length ?? 0);
99
99
 
100
100
  /** 이름 전체가 몇 칸인가. @param 배 가로 배율(1 또는 2) */
101
101
  export function 이름폭(배 = 1, 글자수 = 이름.length) {
@@ -74,7 +74,7 @@ const 앞머리 = 5;
74
74
  // 상자가 차지할 최대 줄 수. 긴 글을 붙여넣어도 화면을 다 먹지 않게.
75
75
  export const 안쪽최대 = 8;
76
76
  // 일하는 중에 미리 쳐 두는 글은 이보다 좁게 보인다 — 위쪽 '일하는 중' 줄을 안 밀어내게.
77
- export const 대기최대 = 3;
77
+ const 대기최대 = 3;
78
78
 
79
79
  /**
80
80
  * 상자를 이룰 줄들과 커서 자리를 값으로 내놓는다.
package/src/ui/md.js CHANGED
@@ -304,7 +304,7 @@ export function 표그리기(줄들, 폭 = 80, 어떻게 = {}) {
304
304
  * 상한에 걸려 표를 나눠 그릴 때, 뒤 묶음에 앞 묶음의 너비를 그대로 물려 주려고
305
305
  * 쓴다. 묶음마다 제 폭을 다시 재면 같은 표인데 위아래의 세로줄이 안 맞는다.
306
306
  */
307
- export function 표칸너비(줄들, 폭 = 80) {
307
+ function 표칸너비(줄들, 폭 = 80) {
308
308
  return 짜기(줄들, 폭)?.너비 ?? null;
309
309
  }
310
310
 
package/src/ui/motion.js CHANGED
@@ -231,7 +231,7 @@ export const 틀 = Object.fromEntries(
231
231
  * 상자 높이와 커서 셈이 통째로 달라진다. 그래서 **가로로 누운 옆모습**으로
232
232
  * 그린다 — 세로로 선 정면 모습은 4점 안에서 절대 안 읽힌다.
233
233
  */
234
- export const 넓은칸수 = 6; // 점자 여섯 칸
234
+ const 넓은칸수 = 6; // 점자 여섯 칸
235
235
 
236
236
  /*
237
237
  * 중세 기사 — 옆모습. 왼쪽에 몸이 서 있고 오른쪽에서 칼이 움직인다.