deel-local-cli 1.20.13 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.ko.md +97 -78
  2. package/README.md +120 -81
  3. package/bin/deel.js +398 -32
  4. package/package.json +2 -1
  5. package/src/acp/jsonrpc.js +87 -7
  6. package/src/acp/map.js +23 -3
  7. package/src/acp/serve.js +333 -25
  8. package/src/agent/agents.js +110 -12
  9. package/src/agent/askcheck.js +50 -6
  10. package/src/agent/asks.js +59 -5
  11. package/src/agent/budget.js +9 -2
  12. package/src/agent/card.js +8 -2
  13. package/src/agent/commit.js +168 -16
  14. package/src/agent/compact.js +93 -21
  15. package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
  16. package/src/agent/effort.js +85 -15
  17. package/src/agent/evidence.js +63 -7
  18. package/src/agent/evolve.js +132 -24
  19. package/src/agent/grade.js +12 -3
  20. package/src/agent/loop.js +408 -81
  21. package/src/agent/memory.js +176 -29
  22. package/src/agent/mention.js +22 -5
  23. package/src/agent/models.js +31 -7
  24. package/src/agent/modes.js +25 -10
  25. package/src/agent/outschema.js +381 -56
  26. package/src/agent/pins.js +33 -2
  27. package/src/agent/project.js +73 -8
  28. package/src/agent/recall.js +40 -9
  29. package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
  30. package/src/agent/review.js +143 -13
  31. package/src/agent/route.js +253 -26
  32. package/src/agent/salvage.js +2 -1
  33. package/src/agent/session.js +457 -31
  34. package/src/agent/store.js +232 -22
  35. package/src/agent/threads.js +52 -3
  36. package/src/backend/adapter.js +508 -85
  37. package/src/backend/azure.js +12 -1
  38. package/src/backend/cachemark.js +31 -5
  39. package/src/backend/clientcert.js +23 -1
  40. package/src/backend/ctxsize.js +56 -9
  41. package/src/backend/detect.js +252 -19
  42. package/src/backend/http.js +383 -42
  43. package/src/backend/learn.js +101 -11
  44. package/src/backend/mcp.js +333 -24
  45. package/src/backend/price.js +61 -13
  46. package/src/backend/probe.js +188 -36
  47. package/src/backend/proxy.js +108 -14
  48. package/src/backend/quota.js +226 -30
  49. package/src/backend/retry.js +46 -3
  50. package/src/backend/scan.js +47 -10
  51. package/src/backend/scanui.js +57 -7
  52. package/src/backend/toolfit.js +204 -22
  53. package/src/backend/vision.js +14 -1
  54. package/src/backend/wire.js +58 -10
  55. package/src/cmdnames.js +16 -0
  56. package/src/commands/common.js +70 -0
  57. package/src/commands/extend.js +299 -0
  58. package/src/commands/model.js +851 -0
  59. package/src/commands/view.js +328 -0
  60. package/src/commands/work.js +823 -0
  61. package/src/commands.js +193 -2082
  62. package/src/completion.js +45 -3
  63. package/src/config.js +454 -41
  64. package/src/configexplain.js +127 -15
  65. package/src/doctor.js +164 -27
  66. package/src/i18n/en.js +22 -0
  67. package/src/i18n/index.js +31 -1
  68. package/src/i18n/ja.js +25 -1
  69. package/src/i18n/ko.js +25 -2
  70. package/src/i18n/zh.js +25 -1
  71. package/src/lsp/client.js +111 -9
  72. package/src/lsp/servers.js +86 -22
  73. package/src/oneshot.js +325 -65
  74. package/src/pack/sbom.js +28 -5
  75. package/src/pack/selfpack.js +54 -5
  76. package/src/pack/sheet.en.js +24 -3
  77. package/src/pack/tar.js +45 -8
  78. package/src/pack/zip.js +103 -8
  79. package/src/plugins/manage.js +165 -14
  80. package/src/preview/serve.js +239 -23
  81. package/src/providers/bedrock.js +30 -5
  82. package/src/providers/gemini.js +8 -0
  83. package/src/providers/index.js +17 -1
  84. package/src/repl.js +254 -61
  85. package/src/report.js +7 -2
  86. package/src/reset.js +151 -25
  87. package/src/safety/audit.js +108 -8
  88. package/src/safety/authcmd.js +75 -5
  89. package/src/safety/guard.js +531 -15
  90. package/src/safety/hooks.js +172 -20
  91. package/src/safety/keystore.js +178 -18
  92. package/src/safety/network.js +209 -6
  93. package/src/safety/policy.js +377 -36
  94. package/src/safety/runmode.js +9 -2
  95. package/src/safety/secrets.js +243 -17
  96. package/src/safety/shellenv.js +64 -3
  97. package/src/safety/trust.js +298 -32
  98. package/src/safety/undo.js +409 -17
  99. package/src/setup.js +175 -17
  100. package/src/skills/discover.js +122 -19
  101. package/src/stats.js +57 -16
  102. package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
  103. package/src/tools/clipboard.js +53 -20
  104. package/src/tools/convert.js +118 -37
  105. package/src/tools/desc.en.js +48 -5
  106. package/src/tools/doc2md.js +36 -4
  107. package/src/tools/docs.js +94 -18
  108. package/src/tools/edit-match.js +212 -13
  109. package/src/tools/encoding.js +525 -28
  110. package/src/tools/excel-com.js +63 -15
  111. package/src/tools/excel.js +12 -7
  112. package/src/tools/fastgrep.js +501 -38
  113. package/src/tools/fig.js +21 -4
  114. package/src/tools/fsutil.js +235 -21
  115. package/src/tools/hwpxwrite.js +80 -19
  116. package/src/tools/ignore.js +55 -7
  117. package/src/tools/index.js +1083 -148
  118. package/src/tools/jobs.js +328 -50
  119. package/src/tools/kiwi.js +50 -5
  120. package/src/tools/lsp.js +164 -32
  121. package/src/tools/outline.js +52 -12
  122. package/src/tools/pdf.js +367 -59
  123. package/src/tools/shell.js +12 -4
  124. package/src/tools/spawn.js +203 -18
  125. package/src/tools/todo.js +73 -4
  126. package/src/tools/verify.js +328 -45
  127. package/src/tools/webfetch.js +194 -25
  128. package/src/tools/xlsx.js +232 -31
  129. package/src/ui/ansi.js +209 -15
  130. package/src/ui/banner.js +2 -1
  131. package/src/ui/complete.js +46 -4
  132. package/src/ui/diff.js +4 -2
  133. package/src/ui/export.js +91 -17
  134. package/src/ui/inputbox.js +58 -18
  135. package/src/ui/intro.js +9 -2
  136. package/src/ui/level.js +32 -7
  137. package/src/ui/md.js +101 -15
  138. package/src/ui/motion.js +5 -2
  139. package/src/ui/notify.js +7 -1
  140. package/src/ui/office.js +37 -5
  141. package/src/ui/pastechip.js +3 -2
  142. package/src/ui/pick.js +85 -12
  143. package/src/ui/prompt.js +176 -13
  144. package/src/ui/screen.js +2 -1
  145. package/src/ui/spinner.js +6 -1
  146. package/src/ui/status.js +80 -12
  147. package/src/ui/wrap.js +23 -37
  148. /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
  149. /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
  150. /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
  151. /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
  152. /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
  153. /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
  154. /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
  155. /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
