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
@@ -4,7 +4,7 @@
4
4
  // 오프라인 기기에 반입한다. 오프라인에서는 압축만 풀면 그대로 인식된다.
5
5
  import { execFile } from 'node:child_process';
6
6
  import { homeDir } from '../config.js';
7
- import { join, dirname, basename, resolve, sep } from 'node:path';
7
+ import { join, dirname, basename, resolve, relative, sep } from 'node:path';
8
8
  import {
9
9
  existsSync, mkdirSync, writeFileSync, readFileSync, rmSync,
10
10
  readdirSync, statSync,
@@ -25,6 +25,7 @@ export function 플러그인되돌림(다음) {
25
25
  }
26
26
  }
27
27
  import { copyDir } from '../tools/fsutil.js';
28
+ import { BOM떼기 } from '../safety/trust.js';
28
29
 
29
30
  /*
30
31
  * 플러그인이 깔리는 자리.
@@ -93,6 +94,36 @@ export function 묶음풀기(dest, files) {
93
94
  if (밖엣것.length) {
94
95
  return { error: `묶음 안에 플러그인 폴더 밖을 가리키는 이름이 있습니다 — 풀지 않았습니다: ${밖엣것[0].name}` };
95
96
  }
97
+ /*
98
+ * ── 안쪽이어도 **못 쓰는** 이름은 지우기 전에 거른다 (2.0.0 3회차 사냥) ──
99
+ *
100
+ * 위는 밖으로 나가는 것만 본다. 안쪽인데 쓰다가 넘어지는 이름이 셋 있었다:
101
+ *
102
+ * `a/..` 풀 자리 그 자체 → EISDIR
103
+ * `x` 와 `x/y` 파일과 폴더가 같은 이름 → EEXIST
104
+ * `Doc.md` · `doc.md` 윈도우·맥에서는 한 파일 → 하나가 말없이 사라진다
105
+ *
106
+ * 앞의 둘은 **rmSync 뒤에** 던져서 풀 자리를 비운 채 날것 오류(EISDIR)로 끝났고, 셋째는 「2개 풀었다」
107
+ * 고 하고 하나만 남겼다. 쓰기 전에 전부 재 보고 사람 말로 거절한다.
108
+ */
109
+ const 뿌리 = resolve(dest);
110
+ const 자리들 = new Map(); // 소문자 상대 자리 → 적힌 이름
111
+ for (const f of files) {
112
+ const 상대 = relative(뿌리, resolve(뿌리, String(f.name ?? '').replace(/\\/g, '/'))).replace(/\\/g, '/');
113
+ if (!상대) return { error: `묶음 안에 풀 폴더 그 자체를 가리키는 파일이 있습니다 — 풀지 않았습니다: ${f.name}` };
114
+ const 열쇠 = 상대.toLowerCase();
115
+ if (자리들.has(열쇠)) {
116
+ return { error: `묶음 안에 같은 자리를 가리키는 이름이 둘 있습니다(대소문자만 다를 수 있음) — 풀지 않았습니다: ${자리들.get(열쇠)} · ${f.name}` };
117
+ }
118
+ 자리들.set(열쇠, f.name);
119
+ }
120
+ for (const [열쇠, 이름] of 자리들) {
121
+ const 마디 = 열쇠.split('/');
122
+ for (let i = 1; i < 마디.length; i++) {
123
+ const 윗자리 = 마디.slice(0, i).join('/');
124
+ if (자리들.has(윗자리)) return { error: `묶음 안에 파일과 폴더가 같은 이름입니다 — 풀지 않았습니다: ${자리들.get(윗자리)} · ${이름}` };
125
+ }
126
+ }
96
127
  rmSync(dest, { recursive: true, force: true });
