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
package/bin/deel.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // deel 진입점. 외부 의존성 없음 — Node 표준 기능만 씁니다.
3
- import { join } from 'node:path';
4
- import { readFileSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { readFileSync, writeFileSync, statSync } from 'node:fs';
5
5
  import { c, say, mark, rule, clip, width } from '../src/ui/ansi.js';
6
6
  import { runSetup, runDiagnose, showStatus, banner } from '../src/setup.js';
7
7
  import { chatLoop } from '../src/repl.js';
8
- import { runOnce } from '../src/oneshot.js';
8
+ import { runOnce, EXIT, 실패덩이 } from '../src/oneshot.js';
9
9
  import { packSelf, audit, reviewSheet } from '../src/pack/selfpack.js';
10
10
  import { sbom, 심사명세, 명세요약 } from '../src/pack/sbom.js';
11
11
  import { runScan } from '../src/backend/scanui.js';
@@ -17,8 +17,8 @@ import { runCompletion } from '../src/completion.js';
17
17
  import { runReset } from '../src/reset.js';
18
18
  import { 마크다운, 읽는갈래 } from '../src/tools/doc2md.js';
19
19
  import { 언어잡기, 언어, 말 } from '../src/i18n/index.js';
20
- import { 믿기, 안믿기, 믿는목록, 프로젝트금지칸 } from '../src/safety/trust.js';
21
- import { load as 설정읽기 } from '../src/config.js';
20
+ import { 믿기, 안믿기, 믿는목록, 프로젝트금지칸, 너무넓은자리 as 넓은자리 } from '../src/safety/trust.js';
21
+ import { load as 설정읽기, homeDir, 이PC설정값 } from '../src/config.js';
22
22
  import { 규칙모으기, 어떻게할까, 확인목록, 확인인자, 확인돌리기 } from '../src/safety/policy.js';
23
23
  import { 기록자리, 세기, 도구차례, 막힘차례, 셈JSON } from '../src/stats.js';
24
24
  import { 진찰 } from '../src/doctor.js';
@@ -122,7 +122,19 @@ function runConfig(args, flags) {
122
122
  return 1;
123
123
  }
124
124
  const r = 설명(칸, { root: flags.root ? String(flags.root) : process.cwd() });
125
- if (flags.json === true) { process.stdout.write(JSON.stringify(r) + '\n'); return 0; }
125
+ /*
126
+ * 찾았나 못 찾았나를 **끝값으로도** 말한다.
127
+ *
128
+ * 여기가 `return r.이긴층 ? 0 : 0` 이었다 — 두 갈래를 적어 놓고 같은 수를 냈다.
129
+ * 그래서 칸 이름에 오타를 쳐도, 아무 데도 안 적힌 칸을 물어도 성공으로 끝났다.
130
+ * 이 명령은 스크립트에서 값의 출처를 캐묻는 길이고, 그 길에서 끝값은 유일한
131
+ * 계약이다 — 화면 글자를 grep 하게 만들면 계약이 아니다.
132
+ *
133
+ * 금지·허락처럼 **합쳐지는** 칸은 이긴 층이 없어도 찾은 것이다 (configexplain
134
+ * 의 합치는칸). 이긴 층만 보면 분명히 걸려 있는 규칙이 「못 찾았다」 가 된다.
135
+ */
136
+ const 찾음 = !!(r.이긴층 || r.합침?.length);
137
+ if (flags.json === true) { process.stdout.write(JSON.stringify(r) + '\n'); return 찾음 ? 0 : 1; }
126
138
 
127
139
  const 표 = { 값: c.bold('='), 없음: c.gray('·'), 이김: mark.ok, 아래: c.gray('·'), 안읽음: c.yellow('⚠'), 참고: c.gray('i') };
128
140
  say('');
@@ -133,7 +145,7 @@ function runConfig(args, flags) {
133
145
  say(` ${표[줄.갈래] ?? ' '} ${글}${줄.곁 ? ` ${c.gray(줄.곁)}` : ''}`);
134
146
  }
135
147
  say('');
136
- return r.이긴층 ? 0 : 0;
148
+ return 찾음 ? 0 : 1;
137
149
  }
138
150
 
139
151
  /*
@@ -144,14 +156,23 @@ function runConfig(args, flags) {
144
156
  * 것을 모르면 사람은 파일만 백 번 고친다.
145
157
  */