@@ -44,18 +44,48 @@
44
44
  // `~/.deel/trusted.json` — 사용자 자리다. 프로젝트 안에 두면 저장소가 제
45
45
  // 신뢰를 제가 적는 셈이라 아무 뜻이 없다.
46
46
  import { homedir } from 'node:os';
47
- import { join, resolve } from 'node:path';
48
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
47
+ import { join, resolve, dirname, isAbsolute, parse as 경로쪼개기 } from 'node:path';
48
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, rmSync, openSync, closeSync, statSync, realpathSync } from 'node:fs';
49
49
  import { c, mark } from '../ui/ansi.js';
50
50
  import { 말 } from '../i18n/index.js';
51
51
 
52
- function 사용자자리(env = process.env) {
53
- return env.DEEL_HOME ? resolve(env.DEEL_HOME) : join(homedir(), '.deel');
52
+ /**
53
+ * 글 맨 앞의 BOM(U+FEFF) 하나를 뗀다.
54
+ *
55
+ * 윈도우 파워셸 5.1 의 `Set-Content -Encoding UTF8` 은 파일 앞에 BOM 을 붙인다.
56
+ * 사내에서 설정을 스크립트로 까는 자리가 딱 그 명령이다. JSON.parse 는 그 한
57
+ * 글자에서 넘어지는데, 넘어진 뒤가 자리마다 달랐다 — 설정은 모든 명령이
58
+ * 종료코드 1 로 죽었고, 이 파일의 믿는 목록은 **아무 말 없이** 빈 목록이 됐다.
59
+ * 믿는다고 적어 둔 사람은 왜 안 믿기는지 알 길이 없다.
60
+ *
61
+ * 읽는 자리마다 따로 떼면 한 자리는 반드시 빠진다. 여기 하나를 같이 쓴다.
62
+ */
63
+ export function BOM떼기(글) {
64
+ const s = String(글 ?? '');
65
+ return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
66
+ }
67
+
68
+ /*
69
+ * 이 환경이 말하는 집 폴더.
70
+ *
71
+ * os.homedir() 는 **process.env 만** 본다. 그래서 env 를 따로 받은 부름(검사·훅·하위
72
+ * 작업)에서 아래 너무넓은자리() 는 그 env 의 USERPROFILE·HOME 으로 집을 재는데
73
+ * 사용자자리() 만 진짜 집을 썼다 — 가짜 집을 준 부름이 **이 PC 의 진짜 목록**을 읽고
74
+ * 적었다. 한 함수 안에서 집이 둘이면, 넓은지 재는 집과 적는 집이 다른 폴더가 된다.
75
+ * 집을 묻는 자리는 여기 하나다.
76
+ */
77
+ function 집자리(env = process.env, platform = process.platform) {
78
+ const 차례 = platform === 'win32' ? [env.USERPROFILE, env.HOME] : [env.HOME, env.USERPROFILE];
79
+ return 차례.find((x) => typeof x === 'string' && x) || homedir();
80
+ }
81
+
82
+ function 사용자자리(env = process.env, platform = process.platform) {
83
+ return env.DEEL_HOME ? resolve(env.DEEL_HOME) : join(집자리(env, platform), '.deel');
54
84
  }
55
85
 
56
86
  /** 믿는 폴더 목록이 놓이는 자리. */
57
- export function 신뢰자리(env = process.env) {
58
- return join(사용자자리(env), 'trusted.json');
87
+ export function 신뢰자리(env = process.env, platform = process.platform) {
88
+ return join(사용자자리(env, platform), 'trusted.json');
59
89
  }
60
90
 
61
91
  /*
@@ -76,9 +106,7 @@ function 목록읽기(env) {
76
106
  const p = 신뢰자리(env);
77
107
  if (!existsSync(p)) return [];
78
108
  try {
79
- const raw = JSON.parse(readFileSync(p, 'utf8'));
80
- const 것들 = Array.isArray(raw) ? raw : raw?.trusted;
81
- return Array.isArray(것들) ? 것들.filter((x) => typeof x === 'string') : [];
109
+ return 목록풀기(readFileSync(p, 'utf8'));
82
110
  } catch {
83
111
  // 못 읽으면 **아무것도 안 믿는다.** 반대로 하면 목록 파일 하나가 깨진
84
112
  // 것으로 온 폴더가 열린다. 여기서 안전한 쪽은 닫는 쪽뿐이다.
@@ -91,6 +119,52 @@ export function 믿는목록({ env = process.env } = {}) {
91
119
  return 목록읽기(env);
92
120
  }
93
121
 
122
+ /*
123
+ * ── 믿으면 너무 많은 것이 같이 믿기는 자리 ─────────────────────────────
124
+ *
125
+ * 믿기는 **하위 폴더까지** 간다(아래 믿나). 저장소마다 스무 번 답하지 않게 하려는
126
+ * 것인데, 그 규칙이 넓은 자리에서는 거꾸로 돈다. `~` 를 믿으면 `~/src` 아래 받아 둔
127
+ * 남의 저장소가 **전부** 믿긴다.
128
+ *
129
+ * 막는 자리는 셋이다. 드라이브·파일시스템 뿌리, 사용자 집 폴더와 그 위, deel 설정
130
+ * 폴더를 품은 폴더와 그 위. 그리고 **절대 경로가 아닌 것** — `""`·`"."`·`"D:"`·`"D:foo"`
131
+ * 는 resolve 를 지나면 켠 자리가 되어서, 목록에 적혀 있으면 어디서 켜든 믿겼다.
132
+ * 드라이브의 켠 자리(`"D:"`)도 여기서 같이 걸린다. isAbsolute 는 콜론 뒤에 구분자가
133
+ * 없으면 절대 경로로 안 치기 때문이다 — 뒤에 무늬로 한 번 더 보던 줄은 그래서 한 번도
134
+ * 안 돌았고, 안 도는 규칙은 지키는 것처럼 보이기만 해서 걷어냈다.
135
+ *
136
+ * 이 자는 원래 bin/deel.js 에만 있었고 **적을 때만** 돌았다. 목록 파일에 이미
137
+ * `"C:\\"` 가 적혀 있으면(손으로 고쳤든, 옛 판이든, 스크립트든) 믿나() 는 그대로
138
+ * 믿었다. 그래서 여기 하나를 두고 적는 자리와 읽는 자리가 같이 쓴다.
139
+ *
140
+ * ── 링크를 따라가서 잰다 ──
141
+ *
142
+ * resolve 는 정션·심볼릭 링크를 안 따라간다. 집 폴더를 가리키는 `…\고리` 안에서
143
+ * `deel trust` 를 치면 글자가 달라 지나갔고, 믿긴 것은 고리 아래 전부 — 곧 집 폴더
144
+ * 전부였다. 그래서 실제 자리(realpath)로도 재고, 적힌 글자로도 잰다. 둘 중 하나라도
145
+ * 넓으면 넓다. 없는 자리라 실제 자리를 못 얻으면 적힌 글자로만 잰다.
146
+ */
147
+ function 실제자리들(p) {
148
+ const 풀린 = resolve(p);
149
+ let 진짜 = 풀린;
150
+ try { 진짜 = realpathSync.native(풀린); } catch { /* 없는 자리 — 글자 그대로 잰다 */ }
151
+ return 진짜 === 풀린 ? [풀린] : [풀린, 진짜];
152
+ }
153
+
154
+ export function 너무넓은자리(폴더, { env = process.env, platform = process.platform } = {}) {
155
+ const 글 = String(폴더 ?? '');
156
+ if (!글 || !isAbsolute(글)) return true; // "" · "." · "D:" · "D:foo" — resolve 를 지나면 켠 자리다
157
+ const 나들 = 실제자리들(글);
158
+ if (나들.some((p) => p === 경로쪼개기(p).root)) return true;
159
+ // 집 폴더는 이 환경이 말하는 것으로 잰다 (집자리 머리말 — 목록을 적는 자리와 같은 집이어야 한다).
160
+ const 집 = 집자리(env, platform);
161
+ const 품을것 = [...실제자리들(집), ...실제자리들(dirname(사용자자리(env)))].map((p) => 고른경로(p, platform));
162
+ return 나들.some((p) => {
163
+ const 나 = 고른경로(p, platform);
164
+ return 품을것.some((그것) => 그것 === 나 || 그것.startsWith(나 + '/'));
165
+ });
166
+ }
167
+
94
168
  /**
95
169
  * 이 폴더의 프로젝트 설정을 믿나.
96
170
  *
@@ -103,48 +177,156 @@ export function 믿나(폴더, { env = process.env, platform = process.platform
103
177
  if (env.DEEL_TRUST_ALL === '1') return true; // 검사·컨테이너용. 문서에 안 적는다.
104
178
  const 나 = 고른경로(폴더, platform);
105
179
  for (const 것 of 목록읽기(env)) {
180
+ // 목록에 적힌 넓은 자리는 읽을 때도 안 믿는다 (위 너무넓은자리 머리말).
181
+ // 파일은 안 고친다 — 적힌 것을 조용히 지우면 무엇이 왜 빠졌는지가 안 남는다.
182
+ if (너무넓은자리(것, { env, platform })) continue;
106
183
  const 그것 = 고른경로(것, platform);
107
184
  if (나 === 그것 || 나.startsWith(그것 + '/')) return true;
108
185
  }
109
186
  return false;
110
187
  }
111
188
 
189
+ /** 목록 파일의 글을 목록으로. 못 풀면 던진다 — 던진 것을 어떻게 칠지는 부르는 쪽이 정한다. */
190
+ function 목록풀기(글) {
191
+ const raw = JSON.parse(BOM떼기(글));
192
+ const 것들 = Array.isArray(raw) ? raw : raw?.trusted;
193
+ return Array.isArray(것들) ? 것들.filter((x) => typeof x === 'string') : [];
194
+ }
195
+
196
+ /*
197
+ * ── 열두 개를 한꺼번에 믿으면 여섯 개만 남았다 ─────────────────────────
198
+ *
199
+ * 믿기·안믿기는 「읽고 → 하나 더하고 → 통째로 쓰기」 였다. 둘이 같은 옛 목록을
200
+ * 읽고 각자 제 것을 더해 쓰면 **뒤에 쓴 쪽이 앞의 것을 덮는다.** 저장소 여럿을
201
+ * 한꺼번에 준비하는 스크립트가 딱 그렇게 부르고, 실제로 열둘을 띄워 여섯이 남았다.
202
+ * 적힌 줄 알았던 폴더는 조용히 안 믿긴다.
203
+ *
204
+ * 세 겹으로 막는다.
205
+ * 1. 잠금 파일(`trusted.json.lock`)을 `wx`(없을 때만 만들기)로 잡는다. 그건 두
206
+ * 프로세스가 동시에 성공할 수 없는 연산이라, 읽고-쓰는 사이에 아무도 못 낀다.
207
+ * 다 쓰고 나서 다시 읽어 맞추는 것만으로는 모자랐다 — 확인을 마친 **뒤에**
208
+ * 옛 목록을 들고 있던 쪽이 덮으면 그 확인은 아무것도 안 지킨다.
209
+ * 2. 쓸 때는 옆에 임시 파일로 쓰고 이름을 바꾼다. 그래야 그 사이에 믿나() 가
210
+ * 반쯤 쓴 파일을 읽고 「깨졌으니 아무것도 안 믿는다」 로 떨어지지 않는다.
211
+ * 3. 쓰는 쪽은 잠깐 못 읽은 것(윈도우에서 이름 바꾸는 순간의 EPERM)을 빈 목록으로
212
+ * 치지 않는다. 치면 남의 것을 다 지운 목록을 쓰게 된다. 믿나() 는 여전히
213
+ * 닫는 쪽(빈 목록)으로 떨어진다 — 거기서 안전한 쪽은 그쪽이다.
214
+ *
215
+ * 잠금을 쥔 채 죽은 프로세스가 있으면 영영 못 쓰게 된다. 그래서 오래된(10초)
216
+ * 잠금은 치운다. 믿기 한 번은 1밀리초 안쪽이라 10초짜리 진짜 잠금은 없다.
217
+ */
218
+ const 잠금오래됨 = 10_000;
219
+ const 잠금기다림 = 5_000;
220
+
221
+ function 쉬기(ms) {
222
+ try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* 못 쉬면 바로 다시 본다 */ }
223
+ }
224
+
225
+ /*
226
+ * 기다리는 시간은 부르는 쪽이 줄일 수 있다. `deel trust` 는 기본값을 쓰고, 검사는
227
+ * 잠금을 먼저 쥐고 짧게 기다려 본다 — 경합에 기대지 않고 「잠금을 지키나」 를 재려고.
228
+ */
229
+ function 잠그고(env, 할일, 기다림 = 잠금기다림) {
230
+ mkdirSync(사용자자리(env), { recursive: true });
231
+ const 잠금 = `${신뢰자리(env)}.lock`;
232
+ const 끝 = Date.now() + 기다림;
233
+ for (;;) {
234
+ try {
235
+ closeSync(openSync(잠금, 'wx'));
236
+ break;
237
+ } catch (err) {
238
+ if (err?.code !== 'EEXIST' && err?.code !== 'EPERM' && err?.code !== 'EACCES') throw err;
239
+ try {
240
+ if (Date.now() - statSync(잠금).mtimeMs > 잠금오래됨) { rmSync(잠금, { force: true }); continue; }
241
+ } catch { /* 그 사이에 풀렸다 — 바로 다시 잡아 본다 */ }
242
+ if (Date.now() > 끝) throw new Error(`다른 deel 이 믿는 목록을 쓰고 있어 기다리다 그만뒀습니다 (${잠금})`);
243
+ 쉬기(5 + Math.floor(Math.random() * 20));
244
+ }
245
+ }
246
+ try { return 할일(); } finally { try { rmSync(잠금, { force: true }); } catch { /* 오래되면 다음 사람이 치운다 */ } }
247
+ }
248
+
249
+ /** 쓰는 자리의 읽기. 파일이 없거나 깨졌으면 빈 목록, **잠깐 못 읽은 것은 다시 읽는다.** */
250
+ function 쓸목록읽기(env) {
251
+ const p = 신뢰자리(env);
252
+ for (let 번 = 0; ; 번++) {
253
+ let 글;
254
+ try { 글 = readFileSync(p, 'utf8'); }
255
+ catch (err) {
256
+ if (err?.code === 'ENOENT') return [];
257
+ if (번 >= 20) throw err;
258
+ 쉬기(10);
259
+ continue;
260
+ }
261
+ // 깨진 파일은 예전처럼 새 목록으로 덮는다. 믿나() 는 이미 그 파일을 안 믿고 있다.
262
+ try { return 목록풀기(글); } catch { return []; }
263
+ }
264
+ }
265
+
112
266
  function 쓰기(것들, env) {
113
267
  const p = 신뢰자리(env);
114
268
  mkdirSync(사용자자리(env), { recursive: true });
115
- writeFileSync(p, JSON.stringify({ version: 1, trusted: 것들 }, null, 2) + '\n', 'utf8');
116
- return p;
269
+ const 임시 = `${p}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
270
+ writeFileSync(임시, JSON.stringify({ version: 1, trusted: 것들 }, null, 2) + '\n', 'utf8');
271
+ // 윈도우는 누가 그 파일을 읽는 순간 이름 바꾸기를 EPERM 으로 튕긴다. 잠깐 뒤에 된다.
272
+ for (let 번 = 0; ; 번++) {
273
+ try { renameSync(임시, p); return p; }
274
+ catch (err) {
275
+ if (번 >= 20) { try { rmSync(임시, { force: true }); } catch { /* 남아도 목록은 멀쩡하다 */ } throw err; }
276
+ 쉬기(10);
277
+ }
278
+ }
117
279
  }
118
280
 
119
281
  /** 이 폴더를 믿는다고 적는다. */
120
- export function 믿기(폴더, { env = process.env, platform = process.platform } = {}) {
121
- const 것들 = 목록읽기(env);
282
+ export function 믿기(폴더, { env = process.env, platform = process.platform, 기다림 = 잠금기다림 } = {}) {
283
+ // 넓은 자리는 여기서도 안 적는다. `deel trust` 앞에서만 막으면 이 함수를 부르는
284
+ // 다른 문(스크립트·훗날의 명령)이 그대로 열려 있다. 적어 봐야 읽을 때 버려진다.
285
+ if (너무넓은자리(resolve(String(폴더 ?? '')), { env, platform })) {
286
+ return { ok: false, 넓음: true, 왜: 말('trust.refuseWide') };
287
+ }
122
288
  const 나 = 고른경로(폴더, platform);
123
- if (것들.some((x) => 고른경로(x, platform) === 나)) return { ok: true, 이미: true, 자리: 신뢰자리(env) };
124
- 것들.push(resolve(String(폴더)));
125
- try { return { ok: true, 이미: false, 자리: 쓰기(것들, env) }; }
126
- catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
289
+ try {
290
+ // 받아 놓고 안 넘기면 기본값(5초)을 기다린다. 짧게 기다려 보려고 준 수가 아무 일도 안 하면,
291
+ // 그 수를 준 쪽(검사·배치)은 잠금을 지키는지 재고 있다고 믿으면서 5초를 서 있는다.
292
+ return 잠그고(env, () => {
293
+ const 것들 = 쓸목록읽기(env);
294
+ if (것들.some((x) => 고른경로(x, platform) === 나)) return { ok: true, 이미: true, 자리: 신뢰자리(env) };
295
+ 것들.push(resolve(String(폴더)));
296
+ return { ok: true, 이미: false, 자리: 쓰기(것들, env) };
297
+ }, 기다림);
298
+ } catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
127
299
  }
128
300
 
129
301
  /**
130
302
  * 이 폴더를 믿는 목록에서 뺀다.
131
303
  *
132
- * 위 폴더를 믿어서 믿기던 것이면 못 뺀다 — 여기서 「뺐습니다」 라고 하고
133
- * 실제로는 위 폴더 때문에 그대로 믿기면, 그게 제일 나쁜 거짓말이다.
134
- * 그래서 무엇 때문에 아직 믿기는지를 돌려준다.
304
+ * 빼는 것은 **적힌 줄 하나**다. 위 폴더를 믿어서 믿기던 것이면 그 줄을 빼도 그대로
305
+ * 믿긴다 — 여기서 「뺐습니다」 만 돌려주고 말면, 그게 제일 나쁜 거짓말이다. 사람은
306
+ * 닫힌 줄 알고 그 폴더에서 일한다. 그래서 무엇 때문에 아직 믿기는지(위폴더)와
307
+ * 아직 믿기는지 그 자체(아직믿김)를 같이 돌려준다. `deel trust --off` 는 둘을 같이 찍는다.
308
+ *
309
+ * 아직 믿기는지는 **믿나() 와 같은 자로** 잰다. 목록에 적힌 넓은 자리는 믿나() 가 읽을
310
+ * 때 버리는데(너무넓은자리) 여기서만 세던 동안, 아무것도 안 믿기는 폴더를 두고
311
+ * 「위 폴더 때문에 아직 믿습니다」 라고 겁줬다 — 닫은 사람이 안 닫힌 줄 알고 찾아다닌다.
312
+ * 두 자리가 다른 자를 쓰면 어느 쪽이든 거짓말이 된다.
135
313
  */
136
314
  export function 안믿기(폴더, { env = process.env, platform = process.platform } = {}) {
137
- const 것들 = 목록읽기(env);
138
315
  const 나 = 고른경로(폴더, platform);
139
- const 남길것 = 것들.filter((x) => 고른경로(x, platform) !== 나);
140
- const 뺐나 = 남길것.length !== 것들.length;
316
+ let 남길것;
317
+ let 뺐나 = false;
141
318
  let 자리 = 신뢰자리(env);
142
- if (뺐나) {
143
- try { 자리 = 쓰기(남길것, env); }
144
- catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
145
- }
146
- const 위 = 남길것.find((x) => 나.startsWith(고른경로(x, platform) + '/')) ?? null;
147
- return { ok: true, 뺐나, 자리, 위폴더: 위 };
319
+ // 빼는 쪽도 같은 잠금을 쥔다. 믿기 사이에 끼어 옛 목록으로 덮으면 남이 방금 믿은 폴더가 사라진다.
320
+ try {
321
+ 잠그고(env, () => {
322
+ const 것들 = 쓸목록읽기(env);
323
+ 남길것 = 것들.filter((x) => 고른경로(x, platform) !== 나);
324
+ 뺐나 = 남길것.length !== 것들.length;
325
+ if (뺐나) 자리 = 쓰기(남길것, env);
326
+ });
327
+ } catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
328
+ const 위 = 남길것.find((x) => !너무넓은자리(x, { env, platform }) && 나.startsWith(고른경로(x, platform) + '/')) ?? null;
329
+ return { ok: true, 뺐나, 자리, 위폴더: 위, 아직믿김: 믿나(폴더, { env, platform }) };
148
330
  }
149
331
 
150
332
  /*
@@ -167,12 +349,40 @@ export function 안믿기(폴더, { env = process.env, platform = process.platfo
167
349
  * 화면에서 이 세 줄만 한국어로 남는다 — 그것도 하필 「무엇을 왜 막았나」 를
168
350
  * 설명하는 자리라, 못 읽으면 막힌 까닭을 영영 모른다.
169
351
  */
352
+ /*
353
+ * ── 셋으로는 모자랐다 ──────────────────────────────────────────────────
354
+ *
355
+ * 위 셋은 저장소에 **적힌** 열쇠와 명령만 막았다. 그런데 열쇠는 저장소에 안
356
+ * 적어도 딸려 간다. 이 PC 프로필과 같은 id 로 `baseUrl` 한 칸만 적으면 겹치기가
357
+ * 칸 단위로 덮어서, **이 PC 열쇠가 저장소가 고른 주소로** 붙었다. 이 파일
358
+ * 머리말이 막는다고 적어 둔 그 모양이다(docs/ko/config.md 도 같은 예를 든다).
359
+ *
360
+ * 같은 부류가 넷 더 있었다 — 봉인 풀기(offline:false), 저장소 프로필로
361
+ * 연결 돌리기(active), 모든 요청을 가로채는 프록시(proxy), Bash 자식에게
362
+ * 비밀 환경변수를 도로 물려주기(셸환경), 사람만 붙이는 바깥 허가(online).
363
+ * 전부 넓히는 칸이다.
364
+ *
365
+ * 연결 칸과 active 는 이 PC 설정을 봐야 가를 수 있어 config.js 의 겹치기가
366
+ * 걷는다. 목록은 여기 한 자리에 둔다 — 화면(`deel trust --list`)과 소식이
367
+ * 같은 이름을 쓰게.
368
+ */
170
369
  export const 프로젝트금지칸 = Object.freeze([
171
370
  { 칸: 'permissions.allow', 열쇠: 'trust.why.allow' },
172
371
  { 칸: 'profiles[].apiKey', 열쇠: 'trust.why.apiKey' },
173
372
  { 칸: 'profiles[].열쇠받기', 열쇠: 'trust.why.authCmd' },
373
+ { 칸: 'profiles[].baseUrl·kind·auth·제공자', 열쇠: 'trust.why.connection' },
374
+ { 칸: 'profiles[].online', 열쇠: 'trust.why.online' },
375
+ // 최상위 offline 과 같은 까닭이다. 프로필 안에 적어도 봉인은 봉인이다.
376
+ { 칸: 'profiles[].offline', 열쇠: 'trust.why.offline' },
377
+ { 칸: 'active', 열쇠: 'trust.why.active' },
378
+ { 칸: 'offline', 열쇠: 'trust.why.offline' },
379
+ { 칸: 'proxy', 열쇠: 'trust.why.proxy' },
380
+ { 칸: '셸환경', 열쇠: 'trust.why.shellEnv' },
174
381
  ]);
175
382
 
383
+ /** 이 PC 프로필에 겹칠 때 저장소가 못 바꾸는 칸. 바꾸면 열쇠가 딴 데로 간다. */
384
+ export const 연결칸 = Object.freeze(['baseUrl', 'kind', 'auth', '제공자']);
385
+
176
386
  /**
177
387
  * 프로젝트 설정에서 못 정하는 칸을 걷어낸다.
178
388
  *
@@ -193,10 +403,48 @@ export function 프로젝트거르기(raw) {
193
403
  delete 값.permissions.allow;
194
404
  적기('permissions.allow');
195
405
  }
406
+ // 봉인은 켜는 것만 받는다. 끄는 것은 이 PC 가 켠 봉인을 저장소가 푸는 길이다.
407
+ // 글자 'true' 도 켠 것이다 (safety/runmode.js 의 봉인됐나와 같은 기준).
408
+ const 켠봉인 = (v) => v === true || v === 'true';
409
+ if (Object.prototype.hasOwnProperty.call(값, 'offline') && !켠봉인(값.offline)) {
410
+ delete 값.offline;
411
+ 적기('offline');
412
+ }
413
+ if (값.proxy !== undefined) { delete 값.proxy; 적기('proxy'); }
414
+ // 영어 이름도 같은 칸이다(safety/shellenv.js 의 남길것읽기).
415
+ if (값.셸환경 !== undefined || 값.shellEnv !== undefined) {
416
+ delete 값.셸환경;
417
+ delete 값.shellEnv;
418
+ 적기('셸환경');
419
+ }
420
+ // 목록이 아니면 겹치는 자리에서 켜자마자 터졌다. 통째로 안 읽고 그렇게 말한다.
421
+ // 까닭은 형제 칸과 같이 **열쇠**로 든다 — 여기만 한국어를 박아 두면 영어로 켠 사람의
422
+ // 화면에서 이 줄만 한글로 남는다 (바로 위 「까닭은 글이 아니라 열쇠로 든다」).
423
+ if (값.profiles !== undefined && !Array.isArray(값.profiles)) {
424
+ delete 값.profiles;
425
+ 걸러낸것.push({ 칸: 'profiles', 열쇠: 'trust.why.profilesNotArray' });
426
+ }
196
427
  for (const p of Array.isArray(값.profiles) ? 값.profiles : []) {
197
428
  if (!p || typeof p !== 'object') continue;
198
429
  if (p.apiKey !== undefined) { delete p.apiKey; 적기('profiles[].apiKey'); }
199
- if (p.열쇠받기 !== undefined) { delete p.열쇠받기; 적기('profiles[].열쇠받기'); }
430
+ // 영어 이름(authCommand)도 같은 칸이다 — safety/authcmd.js 의 받기설정이 둘 다 읽는다.
431
+ // 한글 이름만 걷던 동안 저장소가 영어로 적으면 명령이 그대로 돌았다.
432
+ if (p.열쇠받기 !== undefined || p.authCommand !== undefined) {
433
+ delete p.열쇠받기;
434
+ delete p.authCommand;
435
+ 적기('profiles[].열쇠받기');
436
+ }
437
+ if (p.online !== undefined) { delete p.online; 적기('profiles[].online'); }
438
+ /*
439
+ * 프로필 안의 봉인도 켜는 것만 받는다.
440
+ *
441
+ * 최상위 `offline` 만 걷고 여기는 안 걷었다. 겹치기(config.js)가 프로필을 칸
442
+ * 단위로 덮으니, 믿는 저장소가 `{"id":"loc","offline":false}` 한 줄로 이 PC
443
+ * 프로필의 `offline:true` 를 풀었다 — 요청이 바깥으로 나갔고, 걷었다는 말도
444
+ * 없었고, `deel config explain` 은 저장소 값이 이겼다고 그렸다. releases/2.0 이
445
+ * 「저장소는 봉인을 못 끈다」 고 약속한 바로 그 자리다.
446
+ */
447
+ if (p.offline !== undefined && !켠봉인(p.offline)) { delete p.offline; 적기('profiles[].offline'); }
200
448
  }
201
449
  return { 값, 걸러낸것 };
202
450
  }
@@ -223,8 +471,26 @@ export function 프로젝트설정줄들(소식) {
223
471
  // 걸러냄 — 무엇을 왜 걷어냈는지까지 말한다. 개수만 말하면 사람이 제
224
472
  // 설정에서 무엇을 지워야 하는지 모른 채로 남는다.
225
473
  줄.push(` ${mark.warn} ${말('trust.filtered')}`);
226
- for (const { 칸, 열쇠 } of 소식.걸러낸것 ?? []) {
227
- 줄.push(` ${c.gray(' ·')} ${c.white(칸)} ${c.gray(말(열쇠))}`);
474
+ /*
475
+ * ── 까닭 자리가 빈 줄로 찍히고 있었다 ─────────────────────────────────
476
+ *
477
+ * 걸러낸 것은 두 자리에서 온다. 프로젝트금지칸() 은 `{칸, 열쇠}`(i18n
478
+ * 열쇠)를 주고, config.js 의 프로필 거르기는 `{칸, 왜}`(이미 지어 둔 말)를
479
+ * 준다. 여기는 `열쇠` 만 꺼내서 `말(undefined)` 을 불렀고, 그건 빈 글자다.
480
+ *
481
+ * ⚠ 프로젝트 설정에서 걸러낸 것이 있습니다
482
+ * · profiles
483
+ *
484
+ * 「id 도 name 도 없는 것 2개는 못 붙였습니다」 가 통째로 사라진 자리다.
485
+ * 개수도 까닭도 없으니, 적어 둔 사람은 제 설정에서 무엇을 고쳐야 하는지
486
+ * 모른 채 남는다 — 이 블록 바로 위 주석이 그러지 말자고 적어 둔 것이다.
487
+ *
488
+ * 검사가 못 잡은 까닭도 같다. test/trust.test.js 는 `{칸, 열쇠}` 꼴만
489
+ * 먹여 본다.
490
+ */
491
+ for (const { 칸, 열쇠, 왜 } of 소식.걸러낸것 ?? []) {
492
+ const 까닭 = 열쇠 ? 말(열쇠) : (왜 ?? '');
493
+ 줄.push(` ${c.gray(' ·')} ${c.white(칸)}${까닭 ? ` ${c.gray(까닭)}` : ''}`);
228
494
  }
229
495
  return 줄;
230
496
  }