97
128
  for (const f of files) {
98
129
  const p = join(dest, f.name);
@@ -193,6 +224,19 @@ export function 이름한칸(이름) {
193
224
  if (!한칸 || 한칸 === '.' || 한칸 === '..') return null;
194
225
  // 드라이브 글자·칸막이가 남아 있으면 이름이 아니다.
195
226
  if (/[/:]/.test(한칸)) return null;
227
+ /*
228
+ * ── 윈도우가 폴더 이름으로 못 쓰는 것도 이름이 아니다 (2.0.0 4회차 사냥) ──
229
+ *
230
+ * 칸막이만 막았더니 `a*b` · `a?b` · NUL 글자 같은 이름이 통과해 install() 이 mkdir 에서
231
+ * **던지고** `.tmp-*` 를 남겼다. `keep.` 은 윈도우가 끝 점을 떼어 `keep` 자리에 깔려
232
+ * 옆 플러그인과 겹쳤고, `CON` · `nul.txt` 는 장치 이름이라 파일이 딴 데로 간다.
233
+ * 리눅스에서는 쓸 수 있는 이름도 막는다 — 묶음(pack)은 윈도우 PC 로 반입된다.
234
+ * 막힌 이름이면 부르는 쪽이 우리가 아는 이름(폴더·저장소 이름)으로 간다.
235
+ */
236
+ if (/[<>"|?*]/.test(한칸) || [...한칸].some((ch) => ch.charCodeAt(0) < 32 || ch.charCodeAt(0) === 127)) return null;
237
+ if (/[. ]$/.test(한칸)) return null;
238
+ if (/^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i.test(한칸)) return null;
239
+ if (한칸.length > 100) return null;
196
240
  return 한칸;
197
241
  }
198
242
 
@@ -211,7 +255,8 @@ export function 플러그인자리인가(base, dest) {
211
255
  function manifestOf(dir) {
212
256
  const f = join(dir, '.claude-plugin', 'plugin.json');
213
257
  if (!existsSync(f)) return null;
214
- try { return JSON.parse(readFileSync(f, 'utf8')); } catch { return null; }
258
+ // BOM 을 뗀다 — 윈도우에서 저장한 plugin.json 이 이름·판·라이선스 없이 읽혔다(2.0.0 3회차).
259
+ try { return JSON.parse(BOM떼기(readFileSync(f, 'utf8'))); } catch { return null; }
215
260
  }
216
261
 
217
262
  function countIn(dir) {
@@ -252,7 +297,22 @@ function walkCount(dir, match, depth = 5) {
252
297
  return n;
253
298
  }
254
299
 
255
- export async function install(spec, { home = homeDir(), onStep } = {}) {
300
+ /*
301
+ * 설치는 **던지지 않는다.** 이름을 걸러도 디스크가 넘어질 자리(권한·꽉 찬 디스크·긴 경로)는
302
+ * 남는다. 던지면 `/plugin install` 이 통째로 죽고 `.tmp-*` 가 남아, 다음 설치가 그 찌꺼기
303
+ * 위에서 돈다(2.0.0 4회차 사냥). 받은 오류는 사람 말로 돌려주고 임시 폴더를 치운다.
304
+ */
305
+ export async function install(spec, 옵션 = {}) {
306
+ const 치울것 = [];
307
+ try {
308
+ return await 설치하기(spec, 옵션, 치울것);
309
+ } catch (err) {
310
+ for (const p of 치울것) { try { rmSync(p, { recursive: true, force: true }); } catch { /* 못 치우면 다음 설치가 먼저 지운다 */ } }
311
+ return { error: `플러그인을 설치하지 못했습니다 — ${String(err?.message ?? err)}` };
312
+ }
313
+ }
314
+
315
+ async function 설치하기(spec, { home = homeDir(), onStep } = {}, 치울것 = []) {
256
316
  const base = pluginsDir(home);
257
317
 
258
318
  // 이미 풀어 놓은 폴더를 그대로 넣는 길. 오프라인 기기에서 이쪽을 쓴다.
@@ -265,12 +325,31 @@ export async function install(spec, { home = homeDir(), onStep } = {}) {
265
325
  }
266
326
 
267
327
  const tmp = join(base, `.tmp-${isLocal ? 'local' : parsed.repo}`);
328
+ 치울것.push(tmp);
329
+ /*
330
+ * ── copyDir 이 건너뛴 것을 아무도 안 읽고 있었다 ──────────────────────
331
+ *
332
+ * copyDir(tools/fsutil.js)은 심볼릭 링크와 파일도 폴더도 아닌 것을
333
+ * **말없이 건너뛴다.** 그러라고 `skipped` 배열까지 내주는데, 여기
334
+ * 두 군데 부르는 자리가 둘 다 그 값을 안 받았다.
335
+ *
336
+ * 링크로 SKILL.md 를 나눠 쓰는 것은 흔한 꼴이다(맥·리눅스에서 스킬
337
+ * 여러 개가 같은 뼈대를 가리키게 하는 것). 그런 플러그인을 넣으면
338
+ * 화면에는 `✓ myplugin 1.0.0 (MIT) · 스킬 3개 명령 2개` 가 뜨고,
339
+ * 실제로 깔린 것은 2개다.
340
+ *
341
+ * 세는 자리도 어긋나 있었다. countIn 을 **복사하기 전** 임시 폴더에서
342
+ * 부르므로, 복사하다 빠진 것이 개수에 안 잡힌다. 나중에 `/plugin` 으로
343
+ * 목록을 보면 list() 는 진짜 폴더를 세니 숫자가 다르다 — 같은 상태를
344
+ * 두 자리에서 따로 세는 바로 그 모양이다.
345
+ */
346
+ const 건너뛴것 = [];
268
347
  let got;
269
348
  if (isLocal) {
270
349
  onStep?.('폴더 복사');
271
350
  rmSync(tmp, { recursive: true, force: true });
272
351
  mkdirSync(dirname(tmp), { recursive: true });
273
- copyDir(asPath, tmp);
352
+ copyDir(asPath, tmp, { skipped: 건너뛴것 });
274
353
  rmSync(join(tmp, '.git'), { recursive: true, force: true });
275
354
  got = { how: '폴더', from: asPath };
276
355
  } else {
@@ -298,16 +377,18 @@ export async function install(spec, { home = homeDir(), onStep } = {}) {
298
377
  return { error: `설치할 자리가 플러그인 폴더 밖입니다 — 설치하지 않았습니다 (${name}).` };
299
378
  }
300
379
 
301
- const counts = countIn(tmp);
302
- if (!counts.skills && !counts.commands) {
380
+ const 담을것 = countIn(tmp);
381
+ if (!담을것.skills && !담을것.commands) {
303
382
  rmSync(tmp, { recursive: true, force: true });
304
383
  return { error: `스킬도 명령도 없습니다. 플러그인이 맞는지 확인하세요 (${isLocal ? asPath : parsed.owner + '/' + parsed.repo})` };
305
384
  }
306
385
 
307
386
  rmSync(dest, { recursive: true, force: true });
308
387
  mkdirSync(dirname(dest), { recursive: true });
309
- copyDir(tmp, dest);
388
+ copyDir(tmp, dest, { skipped: 건너뛴것 });
310
389
  rmSync(tmp, { recursive: true, force: true });
390
+ // 개수는 **깔린 자리**에서 다시 센다. 복사하다 빠진 것이 여기서 빠진다.
391
+ const counts = countIn(dest);
311
392
 
312
393
  // 어디서 왔는지 남긴다 — 나중에 반입 심사에서 출처를 물어본다.
313
394
  writeFileSync(join(dest, '.deel-source.json'), JSON.stringify({
@@ -318,7 +399,16 @@ export async function install(spec, { home = homeDir(), onStep } = {}) {
318
399
  license: info?.license ?? null,
319
400
  }, null, 2) + '\n', 'utf8');
320
401
 
321
- return { name, version: info?.version ?? '', license: info?.license ?? null, path: dest, ...counts, how: got.how };
402
+ return {
403
+ name, version: info?.version ?? '', license: info?.license ?? null, path: dest,
404
+ ...counts, how: got.how,
405
+ // 건너뛴 것이 있으면 **개수와 함께** 올린다. 부르는 쪽이 이걸 안 적으면
406
+ // 「스킬 3개」 라고 해 놓고 2개만 깔린 상태가 그대로 초록으로 보인다.
407
+ 건너뜀: 건너뛴것,
408
+ // 세기 전과 후가 다르면 그것도 말한다 — 무엇이 빠졌는지 짚어 주는 값이다.
409
+ ...(담을것.skills !== counts.skills || 담을것.commands !== counts.commands
410
+ ? { 덜깔림: { 담을것, 깔린것: counts } } : {}),
411
+ };
322
412
  }
323
413
 
324
414
  export function list({ home = homeDir() } = {}) {
@@ -331,9 +421,16 @@ export function list({ home = homeDir() } = {}) {
331
421
  const info = manifestOf(dir);
332
422
  let src = null;
333
423
  const sf = join(dir, '.deel-source.json');
334
- if (existsSync(sf)) { try { src = JSON.parse(readFileSync(sf, 'utf8')); } catch {} }
424
+ if (existsSync(sf)) { try { src = JSON.parse(BOM떼기(readFileSync(sf, 'utf8'))); } catch {} }
425
+ /*
426
+ * 매니페스트 이름이 폴더 이름으로 못 쓸 것(`..` · `keep.`)이면 **폴더 이름**을 보인다.
427
+ * 목록이 보여 준 이름으로 remove() 가 못 찾으면 사람은 지울 길이 없다(2.0.0 4회차 사냥).
428
+ * 쓸 수 있는 이름이면 매니페스트 이름 그대로다 — 폴더와 달라도 remove() 가 그 이름으로 찾는다.
429
+ */
430
+ const 적힌 = typeof info?.name === 'string' ? info.name : '';
335
431
  out.push({
336
- name: info?.name || e.name,
432
+ name: 적힌 && 이름한칸(적힌) === 적힌 ? 적힌 : e.name,
433
+ 폴더: e.name,
337
434
  version: info?.version ?? '',
338
435
  license: info?.license ?? src?.license ?? null,
339
436
  from: src?.from ?? '(직접 넣음)',
@@ -354,12 +451,55 @@ export function remove(name, { home = homeDir() } = {}) {
354
451
  */
355
452
  const base = pluginsDir(home);
356
453
  const 한칸 = 이름한칸(name);
357
- const dir = 한칸 ? join(base, 한칸) : null;
358
- if (!dir || !플러그인자리인가(base, dir) || !existsSync(dir)) {
359
- return { error: `설치돼 있지 않습니다: ${name}` };
454
+ const 찾는이름 = String(name ?? '').trim();
455
+ /*
456
+ * 폴더 이름으로 못 찾으면 **매니페스트 이름**으로 찾는다. 목록(list)은 매니페스트
457
+ * 이름을 보여 주는데, 손으로 넣은 플러그인은 폴더 이름과 다르다. 찾는 대상은
458
+ * 플러그인 폴더를 읽어 나온 칸뿐이라 밖으로 나갈 길이 없다. 둘 이상 걸리면 아무것도
459
+ * 안 지운다 — 어느 것인지 우리가 고르면 사람이 안 고른 것을 지우게 된다.
460
+ */
461
+ const 이름으로찾기 = () => {
462
+ try {
463
+ return readdirSync(base, { withFileTypes: true })
464
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.') && 찾는이름
465
+ && manifestOf(join(base, e.name))?.name === 찾는이름)
466
+ .map((e) => e.name);
467
+ } catch { return []; } // 플러그인 폴더가 없다 — 아래에서 없다고 말한다
468
+ };
469
+ let dir = 한칸 ? join(base, 한칸) : null;
470
+ if (dir && 플러그인자리인가(base, dir) && existsSync(dir)) {
471
+ /*
472
+ * ── 폴더로 찾았어도 **목록 이름**과 겹치는지 본다 (6회차 Gemini 플러그인 P6) ──
473
+ *
474
+ * 목록은 매니페스트 이름을 보여 준다. 폴더 `foo` 의 이름이 `bar` 이고 딴 폴더
475
+ * `plugin-a` 의 이름이 `foo` 면, 사람이 목록에서 보고 친 `foo` 는 plugin-a 다.
476
+ * 그런데 폴더부터 찾아 **bar 를** 지웠다. 두 가지로 읽히면 아무것도 안 지운다.
477
+ */
478
+ /*
479
+ * 앞에 `manifestOf(dir)?.name === 찾는이름 ? [] :` 이 붙어 있었다 (8회차 판정).
480
+ *
481
+ * 폴더 이름과 그 폴더의 매니페스트 이름이 **같기만 하면** 삼항 앞쪽이 위 검사를
482
+ * 통째로 껐다. 그런데 딴 폴더가 목록에 같은 이름으로 떠 있을 수 있다 —
483
+ * 재 보니 폴더 `foo`(이름 foo) 와 폴더 `plugin-a`(이름 foo) 가 같이 있을 때
484
+ * `remove('foo')` 가 아무 말 없이 `foo` 를 지웠다. 바로 위 주석이 「두 가지로
485
+ * 읽히면 아무것도 안 지운다」 라고 적어 둔 그 자리다. 반쪽 붙은 고침이었다.
486
+ *
487
+ * 지금 폴더를 뺀 나머지에 같은 이름이 있으면, 폴더 이름이 무엇이든 두 가지다.
488
+ */
489
+ const 딴것 = 이름으로찾기().filter((n) => n !== basename(dir));
490
+ if (딴것.length) {
491
+ return { error: `${찾는이름} 이 두 플러그인으로 읽힙니다 — 폴더 이름이 ${찾는이름} 인 것과 목록 이름이 ${찾는이름} 인 ${딴것.join(' · ')}. 목록에 보인 딴 이름이나 겹치지 않는 폴더 이름으로 지우세요` };
492
+ }
493
+ } else {
494
+ const 같은것 = 이름으로찾기();
495
+ if (같은것.length > 1) {
496
+ return { error: `이름이 ${찾는이름} 인 플러그인이 ${같은것.length}개입니다 — 폴더 이름으로 지우세요: ${같은것.join(' · ')}` };
497
+ }
498
+ if (!같은것.length) return { error: `설치돼 있지 않습니다: ${name}` };
499
+ dir = join(base, 같은것[0]);
360
500
  }
361
501
  rmSync(dir, { recursive: true, force: true });
362
- return { removed: 한칸 };
502
+ return { removed: basename(dir) };
363
503
  }
364
504
 
365
505
  // 반입용 묶음. 실행 스크립트는 빼고 스킬·명령만 담는다.
@@ -373,6 +513,7 @@ export function pack(outFile, { home = homeDir(), only = null } = {}) {
373
513
  const entries = [];
374
514
  const included = [];
375
515
  let skipped = 0;
516
+ let 안담은링크 = 0;
376
517
 
377
518
  for (const p of list({ home })) {
378
519
  if (only?.length && !only.includes(p.name)) continue;
@@ -386,6 +527,15 @@ export function pack(outFile, { home = homeDir(), only = null } = {}) {
386
527
  if (!PACK_SKIP_DIRS.has(e.name)) stack.push(full);
387
528
  continue;
388
529
  }
530
+ /*
531
+ * 폴더도 파일도 아닌 것은 담지 않는다 (6회차 Gemini 플러그인 P10).
532
+ *
533
+ * 폴더를 가리키는 링크(윈도 정션·심볼릭 링크)는 isDirectory() 가 거짓이라 파일로
534
+ * 읽다가 EISDIR 로 던졌다 — /plugin pack 이 통째로 죽었다. 따라가도 안 된다:
535
+ * 링크는 플러그인 폴더 밖을 가리킬 수 있고, 이 묶음은 딴 PC 로 반입된다.
536
+ * (손으로 넣은 플러그인에만 있다 — install 의 copyDir 은 링크를 이미 건너뛴다.)
537
+ */
538
+ if (!e.isFile()) { 안담은링크++; continue; }
389
539
  if (PACK_SKIP_EXT.test(e.name)) { skipped++; continue; }
390
540
  const rel = full.slice(base.length + 1).split(/[\\/]/).join('/');
391
541
  entries.push({ name: rel, data: readFileSync(full), mtime: statSync(full).mtime });
@@ -404,6 +554,7 @@ export function pack(outFile, { home = homeDir(), only = null } = {}) {
404
554
  `플러그인 ${included.length}개`,
405
555
  `파일 ${entries.length}개`,
406
556
  `제외한 실행 스크립트 ${skipped}개 (js·sh·ps1·py 등은 담지 않습니다)`,
557
+ ...(안담은링크 ? [`제외한 링크 ${안담은링크}개 (플러그인 폴더 밖을 가리킬 수 있어 담지 않습니다)`] : []),
407
558
  '',
408
559
  '이름'.padEnd(24) + '판'.padEnd(10) + '라이선스'.padEnd(16) + '스킬 명령 출처',
409
560
  '-'.repeat(96),