146
158
  async function runDoctor(flags) {
147
- const { load: 읽기, activeProfile: 고른것, resolveKey: 열쇠풀기, configPath: 설정경로 } =
159
+ const { load: 읽기, activeProfile: 고른것, resolveKey: 열쇠풀기, configPath: 설정경로, 소식줄들 } =
148
160
  await import('../src/config.js');
149
161
  const { 지금모드, 바깥인가, 나갈수있나, 봉인됐나 } = await import('../src/safety/runmode.js');
150
162
  const { allowEndpoint } = await import('../src/safety/network.js');
151
163
 
152
164
  banner();
153
165
  let cfg = null;
154
- try { cfg = 읽기(); } catch { /* 설정이 망가져도 아래에서 말한다 */ }
166
+ /*
167
+ * 설정이 망가져도 멈추지 않고 아래에서 말한다 — **망가진 까닭까지.**
168
+ *
169
+ * 여기가 `catch {}` 였다. 그러면 「아래에서 말한다」 가 거짓말이 된다: 진찰은
170
+ * 파일이 있으니 ✓설정 파일, 프로필이 안 읽혔으니 ✗프로필 을 찍었고, 쉼표 하나
171
+ * 틀린 몇째 줄이라는 진짜 까닭은 이 catch 에서 사라졌다.
172
+ */
173
+ let 설정탈 = null;
174
+ try { cfg = 읽기(); }
175
+ catch (err) { 설정탈 = { 자리: err?.설정자리 ?? null, 까닭: err?.까닭 ?? String(err?.message ?? err) }; }
155
176
  const prof = cfg ? 고른것(cfg) : null;
156
177
 
157
178
  /*
@@ -169,12 +190,19 @@ async function runDoctor(flags) {
169
190
  cfg, prof,
170
191
  root: flags.root ? String(flags.root) : process.cwd(),
171
192
  설정자리: 설정경로(),
193
+ 설정탈,
172
194
  열쇠: prof ? 열쇠풀기(prof) : '',
173
195
  바깥가도되나: 두드려도되나,
174
196
  });
175
197
 
176
198
  const 표 = { ok: mark.ok, warn: mark.warn, no: c.red('✗'), unknown: c.gray('?') };
177
199
  say('');
200
+ /*
201
+ * 모아 둔 소식을 여기서 비운다. 안 비우면 「알릴 것이 없다」 와 똑같이
202
+ * 생긴다 — 정책 파일이 깨져서 관리자가 건 금지가 통째로 안 걸리는
203
+ * 상태조차 조용하다 (config.js 의 소식줄들 머리말).
204
+ */
205
+ for (const 줄 of 소식줄들(cfg)) say(줄);
178
206
  for (const x of 줄들) {
179
207
  const 이름 = String(x.이름) + ' '.repeat(Math.max(0, 14 - width(String(x.이름))));
180
208
  say(` ${표[x.상태] ?? ' '} ${c.gray(이름)} ${x.값}${x.덧말 ? c.gray(` — ${x.덧말}`) : ''}`);
@@ -199,6 +227,22 @@ async function runDoctor(flags) {
199
227
  * 없었다 (src/stats.js 머리말).
200
228
  */
201
229
  function runStats(flags) {
230
+ /*
231
+ * 못 읽는 `--days` 는 멈춘다.
232
+ *
233
+ * 여기가 `parseInt(…) || 30` 이었다. `--days abc` 도 `--days 0` 도 조용히 30일이
234
+ * 되고, 화면은 물어본 적 없는 「최근 30일」 을 찍고 0 으로 끝났다. 감사 기간을
235
+ * 인자로 받는 배치는 엉뚱한 기간을 세어 놓고 초록불로 넘어간다.
236
+ * 이 판이 `--ctx` · `--max-tokens` 를 같은 까닭으로 고쳤는데 이 줄만 남아 있었다.
237
+ */
238
+ if (flags.days !== undefined) {
239
+ const 적은것 = String(flags.days).trim();
240
+ if (!/^\d+$/.test(적은것) || Number(적은것) < 1) {
241
+ process.stderr.write(`\n --days 에는 1 이상 숫자를 주세요: ${String(flags.days)}\n`);
242
+ process.stderr.write(' 기간 없이 전부 보려면 --all 을 쓰세요.\n\n');
243
+ return EXIT.usage;
244
+ }
245
+ }
202
246
  const 자리 = 기록자리(flags.root ? String(flags.root) : process.cwd());
203
247
  const 전부 = flags.all === true || flags.all === 'true';
204
248
  const 날수 = 전부 ? null : Math.max(1, parseInt(String(flags.days ?? '30'), 10) || 30);
@@ -234,14 +278,18 @@ function runStats(flags) {
234
278
  const 실패말 = 셈.도구
235
279
  ? c.gray(` · ${말('stats.failed', { n: 셈.도구실패 })} (${((셈.도구실패 / 셈.도구) * 100).toFixed(1)}%)`)
236
280
  : '';
237
- say(`${칸(말('stats.tools'))} ${셈.도구.toLocaleString()}${실패말}`);
281
+ // 됐는지 모르는 줄이 있으면 **있다고** 말한다. 안 말하면 위 실패율이
282
+ // 「다 됐다」 로 읽힌다 — 모르는 것을 좋은 쪽으로 세는 것과 같아진다.
283
+ const 모름말 = 셈.도구모름 ? c.yellow(` · ${말('stats.unknown', { n: 셈.도구모름 })}`) : '';
284
+ say(`${칸(말('stats.tools'))} ${셈.도구.toLocaleString()}${실패말}${모름말}`);
238
285
 
239
286
  const 표 = 도구차례(셈);
240
287
  if (표.length) {
241
288
  say('');
242
289
  for (const x of 표) {
243
290
  const 실패 = x.실패 ? c.yellow(` ${말('stats.failed', { n: x.실패 })}`) : '';
244
- say(` ${c.white(x.이름.padEnd(12))} ${String(x.수).padStart(6)}${실패}`);
291
+ const 모름 = x.모름 ? c.yellow(` ${말('stats.unknown', { n: x.모름 })}`) : '';
292
+ say(` ${c.white(x.이름.padEnd(12))} ${String(x.수).padStart(6)}${실패}${모름}`);
245
293
  }
246
294
  }
247
295
 
@@ -283,8 +331,40 @@ function runStats(flags) {
283
331
  * 켤 때마다 물어보는 길도 있었지만 그 물음은 맨 앞에 뜨고, 앞에 뜨는 물음은
284
332
  * 안 읽힌다 — 사람은 대화를 하러 온 것이지 물음에 답하러 온 것이 아니다.
285
333
  */
334
+ /*
335
+ * ── 믿으면 너무 많은 것이 같이 믿기는 자리 ─────────────────────────────
336
+ *
337
+ * 믿기는 **하위 폴더까지** 간다(safety/trust.js 의 믿나). 저장소마다 스무 번 답하지
338
+ * 않게 하려는 것인데, 그 규칙이 집 폴더에서는 거꾸로 돈다. `~` 에서 `deel trust` 를
339
+ * 치면 `~/src` 아래 받아 둔 남의 저장소가 **전부** 믿긴다 — 도구 승인보다 앞에서
340
+ * 명령을 돌리는 설정(열쇠받기는 걷히지만 deny·mode·hooks 는 읽힌다)까지.
341
+ *
342
+ * 한동안은 deel 이 집 폴더에서 켤 때마다 「deel trust 를 치라」 고 권하기까지 했다
343
+ * (config.js 의 집설정파일인가 머리말). 그 권유는 걷었고, 여기서는 치더라도 안 적는다.
344
+ *
345
+ * 막는 자리는 셋이다. 사용자 집 폴더와 그 위, deel 설정 폴더를 품은 폴더와 그 위,
346
+ * 드라이브·파일시스템 뿌리. 뺄 때(--off)는 안 막는다 — 좁히는 쪽은 언제나 된다.
347
+ */
348
+ /*
349
+ * 자는 safety/trust.js 에 하나만 둔다. 여기 따로 있을 때는 **적을 때만** 돌아서, 목록 파일에
350
+ * 이미 적힌 `"C:\\"` 는 믿나() 가 그대로 믿었다. 그리고 resolve 만 써서 집 폴더를 가리키는
351
+ * 정션 안에서 치면 지나갔다 — 그쪽 자가 링크를 따라가서 잰다.
352
+ */
353
+ function 너무넓은자리(폴더) {
354
+ return 넓은자리(폴더);
355
+ }
356
+
286
357
  function runTrust(flags) {
287
- const 여기 = process.cwd();
358
+ /*
359
+ * `--root` 를 받는다. 안 주면 켠 자리.
360
+ *
361
+ * 여기가 `process.cwd()` 로 박혀 있었다. `--root` 는 아는 깃발이라 「모르는 깃발」
362
+ * 문에서도 안 걸려, `deel trust --root <저장소>` 는 값을 삼키고 **켠 자리**를
363
+ * 믿는 목록에 적었다. 믿을 자리를 잘못 고르는 것은 조용히 넘어가면 안 되는
364
+ * 종류다 — 이 판이 깃발 읽기를 다시 짠 까닭이 「깃발이 조용히 아무 일도 안
365
+ * 하는 것」 을 없애려는 것이었다. rules 도 같은 문에 남아 있었다.
366
+ */
367
+ const 여기 = flags.root ? resolve(String(flags.root)) : process.cwd();
288
368
 
289
369
  if (flags.list === true || flags.list === 'true') {
290
370
  const 것들 = 믿는목록();
@@ -292,7 +372,8 @@ function runTrust(flags) {
292
372
  say(` ${c.bold(말('trust.listTitle'))}`);
293
373
  say('');
294
374
  if (!것들.length) say(` ${c.gray(말('trust.listNone'))}`);
295
- for (const x of 것들) say(` ${c.gray('·')} ${x}`);
375
+ // 적힌 넓은 자리는 믿나() 가 읽을 때 버린다. 목록도 그렇다고 말한다 — 안 그러면 믿긴다고 적힌 줄이 거짓이다.
376
+ for (const x of 것들) say(` ${c.gray('·')} ${x}${너무넓은자리(x) ? ` ${c.yellow(말('trust.listIgnoredWide'))}` : ''}`);
296
377
  say('');
297
378
  say(` ${c.gray(말('trust.blockedTitle'))}`);
298
379
  for (const { 칸, 열쇠 } of 프로젝트금지칸) say(` ${c.gray('·')} ${c.white(칸)} ${c.gray(말(열쇠))}`);
@@ -307,10 +388,35 @@ function runTrust(flags) {
307
388
  say(` ${mark.ok} ${r.뺐나 ? 말('trust.removed') : 말('trust.notListed')} ${c.gray(여기)}`);
308
389
  // 위 폴더 때문에 아직 믿기는 것을 말 안 하면, 「뺐습니다」 가 거짓말이 된다.
309
390
  if (r.위폴더) say(` ${mark.warn} ${말('trust.parentStill')} ${c.gray(r.위폴더)}`);
391
+ else if (r.아직믿김) say(` ${mark.warn} ${말('trust.stillTrustedEnv')}`);
310
392
  say('');
311
393
  return 0;
312
394
  }
313
395
 
396
+ /*
397
+ * 없는 폴더는 믿는 목록에 안 적는다. 적어 두면 그 이름으로 폴더가 생기는 날
398
+ * 아무도 안 물어본 채로 믿긴다 — 오타 하나가 미래의 저장소를 미리 믿는 셈이다.
399
+ * (뺄 때(--off)는 안 본다. 없는 줄을 지우는 것은 언제나 된다.)
400
+ */
401
+ let 폴더인가 = false;
402
+ try { 폴더인가 = statSync(여기).isDirectory(); } catch { 폴더인가 = false; }
403
+ if (!폴더인가) {
404
+ say('');
405
+ say(` ${mark.warn} 그런 폴더가 없습니다: ${c.gray(여기)}`);
406
+ say(` ${c.gray(' 없는 자리는 믿는 목록에 안 적습니다 — 나중에 그 이름으로 폴더가 생기면 바로 믿기게 됩니다')}`);
407
+ say('');
408
+ return EXIT.error;
409
+ }
410
+
411
+ if (너무넓은자리(여기)) {
412
+ say('');
413
+ say(` ${mark.warn} ${말('trust.refuseWide')} ${c.gray(여기)}`);
414
+ say(` ${c.gray(' ' + 말('trust.refuseWideWhy'))}`);
415
+ say(` ${c.gray(' ' + 말('trust.refuseWideHow'))}`);
416
+ say('');
417
+ return 1;
418
+ }
419
+
314
420
  const r = 믿기(여기);
315
421
  say('');
316
422
  if (!r.ok) { say(` ${mark.warn} ${말('trust.saveFail')} — ${r.왜}`); say(''); return 1; }
@@ -333,7 +439,13 @@ function runTrust(flags) {
333
439
  * 그래서 이 명령이 있다. 규칙을 적자마자 확인할 수 있어야 한다.
334
440
  */
335
441
  function runRules(args, flags) {
336
- const cfg = 설정읽기();
442
+ /*
443
+ * 어느 폴더의 규칙을 재나. `--root` 를 안 봐서 CI 가 거짓 초록을 받았다 —
444
+ * `deel rules check --root $REPO` 가 켠 자리의 규칙을 재고 「다 적어 둔 대로입니다」
445
+ * 로 0 을 냈다. 이 판이 load({ root }) 를 만들면서 run · chat · acp 는 고쳤는데
446
+ * 이 문만 옛 길에 남아 있었다 (trust 도 같았다).
447
+ */
448
+ const cfg = 설정읽기({ root: flags.root ? String(flags.root) : process.cwd() });
337
449
  const 규칙들 = 규칙모으기(cfg);
338
450
  const 어느도구 = flags.tool ? String(flags.tool) : 'Bash';
339
451
 
@@ -456,9 +568,20 @@ function runDoc2md(args, flags) {
456
568
  }
457
569
 
458
570
  function runSbom(flags) {
571
+ const 어느것 = String(flags.only ?? '').toLowerCase();
572
+ /*
573
+ * 모르는 `--only` 는 멈춘다.
574
+ *
575
+ * 여기가 「sbom 도 명세도 아니면 둘 다」 였다. `--only sbon` 오타 하나에 두 장이
576
+ * 한 덩이로 나가고 0 으로 끝나니, `deel sbom --only sbon > sbom.cdx.json` 은
577
+ * CycloneDX 가 아닌 파일을 만들고 스캐너에서야 터진다. 표준출력은 비워 둔다.
578
+ */
579
+ if (flags.only !== undefined && !['sbom', '명세', 'spec'].includes(어느것)) {
580
+ process.stderr.write(`\n --only 에는 sbom · 명세(spec) 중 하나를 주세요: ${String(flags.only)}\n\n`);
581
+ return EXIT.usage;
582
+ }
459
583
  const a = audit();
460
584
  const at = new Date();
461
- const 어느것 = String(flags.only ?? '').toLowerCase();
462
585
  const 낼것 = 어느것 === 'sbom' ? sbom(a, { at })
463
586
  : 어느것 === '명세' || 어느것 === 'spec' ? 심사명세(a, { at })
464
587
  : { sbom: sbom(a, { at }), 심사명세: 심사명세(a, { at }) };
@@ -500,27 +623,153 @@ const BOOL = new Set([
500
623
  * 스크립트는 전체 초기화가 된 줄 알고 다음 줄로 넘어간다.
501
624
  */
502
625
  'hard',
626
+ // trust --off · --list, scan --save · --pick. 여태 목록에 없어 뒤의 낱말을 삼킬 수 있었다.
627
+ 'off', 'list', 'save', 'pick',
503
628
  ]);
504
629
 
630
+ /*
631
+ * ── 값을 받는 깃발 ─────────────────────────────────────────────────────
632
+ *
633
+ * 여태는 BOOL 에 없으면 **무엇이든** 값 깃발로 쳤다. 그래서 두 가지가 샜다.
634
+ *
635
+ * · 모르는 깃발이 뒤의 낱말을 값으로 삼켰다. `deel run --jsn 안녕` 은 `jsn=안녕`
636
+ * 이 되어 시킬 말이 사라졌고, 화면은 「무엇을 시킬지 적어 주세요」 였다 —
637
+ * 표준입력이 열려 있으면 거기서 서 버렸다. 오타는 한마디도 안 나왔다.
638
+ * · 값 깃발이 맨 끝에 값 없이 오면 `true` 가 됐고, 그게 `String(true)` 로
639
+ * "true" 라는 값이 됐다. `deel run 안녕 --root` 는 ./true 폴더를 만들어 그
640
+ * 안에서 일했고, `--work --json` 은 오타 막이를 통째로 건너뛰었다.
641
+ *
642
+ * 그래서 받는 이름을 다 적는다. 없는 이름은 멈추고 이름을 말한다. 명령이 깃발을
643
+ * 하나 더 읽게 되면 여기에도 적어야 한다 — 안 적으면 첫 판에 「모르는 깃발」 로
644
+ * 튕기므로 조용히 새지는 않는다.
645
+ */
646
+ const 값깃발 = new Set([
647
+ 'root', 'mode', 'work', 'level', 'ctx', 'max-tokens', 'think', 'effort', 'output-schema',
648
+ 'out', 'only', 'days', 'tool', 'url', 'key', 'model', 'host', 'ports', 'timeout', 'rm', 'delete',
649
+ ]);
650
+ // 값을 줘도 되고 안 줘도 되는 깃발. `--resume` 만 치면 이어 할 대화를 고른다.
651
+ const 값골라깃발 = new Set(['resume']);
652
+
653
+ /*
654
+ * 모르는 깃발에 **가장 가까운 아는 이름**을 짚는다 (글자 편집 거리).
655
+ *
656
+ * 「모르는 깃발입니다: --jsn」 만 적으면 사람은 도움말을 열어 한 줄씩 훑는다. 거의
657
+ * 언제나 한두 글자 오타다. 너무 먼 것까지 짚으면 엉뚱한 깃발을 권하게 되므로
658
+ * 두 글자, 그리고 이름 길이의 절반까지만 본다.
659
+ */
660
+ function 가까운깃발(이름) {
661
+ const 거리 = (a, b) => {
662
+ let 앞 = Array.from({ length: b.length + 1 }, (_, j) => j);
663
+ for (let i = 1; i <= a.length; i++) {
664
+ const 지금 = [i];
665
+ for (let j = 1; j <= b.length; j++) {
666
+ 지금[j] = Math.min(앞[j] + 1, 지금[j - 1] + 1, 앞[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
667
+ }
668
+ 앞 = 지금;
669
+ }
670
+ return 앞[b.length];
671
+ };
672
+ let 고른 = null;
673
+ let 최소 = Infinity;
674
+ for (const k of [...BOOL, ...값깃발, ...값골라깃발]) {
675
+ const n = 거리(String(이름), k);
676
+ if (n < 최소) { 최소 = n; 고른 = k; }
677
+ }
678
+ return 고른 && 최소 <= Math.min(2, Math.floor(String(이름).length / 2)) ? `--${고른}` : null;
679
+ }
680
+
505
681
  function parse(argv) {
506
682
  const flags = {};
507
683
  const args = [];
684
+ const 탈 = [];
508
685
  for (let i = 0; i < argv.length; i++) {
509
686
  const a = argv[i];
687
+ /*
688
+ * `--` 뒤는 전부 낱말이다 — 유닉스 도구들이 다 그렇게 읽는다.
689
+ *
690
+ * 여기가 없어서 `--` 가 이름이 빈 깃발이 되어 뒤의 말을 값으로 삼켰다.
691
+ * `deel run -- "--json 이 뭐야"` 처럼 대시로 시작하는 말을 넘길 길이 없었다.
692
+ */
693
+ if (a === '--') { args.push(...argv.slice(i + 1)); break; }
510
694
  if (a === '-h') { flags.help = true; continue; }
511
695
  // -v 도 받는다. 판 번호를 묻는 방법이 도구마다 달라서 셋 다 되게 둔다.
512
696
  if (a === '-v' || a === '-V') { flags.version = true; continue; }
697
+ /*
698
+ * 대시 하나로 시작하는 낱말(`-x` · `-jq`)도 깃발이다 — 모르는 이름이라 멈춘다.
699
+ *
700
+ * 여태는 그대로 시킬 말에 섞였다. `deel run -n 5 고쳐` 를 치면 모델은 「-n 5 고쳐」 를
701
+ * 받고 일을 시작했다 — 긴 깃발의 오타는 멈추게 해 놓고 짧은 쪽은 문이 열려 있었다.
702
+ * `-p` 는 깃발처럼 생긴 **명령 이름**이라 명령 자리(맨 앞 낱말)에서만 받는다.
703
+ * 대시 하나(`-`) · 음수(`-1`) · 빈칸 섞인 한 덩이(`"-x 는 뭐야"`)는 낱말이다.
704
+ */
705
+ if (/^-[A-Za-z][\w-]*$/.test(a) && !(a === '-p' && !args.length)) { 탈.push({ 갈래: '모름', 깃발: a, 가까운: null }); continue; }
513
706
  if (a.startsWith('--')) {
514
- const [k, inline] = a.slice(2).split('=');
515
- if (inline !== undefined) flags[k] = inline;
516
- else if (!BOOL.has(k) && argv[i + 1] && !argv[i + 1].startsWith('-')) flags[k] = argv[++i];
517
- else flags[k] = true;
707
+ /*
708
+ * 첫 `=` 에서만 가른다.
709
+ *
710
+ * `split('=')` 은 둘째 `=` 뒤를 버렸다. 그래서
711
+ * `--url=https://gw/openai?api-version=2024-10-21` 이 `…?api-version` 으로
712
+ * 잘려 붙었다 — Azure 주소는 거의 다 이 모양이다.
713
+ */
714
+ const 몸 = a.slice(2);
715
+ const 자리 = 몸.indexOf('=');
716
+ const k = 자리 >= 0 ? 몸.slice(0, 자리) : 몸;
717
+ const inline = 자리 >= 0 ? 몸.slice(자리 + 1) : undefined;
718
+ if (!BOOL.has(k) && !값깃발.has(k) && !값골라깃발.has(k)) { 탈.push({ 갈래: '모름', 깃발: a, 가까운: 가까운깃발(k) }); continue; }
719
+ if (inline !== undefined) {
720
+ /*
721
+ * 켜고 끄는 깃발에 붙은 값은 true · false 만 읽는다.
722
+ *
723
+ * 여태는 붙은 글자가 그대로 값이 됐다. 읽는 쪽은 `=== true` 나 `'true'` 로 보니
724
+ * `--json=yes` · `--json=1` 은 **꺼진 것**으로 돌았다 — 스크립트는 JSON 을 기다리는데
725
+ * 맨 글이 오고 종료코드는 0 이었다. 반대로 `--json=false` 는 글자 'false' 라 참으로
726
+ * 읽는 자리가 있었다. 둘 다 여기서 가른다.
727
+ */
728
+ if (BOOL.has(k)) {
729
+ const 뜻 = inline.toLowerCase();
730
+ if (뜻 === 'true') flags[k] = true;
731
+ else if (뜻 !== 'false') 탈.push({ 갈래: '켜끄기', 깃발: `--${k}`, 값: inline });
732
+ } else if (inline === '' && 값깃발.has(k)) 탈.push({ 갈래: '값없음', 깃발: `--${k}` });
733
+ else flags[k] = inline;
734
+ continue;
735
+ }
736
+ const 다음 = argv[i + 1];
737
+ if (BOOL.has(k)) flags[k] = true;
738
+ // `--root ""` 는 `--root=` 와 같다. 빈 값을 받아 두면 읽는 쪽이 「안 줬다」 로 쳐서 조용히 지금 폴더에서 돈다.
739
+ else if (다음 === '') { i++; if (값골라깃발.has(k)) flags[k] = true; else 탈.push({ 갈래: '값없음', 깃발: `--${k}` }); }
740
+ else if (다음 !== undefined && !다음.startsWith('-')) flags[k] = argv[++i];
741
+ else if (값골라깃발.has(k)) flags[k] = true;
742
+ else 탈.push({ 갈래: '값없음', 깃발: `--${k}` });
518
743
  } else args.push(a);
519
744
  }
520
745
  // 명령은 플래그가 아닌 첫 낱말. 없으면 상태 보기.
521
- return { cmd: args[0] ?? '', args: args.slice(1), flags };
746
+ return { cmd: args[0] ?? '', args: args.slice(1), flags, 탈 };
522
747
  }
523
748
 
749
+ /*
750
+ * 인자를 잘못 줘서 **시작도 안 하고** 멈추는 자리. 종료코드는 64(EXIT.usage) 다 —
751
+ * 2 는 걸음 수 상한이라, 오타 하나를 「일이 커서 멈췄다」 로 읽은 스크립트가 있었다.
752
+ *
753
+ * 글은 표준오류로 낸다. `deel run --json` 으로 부른 스크립트는 표준출력을 JSON 으로
754
+ * 읽는데, 여기 맨 글이 섞이면 파싱에서 죽고 진짜 까닭은 그 파싱 오류에 묻힌다.
755
+ * 그래서 run 에 --json 이면 표준출력에 실패 한 덩이를 같이 낸다 (oneshot.js 의 못함과 같은 모양).
756
+ */
757
+ function 인자탈(줄들, { cmd, flags, 코드 = EXIT.usage }) {
758
+ for (const 줄 of ['', ...줄들, '']) process.stderr.write(`${줄}\n`);
759
+ const 배치 = cmd === 'run' || cmd === '-p';
760
+ if (배치 && (flags.json === true || flags.json === 'true')) {
761
+ // 모양은 한 방 실행의 실패덩이 하나로 짓는다 — 성공과 같은 칸이어야 한다 (사냥5 B5-10).
762
+ process.stdout.write(JSON.stringify(실패덩이({
763
+ reason: 코드 === EXIT.usage ? 'usage' : 'config', code: 코드,
764
+ why: 줄들.map((s) => String(s).trim()).filter(Boolean).join(' '),
765
+ })) + '\n');
766
+ }
767
+ return 코드;
768
+ }
769
+
770
+ // 마지막 catch 가 `--json` 인지 알려고 들고 있는다. 인자를 두 번 읽으면 두 벌이 된다.
771
+ let 읽은인자 = null;
772
+
524
773
  function help() {
525
774
  banner();
526
775
  say(` ${c.bold('사용법')}`);
@@ -558,6 +807,9 @@ function help() {
558
807
  say(` ${c.gray('deel reset sessions')} 대화 기록`);
559
808
  say(` ${c.gray('deel reset learned')} 배운 것 (이 PC + 이 폴더)`);
560
809
  say(` ${c.gray('deel reset plugins')} 설치한 플러그인`);
810
+ // all 에만 딸려 가는 갈래 (reset.js 의 숨은것). 따로 고를 수 없어서 이름이 보일 자리가
811
+ // 여기밖에 없다. 안 적었더니 「위 전부」 를 읽은 사람이 .deel/tmp 가 지워진 것을 나중에 알았다.
812
+ say(` ${c.gray('(따로 못 고름)')} 증거·내보낸 것·임시 ${c.gray('(.deel/증거 · export · tmp · 붙인그림)')}`);
561
813
  say(` ${c.gray('deel reset all')} 위 전부 ${c.gray('— 플러그인·되돌리기·감사기록은 빼고')}`);
562
814
  say(` ${c.gray('--hard')} ${c.yellow('되돌리기 스냅샷·감사기록까지')}. all 에서만 씁니다`);
563
815
  say(` ${c.gray('--yes')} 안 묻고 지웁니다 (스크립트용)`);
@@ -662,7 +914,7 @@ function help() {
662
914
  // 이 줄은 src/oneshot.js 의 EXIT 와 짝이다. test/exitcode.test.js 가 둘이
663
915
  // 어긋나면 빨개진다 — 한 판 동안 refusal(6) 이 여기서 빠져 있었고, 그
664
916
  // 사실을 말해 주는 자리가 아무 데도 없었다.
665
- say(` ${c.gray('끝난 까닭이 종료코드에 담깁니다:')} ${c.gray('0 끝냄 · 1 오류 · 2 걸음수상한 · 3 헛돎 · 4 중단 · 5 말없이끊김 · 6 거절 · 7 모양안맞음')}`);
917
+ say(` ${c.gray('끝난 까닭이 종료코드에 담깁니다:')} ${c.gray('0 끝냄 · 1 오류 · 2 걸음수상한 · 3 헛돎 · 4 중단 · 5 말없이끊김 · 6 거절 · 7 모양안맞음 · 64 사용법틀림')}`);
666
918
  say('');
667
919
  say(` ${c.bold('진단 직접 지정')} ${c.gray('— 설정을 남기지 않고 확인만 할 때')}`);
668
920
  say('');
@@ -683,7 +935,9 @@ async function main() {
683
935
  process.exit(1);
684
936
  }
685
937
 
686
- const { cmd: 친명령, args, flags } = parse(process.argv.slice(2));
938
+ const 읽음 = parse(process.argv.slice(2));
939
+ 읽은인자 = 읽음;
940
+ const { cmd: 친명령, args, flags, 탈: 인자탈들 } = 읽음;
687
941
 
688
942
  // 서류를 뽑는 명령(audit·sbom·pack)이 어느 말로 나갈지를 여기서 정한다.
689
943
  말정하기();
@@ -724,17 +978,114 @@ async function main() {
724
978
  *
725
979
  * 판 번호·도움말보다 뒤에 둔다 — 그 둘은 무엇을 잘못 쳤든 답해야 한다.
726
980
  */
727
- if (flags.work !== undefined && flags.work !== true) {
981
+ // 모르는 깃발·값 없는 깃발. 판 번호·도움말은 위에서 이미 답했다.
982
+ if (인자탈들.length) {
983
+ const 줄들 = 인자탈들.map((x) => (x.갈래 === '모름'
984
+ ? ` 모르는 깃발입니다: ${x.깃발}${x.가까운 ? ` — 혹시 ${x.가까운} 인가요?` : ''}`
985
+ : x.갈래 === '켜끄기'
986
+ ? ` ${x.깃발} 은 켜고 끄는 깃발입니다 — ${JSON.stringify(x.값)} 는 못 읽습니다 (${x.깃발} 또는 ${x.깃발}=false)`
987
+ : ` ${x.깃발} 에 값이 없습니다 — ${x.깃발} <값> 또는 ${x.깃발}=<값>`));
988
+ return 인자탈([...줄들, ' 받는 깃발은 deel --help 에 있습니다. 대시로 시작하는 말은 -- 뒤에 두세요.'], { cmd, flags });
989
+ }
990
+
991
+ if (flags.work !== undefined) {
728
992
  const { normalize: 모드이름, MODES: 모드들, 보일이름: 모드보임 } = await import('../src/agent/modes.js');
729
993
  if (!모드이름(String(flags.work))) {
730
994
  const 있는것 = Object.keys(모드들).map((k) => `${모드보임(k)}(${k})`).join(' · ');
731
- say('');
732
- say(` 그런 작업 모드가 없습니다: ${String(flags.work)}`);
733
- say(` 있는 것: ${있는것}`);
734
- say('');
735
- say(' 그냥 켜면 「종합」 으로 돕니다 — 그건 파일을 고칠 수 있는 모드입니다.');
736
- say('');
737
- return 2;
995
+ return 인자탈([
996
+ ` 그런 작업 모드가 없습니다: ${String(flags.work)}`,
997
+ ` 있는 것: ${있는것}`,
998
+ '',
999
+ ' 그냥 켜면 「종합」 으로 돕니다 — 그건 파일을 고칠 수 있는 모드입니다.',
1000
+ ], { cmd, flags });
1001
+ }
1002
+ }
1003
+
1004
+ /*
1005
+ * `--mode` 도 `--work` 와 같은 문을 지난다 — 그리고 이쪽이 더 위험했다.
1006
+ *
1007
+ * 값을 안 보고 그대로 넘겼다. 승인을 묻는 자리는 `session.mode === 'strict'` 로
1008
+ * 보므로, `--mode Strict` · `STRICT` 는 strict 도 confirm 도 아닌 **아무것도 안
1009
+ * 묻는 상태**가 됐다. `deel run --mode Strict` 는 Write 를 묻지 않고 돌려 0 으로
1010
+ * 끝났고, 그 사이 모델에게는 「승인이 필요한 것은 거부된다」 고 거짓말을 했다.
1011
+ * 소문자로 치면 멀쩡하니, 적은 사람은 막힌 줄 안다.
1012
+ *
1013
+ * 대소문자는 안 가린다 — 그건 틀린 것이 아니라 적는 버릇이다. 이름 자체가 다르면
1014
+ * 멈춘다. 대화 중 `/mode` 도 한글 이름(엄격)은 안 받으므로 여기서도 안 받는다.
1015
+ */
1016
+ if (flags.mode !== undefined) {
1017
+ const { 차례: 승인모드들 } = await import('../src/ui/approve.js');
1018
+ const 고른것 = String(flags.mode).trim().toLowerCase();
1019
+ if (!승인모드들.includes(고른것)) {
1020
+ return 인자탈([
1021
+ ` 그런 승인 모드가 없습니다: ${String(flags.mode)}`,
1022
+ ` 있는 것: ${승인모드들.join(' · ')}`,
1023
+ '',
1024
+ ' 모르는 이름으로 켜면 아무것도 안 묻는 상태가 됩니다 — 그래서 켜지 않습니다.',
1025
+ ], { cmd, flags });
1026
+ }
1027
+ flags.mode = 고른것;
1028
+ }
1029
+
1030
+ /*
1031
+ * ── 이 PC 설정의 mode 를 아무 문도 안 읽었다 ──────────────────────────
1032
+ *
1033
+ * `"mode": "strict"` 를 설정에 적어 둔 사람의 `deel run` 이 Write 를 묻지 않고 돌렸다.
1034
+ * 대화 화면도 같았다 — 셋 다 `opts.mode ?? 'auto'` 였다. 적은 사람은 막힌 줄 안다.
1035
+ *
1036
+ * 차례는 깃발 > 이 PC 설정 > auto. **이 PC 설정만** 읽는다. 저장소 설정의 mode 를
1037
+ * 받으면 남의 저장소가 `"mode": "auto"` 한 줄로 이 PC 의 strict 를 푼다.
1038
+ * 모르는 이름이면 멈춘다. 가장 헐거운 auto 로 떨어지는 것이 제일 나쁜 실패다.
1039
+ * 인자를 틀린 것이 아니라 설정이 틀린 것이라 종료코드는 1 이다.
1040
+ */
1041
+ if (flags.mode === undefined && ['run', '-p', '', 'chat', 'acp'].includes(cmd)) {
1042
+ const 설정모드 = 이PC설정값('mode');
1043
+ if (설정모드 !== undefined && 설정모드 !== null) {
1044
+ const { 차례: 승인모드들 } = await import('../src/ui/approve.js');
1045
+ const 설정고른것 = String(설정모드).trim().toLowerCase();
1046
+ if (!승인모드들.includes(설정고른것)) {
1047
+ return 인자탈([
1048
+ ` 이 PC 설정의 mode 를 모릅니다: ${String(설정모드)} (${join(homeDir(), 'config.json')})`,
1049
+ ` 있는 것: ${승인모드들.join(' · ')}`,
1050
+ '',
1051
+ ' 고칠 때까지 켜지 않습니다 — 모르는 이름으로 켜면 아무것도 안 묻는 상태가 됩니다.',
1052
+ ], { cmd, flags, 코드: EXIT.error });
1053
+ }
1054
+ flags.mode = 설정고른것;
1055
+ }
1056
+ }
1057
+
1058
+ /*
1059
+ * 없는 `--root` 는 대화·에디터 문에서도 거절한다 (run 은 oneshot.js 가 같은 일을 한다).
1060
+ *
1061
+ * 대화 화면은 뿌리를 안 봤다. 기록·되돌리기 자리를 짓는 쪽이 폴더를 통째로 만들어
1062
+ * 주니, 드라이브 글자 하나 틀린 `deel --root` 가 빈 폴더에서 대화를 시작했다 —
1063
+ * 「없는 파일은 만든다」 대로 일하면 진짜 저장소 옆에 가짜 저장소가 자란다.
1064
+ * 글은 표준오류로 낸다. `deel acp` 의 표준출력은 에디터가 읽는 JSON-RPC 관이다.
1065
+ */
1066
+ if (['', 'chat', 'acp'].includes(cmd) && flags.root !== undefined) {
1067
+ let 폴더인가 = false;
1068
+ try { 폴더인가 = statSync(String(flags.root)).isDirectory(); } catch { 폴더인가 = false; }
1069
+ if (!폴더인가) {
1070
+ process.stderr.write(`\n 작업 폴더가 없습니다 (--root): ${String(flags.root)} — 없는 폴더를 만들어 그 안에서 일하지 않습니다\n\n`);
1071
+ return EXIT.error;
1072
+ }
1073
+ }
1074
+
1075
+ /*
1076
+ * `--ctx junk` 는 조용히 버려졌다 — parseSize 가 null 을 주면 「안 준 것」 과 같아져
1077
+ * 서버에 맞춘 길이로 돌았다. 대화 중 `/ctx junk` 는 「숫자를 못 읽었습니다」 로
1078
+ * 거절한다. 같은 값을 문에 따라 다르게 받으면, 깃발로 준 사람만 제 값이 먹는 줄 안다.
1079
+ * `auto` 는 「서버에 맞춤」 이라는 뜻이라 안 준 것으로 받는다.
1080
+ */
1081
+ for (const 이름 of ['ctx', 'max-tokens']) {
1082
+ if (flags[이름] === undefined) continue;
1083
+ if (String(flags[이름]).trim().toLowerCase() === 'auto') { delete flags[이름]; continue; }
1084
+ if (parseSize(String(flags[이름])) == null) {
1085
+ return 인자탈([
1086
+ ` --${이름} 숫자를 못 읽었습니다: ${String(flags[이름])}`,
1087
+ ' 이렇게 쓰세요 — 655360 · 640k · 128k (k 는 1024 입니다)',
1088
+ ], { cmd, flags });
738
1089
  }
739
1090
  }
740
1091
 
@@ -809,7 +1160,7 @@ async function main() {
809
1160
  case 'status':
810
1161
  return showStatus();
811
1162
  case 'setup':
812
- return runSetup();
1163
+ return runSetup(flags);
813
1164
  case 'diagnose':
814
1165
  return runDiagnose(flags);
815
1166
  /*
@@ -862,7 +1213,8 @@ async function main() {
862
1213
  say('');
863
1214
  say(` ${c.red('모르는 명령')} ${c.bold(cmd)}`);
864
1215
  help();
865
- return 1;
1216
+ // 인자를 잘못 준 것이다 — 모르는 깃발과 같은 64. 1 이면 스크립트가 「돌다가 오류」 와 못 가른다.
1217
+ return EXIT.usage;
866
1218
  }
867
1219
  }
868
1220
 
@@ -887,6 +1239,20 @@ async function 끝내기(code) {
887
1239
  main()
888
1240
  .then((code) => 끝내기(code ?? 0))
889
1241
  .catch(async (err) => {
1242
+ /*
1243
+ * `deel run --json` 이면 표준출력은 JSON 한 덩이여야 한다(도움말의 약속).
1244
+ * 여기는 무엇이 터졌든 표준출력에 맨 글을 찍었고, JSON 을 읽던 스크립트는
1245
+ * 파싱에서 죽었다. 이 문에서는 글을 표준오류로, 실패 한 덩이를 표준출력으로 낸다.
1246
+ */
1247
+ const 배치JSON = !!읽은인자 && (읽은인자.cmd === 'run' || 읽은인자.cmd === '-p')
1248
+ && (읽은인자.flags.json === true || 읽은인자.flags.json === 'true');
1249
+ if (배치JSON) {
1250
+ process.stderr.write(` ✗ ${err?.message ?? err}\n`);
1251
+ // 인자탈 과 같은 모양 — 성공한 --json 과 칸이 같아야 한다 (사냥5 B5-10).
1252
+ process.stdout.write(JSON.stringify(실패덩이({ reason: 'error', code: 1, why: String(err?.message ?? err) })) + '\n');
1253
+ await 끝내기(1);
1254
+ return;
1255
+ }
890
1256
  say('');
891
1257
  say(` ${c.red('오류')} ${err?.message ?? err}`);
892
1258
  if (process.env.DEEL_DEBUG) say(c.gray(err?.stack ?? ''));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.20.13",
3
+ "version": "2.0.0",
4
4
  "description": "A coding-agent CLI that runs on local models and private gateways. Zero dependencies, Node 20+.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,6 +48,7 @@
48
48
  "chat": "node bin/deel.js",
49
49
  "test": "node test/run.mjs",
50
50
  "docs": "node tools/check-docs.mjs",
51
+ "figures": "node tools/make-figures.mjs",
51
52
  "verify": "node test/no-bundle.test.js && node test/network.test.js && node test/web.test.js && node test/mcp.test.js",
52
53
  "bench": "node test/edit-bench.js",
53
54
  "demo": "node test/demo.js",