deel-local-cli 1.13.0 → 1.14.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 (54) hide show
  1. package/README.ko.md +32 -26
  2. package/README.md +32 -26
  3. package/bin/deel.js +6 -1
  4. package/package.json +3 -2
  5. package/src/acp/map.js +367 -367
  6. package/src/acp/serve.js +830 -810
  7. package/src/agent/budget.js +156 -156
  8. package/src/agent/commit.js +534 -534
  9. package/src/agent/compact.js +441 -441
  10. package/src/agent/effort.js +308 -308
  11. package/src/agent/evolve.js +263 -263
  12. package/src/agent/loop.js +1755 -1728
  13. package/src/agent/memory.js +156 -156
  14. package/src/agent/mention.js +210 -210
  15. package/src/agent/recall.js +222 -222
  16. package/src/agent/session.js +1056 -1056
  17. package/src/backend/adapter.js +1355 -1355
  18. package/src/backend/cachemark.js +223 -223
  19. package/src/backend/detect.js +327 -327
  20. package/src/backend/http.js +460 -460
  21. package/src/backend/mcp.js +407 -407
  22. package/src/backend/price.js +260 -260
  23. package/src/backend/probe.js +487 -487
  24. package/src/backend/quota.js +274 -274
  25. package/src/backend/tokens.js +59 -59
  26. package/src/backend/toolfit.js +352 -352
  27. package/src/backend/wire.js +715 -715
  28. package/src/cmdnames.js +68 -0
  29. package/src/commands.js +3116 -3184
  30. package/src/config.js +283 -283
  31. package/src/i18n/en.js +545 -545
  32. package/src/i18n/ja.js +498 -498
  33. package/src/i18n/ko.js +592 -592
  34. package/src/i18n/zh.js +498 -498
  35. package/src/oneshot.js +685 -625
  36. package/src/pack/selfpack.js +15 -2
  37. package/src/pack/sheet.en.js +16 -2
  38. package/src/pack/tar.js +154 -154
  39. package/src/pack/zip.js +235 -235
  40. package/src/plugins/manage.js +416 -416
  41. package/src/repl.js +2616 -2616
  42. package/src/safety/audit.js +148 -148
  43. package/src/safety/guard.js +714 -641
  44. package/src/safety/network.js +200 -200
  45. package/src/tools/fastgrep.js +229 -229
  46. package/src/tools/fsutil.js +312 -291
  47. package/src/tools/index.js +2590 -2560
  48. package/src/tools/jobs.js +876 -876
  49. package/src/tools/spawn.js +213 -213
  50. package/src/tools/verify.js +358 -358
  51. package/src/tools/webfetch.js +418 -418
  52. package/src/ui/export.js +233 -233
  53. package/src/ui/pick.js +115 -115
  54. package/src/ui/status.js +617 -617
@@ -199,7 +199,7 @@ export function reviewSheet(a, at, { lang = 언어() } = {}) {
199
199
  L.push(' 아예 띄우지 않습니다.');
200
200
  L.push('');
201
201
  L.push(' --offline 으로 켜면 [B]·[C]·[D] 가 모두 막히고, 이 컴퓨터 안으로만 다닙니다.');
202
- L.push(' (검증: npm test 안의 network / web / mcp 검사 123항목이 이를 확인합니다)');
202
+ L.push(' (검증: npm test 안의 network / web / mcp 검사가 이를 확인합니다 — 항목 수는 그 출력에 있습니다)');
203
203
  L.push('');
204
204
 
205
205
  L.push('4. 스킬·플러그인');
@@ -209,7 +209,20 @@ export function reviewSheet(a, at, { lang = 언어() } = {}) {
209
209
  L.push(' (검증: npm test 안의 no-bundle 검사가 이를 확인합니다)');
210
210
  L.push('');
211
211
 
212
- L.push(`5. 담긴 파일 ${a.files.length}개 — SHA-256`);
212
+ L.push('5. 검사가 무엇을 지키는가');
213
+ L.push(줄());
214
+ L.push(' 검사가 초록이라는 것은 「아무도 안 깨졌다」 는 뜻이지 「깨면 잡힌다」 는 뜻이');
215
+ L.push(' 아닙니다. 이 둘은 겉으로 구별이 안 되는데 값어치는 정반대입니다.');
216
+ L.push(' 그래서 지켜야 할 줄을 일부러 어긋내 놓고, 짝지은 검사가 빨개지는지 되잽니다.');
217
+ L.push(' 무엇을 어긋내는지와 「그러면 무슨 일이 나는지」 는 test/mutants.json 에 적어');
218
+ L.push(' 두었습니다 — 살림 폴더 통째로 지우기 · 되돌리기 스냅샷 · ACP 문 파리티 ·');
219
+ L.push(' 도움말의 종료코드 표가 그 안에 있습니다. 하나라도 안 잡히면 실패입니다.');
220
+ L.push(' (검증: npm run mutate — 잡은 수와 샌 수가 그 출력에 있습니다)');
221
+ L.push(' 검사와 이 목록은 이 묶음에 안 들어 있습니다 — 묶음에는 도는 코드만 담습니다.');
222
+ L.push(' 저장소에서 보실 수 있습니다: https://github.com/jysvai/deel-local-cli');
223
+ L.push('');
224
+
225
+ L.push(`6. 담긴 파일 ${a.files.length}개 — SHA-256`);
213
226
  L.push(줄());
214
227
  const w = Math.max(...a.files.map((f) => f.path.length));
215
228
  for (const f of a.files) {
@@ -106,7 +106,7 @@ export function reviewSheetEn(a, at, probes) {
106
106
  L.push(' filtering its requests, --offline does not start the server at all.');
107
107
  L.push('');
108
108
  L.push(' Starting with --offline blocks [B], [C] and [D]; traffic stays on this machine.');
109
- L.push(' (Verified by the network / web / mcp checks in npm test - 123 assertions.)');
109
+ L.push(' (Verified by the network / web / mcp checks in npm test; the count is in that output.)');
110
110
  L.push('');
111
111
 
112
112
  L.push('4. Skills and plugins');
@@ -116,7 +116,21 @@ export function reviewSheetEn(a, at, probes) {
116
116
  L.push(' (Verified by the no-bundle check in npm test.)');
117
117
  L.push('');
118
118
 
119
- L.push(`5. ${a.files.length} files shipped - SHA-256`);
119
+ L.push('5. What the tests actually guard');
120
+ L.push(rule());
121
+ L.push(' A green test run means "nothing broke", not "breaking it would be caught".');
122
+ L.push(' The two look identical and are worth the opposite.');
123
+ L.push(' So we deliberately break the lines that matter and check that the paired test');
124
+ L.push(' turns red. What gets broken, and what would go wrong if it stayed broken, is');
125
+ L.push(' written down in test/mutants.json - deleting the whole .deel folder, undo');
126
+ L.push(' snapshots, ACP door parity, and the exit-code table are among them.');
127
+ L.push(' A single survivor fails the run.');
128
+ L.push(' (Verified by npm run mutate; killed and survived counts are in that output.)');
129
+ L.push(' The tests and that list are not in this package - it ships only running code.');
130
+ L.push(' Read them in the repository: https://github.com/jysvai/deel-local-cli');
131
+ L.push('');
132
+
133
+ L.push(`6. ${a.files.length} files shipped - SHA-256`);
120
134
  L.push(rule());
121
135
  const w = Math.max(...a.files.map((f) => f.path.length));
122
136
  for (const f of a.files) {
package/src/pack/tar.js CHANGED
@@ -1,154 +1,154 @@
1
- // tar 읽기. GitHub 가 주는 tarball 을 풀기 위한 최소 구현.
2
- // git 이 없는 기기에서도 플러그인을 받을 수 있어야 해서 필요하다.
3
- import { gunzipSync } from 'node:zlib';
4
- import { resolve, sep } from 'node:path';
5
-
6
- const BLOCK = 512;
7
-
8
- function str(buf, off, len) {
9
- const end = buf.indexOf(0, off);
10
- const stop = end >= 0 && end < off + len ? end : off + len;
11
- return buf.toString('utf8', off, stop).trim();
12
- }
13
-
14
- function octal(buf, off, len) {
15
- const s = str(buf, off, len).replace(/[^0-7]/g, '');
16
- return s ? parseInt(s, 8) : 0;
17
- }
18
-
19
- /*
20
- * ── 작게 받아서 크게 푸는 묶음(압축 폭탄) ───────────────────────────────
21
- *
22
- * 받는 쪽(plugins/manage.js)은 **압축된** 크기를 64MB 로 막는다. 그런데
23
- * 0 으로만 채운 파일은 천 배 넘게 줄어든다 — 64MB 짜리 하나가 풀면 수십 GB 다.
24
- * gunzipSync 는 그걸 통째로 메모리에 올리므로, 화면에는 아무 말도 안 남고
25
- * 프로세스가 그대로 죽는다. 사용자가 보는 것은 「플러그인을 깔아 줘」 라고
26
- * 시킨 뒤 deel 이 사라진 것뿐이다 — 하던 대화까지 같이.
27
- *
28
- * 그래서 **푼 크기**에도 상한을 둔다. 256MB 로 잡은 까닭은 받는 상한의 네 배라서다.
29
- * 플러그인은 글과 스크립트 묶음이라 서너 배까지는 부푼다. 그보다 더 부푸는 것은
30
- * 플러그인이 아니라 폭탄으로 본다.
31
- */
32
- export const 푼것상한 = 256 * 1024 * 1024;
33
-
34
- /**
35
- * gzip 된 tar 를 풀어 파일 목록을 돌려준다.
36
- * @param {Buffer} gz
37
- * @param {object} o
38
- * @param {number} o.상한 푼 크기 상한. 검사가 작은 값으로 부르려고 열어 뒀다 —
39
- * 256MB 를 진짜로 만들어 재면 그 검사 하나가 기계를 잡아먹는다.
40
- * @returns {Array<{name:string, data:Buffer, mode:number}>}
41
- */
42
- export function untargz(gz, { 상한 = 푼것상한 } = {}) {
43
- let buf;
44
- try {
45
- buf = gunzipSync(gz, { maxOutputLength: 상한 });
46
- } catch (err) {
47
- // 조용히 죽는 대신 왜 안 되는지 말한다. 이 말은 install 이 그대로 화면에 올린다.
48
- if (err?.code === 'ERR_BUFFER_TOO_LARGE') {
49
- // MB 로만 적으면 작은 상한이 「0MB」 가 된다 — 사람에게 아무 말도 안 하는 숫자다.
50
- const 상한말 = 상한 >= 1024 * 1024 ? `${Math.round(상한 / 1024 / 1024)}MB` : `${Math.round(상한 / 1024)}KB`;
51
- throw new Error(`푼 크기가 상한(${상한말})을 넘습니다`
52
- + ' — 압축만 작고 풀면 몇 GB 가 되는 묶음입니다. 저장소가 맞는지 확인하세요.');
53
- }
54
- throw err;
55
- }
56
- const out = [];
57
- let pos = 0;
58
- let longName = null;
59
-
60
- while (pos + BLOCK <= buf.length) {
61
- const head = buf.subarray(pos, pos + BLOCK);
62
- // 빈 블록 두 개면 끝.
63
- if (head.every((b) => b === 0)) break;
64
-
65
- const size = octal(head, 124, 12);
66
- const type = String.fromCharCode(head[156]) || '0';
67
- const mode = octal(head, 100, 8);
68
- const prefix = str(head, 345, 155);
69
- let name = str(head, 0, 100);
70
- if (prefix) name = `${prefix}/${name}`;
71
-
72
- pos += BLOCK;
73
- const data = buf.subarray(pos, pos + size);
74
- pos += Math.ceil(size / BLOCK) * BLOCK;
75
-
76
- if (type === 'L') { // GNU 긴 이름
77
- longName = data.toString('utf8').replace(/\0+$/, '');
78
- continue;
79
- }
80
- if (type === 'x' || type === 'g') { // pax 확장 머리 — path 만 본다
81
- const m = /\d+ path=([^\n]+)\n/.exec(data.toString('utf8'));
82
- if (m) longName = m[1];
83
- continue;
84
- }
85
- if (longName) { name = longName; longName = null; }
86
-
87
- if (type === '0' || type === '\0' || type === '') {
88
- out.push({ name: name.replace(/\\/g, '/'), data: Buffer.from(data), mode: mode || 0o644 });
89
- }
90
- // 폴더('5')·링크 등은 건너뛴다. 파일만 있으면 폴더는 만들면서 채운다.
91
- }
92
- return out;
93
- }
94
-
95
- // GitHub tarball 은 최상위에 <repo>-<커밋> 폴더 한 겹이 더 있다. 그걸 벗긴다.
96
- export function stripTop(files) {
97
- if (!files.length) return files;
98
- const first = files[0].name.split('/')[0];
99
- if (!files.every((f) => f.name.startsWith(first + '/'))) return files;
100
- return files.map((f) => ({ ...f, name: f.name.slice(first.length + 1) }));
101
- }
102
-
103
- /**
104
- * 이 이름이 풀 폴더 **안쪽**을 가리키나.
105
- *
106
- * ── 왜 있어야 하나 ──────────────────────────────────────────────────────
107
- *
108
- * tar 안의 이름은 **남이 적은 글자**다. 우리가 만든 것이 아니다. 그걸 그대로
109
- * join(dest, name) 에 넣으면 `../../../..` 하나로 폴더 밖에 파일을 쓴다.
110
- * 받는 쪽이 mkdirSync(recursive) 까지 해 주므로 없는 폴더도 만들어 가며 나간다.
111
- *
112
- * name: '../../../../윗동네에쓰기.txt'
113
- * dest: C:\Users\누군가\.deel\plugins\x
114
- * → C:\Users\윗동네에쓰기.txt
115
- *
116
- * 이 프로그램이 파는 문장이 「작업 폴더 밖은 안 만진다」 인데, 플러그인을
117
- * 받는 길에는 그 문장을 지키는 코드가 한 줄도 없었다. 여기서 막는다 —
118
- * 푸는 자리마다 따로 적으면 언젠가 한 곳이 빠진다.
119
- *
120
- * 절대경로·드라이브 문자도 같이 걸린다. resolve 는 절대경로를 만나면 뿌리를
121
- * 통째로 갈아 치우므로, 그 결과가 뿌리로 시작하지 않는다.
122
- */
123
- export function 안쪽인가(뿌리, 이름) {
124
- const 원래 = String(이름 ?? '');
125
- /*
126
- * ── 판마다 다르게 막으면 막은 게 아니다 ───────────────────────────────
127
- *
128
- * `resolve()` 는 윈도우에서만 역슬래시를 칸막이로 본다. 그래서
129
- * `..\\..\\밖으로.txt` 는 윈도우에서는 밖으로 잡히고 리눅스·맥에서는
130
- * **그냥 파일 이름 하나**로 통과한다. 같은 묶음이 판에 따라 다르게 처리되는
131
- * 셈이다.
132
- *
133
- * 플러그인 묶음은 기계를 옮겨 다닌다. 리눅스에서 받아 둔 것을 윈도우로
134
- * 옮기거나, 리눅스에서 푼 것을 그대로 다시 묶어 나눠 주는 일이 흔하다.
135
- * 한쪽에서만 막으면 다른 쪽으로 새어 나갈 길이 남는다.
136
- *
137
- * 그래서 역슬래시를 **어느 판에서든** 칸막이로 본다. 이 방향으로만 틀리면
138
- * 손해는 「이름에 역슬래시가 든 파일을 못 푼다」 뿐이고, 그런 이름이 정직한
139
- * 플러그인에 있을 까닭이 없다. 반대 방향으로 틀리면 남의 글자 한 줄로
140
- * 폴더 밖에 파일이 써진다.
141
- */
142
- const 이름꼴 = 원래.replace(/\\/g, '/');
143
- // 드라이브 글자(`C:`)도 마찬가지다. 리눅스에서는 그냥 이름이라 통과하는데,
144
- // 그 묶음을 윈도우에서 풀면 그 자리로 곧장 나간다.
145
- if (/^[a-zA-Z]:/.test(이름꼴)) return false;
146
- const 뿌리절대 = resolve(String(뿌리 ?? ''));
147
- const 갈곳 = resolve(뿌리절대, 이름꼴);
148
- return 갈곳 === 뿌리절대 || 갈곳.startsWith(뿌리절대 + sep);
149
- }
150
-
151
- /** 풀 폴더 밖을 가리키는 것만 골라 준다. 하나라도 있으면 그 묶음은 안 푼다. */
152
- export function 밖을가리키는것(뿌리, files) {
153
- return (files ?? []).filter((f) => !안쪽인가(뿌리, f?.name));
154
- }
1
+ // tar 읽기. GitHub 가 주는 tarball 을 풀기 위한 최소 구현.
2
+ // git 이 없는 기기에서도 플러그인을 받을 수 있어야 해서 필요하다.
3
+ import { gunzipSync } from 'node:zlib';
4
+ import { resolve, sep } from 'node:path';
5
+
6
+ const BLOCK = 512;
7
+
8
+ function str(buf, off, len) {
9
+ const end = buf.indexOf(0, off);
10
+ const stop = end >= 0 && end < off + len ? end : off + len;
11
+ return buf.toString('utf8', off, stop).trim();
12
+ }
13
+
14
+ function octal(buf, off, len) {
15
+ const s = str(buf, off, len).replace(/[^0-7]/g, '');
16
+ return s ? parseInt(s, 8) : 0;
17
+ }
18
+
19
+ /*
20
+ * ── 작게 받아서 크게 푸는 묶음(압축 폭탄) ───────────────────────────────
21
+ *
22
+ * 받는 쪽(plugins/manage.js)은 **압축된** 크기를 64MB 로 막는다. 그런데
23
+ * 0 으로만 채운 파일은 천 배 넘게 줄어든다 — 64MB 짜리 하나가 풀면 수십 GB 다.
24
+ * gunzipSync 는 그걸 통째로 메모리에 올리므로, 화면에는 아무 말도 안 남고
25
+ * 프로세스가 그대로 죽는다. 사용자가 보는 것은 「플러그인을 깔아 줘」 라고
26
+ * 시킨 뒤 deel 이 사라진 것뿐이다 — 하던 대화까지 같이.
27
+ *
28
+ * 그래서 **푼 크기**에도 상한을 둔다. 256MB 로 잡은 까닭은 받는 상한의 네 배라서다.
29
+ * 플러그인은 글과 스크립트 묶음이라 서너 배까지는 부푼다. 그보다 더 부푸는 것은
30
+ * 플러그인이 아니라 폭탄으로 본다.
31
+ */
32
+ export const 푼것상한 = 256 * 1024 * 1024;
33
+
34
+ /**
35
+ * gzip 된 tar 를 풀어 파일 목록을 돌려준다.
36
+ * @param {Buffer} gz
37
+ * @param {object} o
38
+ * @param {number} o.상한 푼 크기 상한. 검사가 작은 값으로 부르려고 열어 뒀다 —
39
+ * 256MB 를 진짜로 만들어 재면 그 검사 하나가 기계를 잡아먹는다.
40
+ * @returns {Array<{name:string, data:Buffer, mode:number}>}
41
+ */
42
+ export function untargz(gz, { 상한 = 푼것상한 } = {}) {
43
+ let buf;
44
+ try {
45
+ buf = gunzipSync(gz, { maxOutputLength: 상한 });
46
+ } catch (err) {
47
+ // 조용히 죽는 대신 왜 안 되는지 말한다. 이 말은 install 이 그대로 화면에 올린다.
48
+ if (err?.code === 'ERR_BUFFER_TOO_LARGE') {
49
+ // MB 로만 적으면 작은 상한이 「0MB」 가 된다 — 사람에게 아무 말도 안 하는 숫자다.
50
+ const 상한말 = 상한 >= 1024 * 1024 ? `${Math.round(상한 / 1024 / 1024)}MB` : `${Math.round(상한 / 1024)}KB`;
51
+ throw new Error(`푼 크기가 상한(${상한말})을 넘습니다`
52
+ + ' — 압축만 작고 풀면 몇 GB 가 되는 묶음입니다. 저장소가 맞는지 확인하세요.');
53
+ }
54
+ throw err;
55
+ }
56
+ const out = [];
57
+ let pos = 0;
58
+ let longName = null;
59
+
60
+ while (pos + BLOCK <= buf.length) {
61
+ const head = buf.subarray(pos, pos + BLOCK);
62
+ // 빈 블록 두 개면 끝.
63
+ if (head.every((b) => b === 0)) break;
64
+
65
+ const size = octal(head, 124, 12);
66
+ const type = String.fromCharCode(head[156]) || '0';
67
+ const mode = octal(head, 100, 8);
68
+ const prefix = str(head, 345, 155);
69
+ let name = str(head, 0, 100);
70
+ if (prefix) name = `${prefix}/${name}`;
71
+
72
+ pos += BLOCK;
73
+ const data = buf.subarray(pos, pos + size);
74
+ pos += Math.ceil(size / BLOCK) * BLOCK;
75
+
76
+ if (type === 'L') { // GNU 긴 이름
77
+ longName = data.toString('utf8').replace(/\0+$/, '');
78
+ continue;
79
+ }
80
+ if (type === 'x' || type === 'g') { // pax 확장 머리 — path 만 본다
81
+ const m = /\d+ path=([^\n]+)\n/.exec(data.toString('utf8'));
82
+ if (m) longName = m[1];
83
+ continue;
84
+ }
85
+ if (longName) { name = longName; longName = null; }
86
+
87
+ if (type === '0' || type === '\0' || type === '') {
88
+ out.push({ name: name.replace(/\\/g, '/'), data: Buffer.from(data), mode: mode || 0o644 });
89
+ }
90
+ // 폴더('5')·링크 등은 건너뛴다. 파일만 있으면 폴더는 만들면서 채운다.
91
+ }
92
+ return out;
93
+ }
94
+
95
+ // GitHub tarball 은 최상위에 <repo>-<커밋> 폴더 한 겹이 더 있다. 그걸 벗긴다.
96
+ export function stripTop(files) {
97
+ if (!files.length) return files;
98
+ const first = files[0].name.split('/')[0];
99
+ if (!files.every((f) => f.name.startsWith(first + '/'))) return files;
100
+ return files.map((f) => ({ ...f, name: f.name.slice(first.length + 1) }));
101
+ }
102
+
103
+ /**
104
+ * 이 이름이 풀 폴더 **안쪽**을 가리키나.
105
+ *
106
+ * ── 왜 있어야 하나 ──────────────────────────────────────────────────────
107
+ *
108
+ * tar 안의 이름은 **남이 적은 글자**다. 우리가 만든 것이 아니다. 그걸 그대로
109
+ * join(dest, name) 에 넣으면 `../../../..` 하나로 폴더 밖에 파일을 쓴다.
110
+ * 받는 쪽이 mkdirSync(recursive) 까지 해 주므로 없는 폴더도 만들어 가며 나간다.
111
+ *
112
+ * name: '../../../../윗동네에쓰기.txt'
113
+ * dest: C:\Users\누군가\.deel\plugins\x
114
+ * → C:\Users\윗동네에쓰기.txt
115
+ *
116
+ * 이 프로그램이 파는 문장이 「작업 폴더 밖은 안 만진다」 인데, 플러그인을
117
+ * 받는 길에는 그 문장을 지키는 코드가 한 줄도 없었다. 여기서 막는다 —
118
+ * 푸는 자리마다 따로 적으면 언젠가 한 곳이 빠진다.
119
+ *
120
+ * 절대경로·드라이브 문자도 같이 걸린다. resolve 는 절대경로를 만나면 뿌리를
121
+ * 통째로 갈아 치우므로, 그 결과가 뿌리로 시작하지 않는다.
122
+ */
123
+ export function 안쪽인가(뿌리, 이름) {
124
+ const 원래 = String(이름 ?? '');
125
+ /*
126
+ * ── 판마다 다르게 막으면 막은 게 아니다 ───────────────────────────────
127
+ *
128
+ * `resolve()` 는 윈도우에서만 역슬래시를 칸막이로 본다. 그래서
129
+ * `..\\..\\밖으로.txt` 는 윈도우에서는 밖으로 잡히고 리눅스·맥에서는
130
+ * **그냥 파일 이름 하나**로 통과한다. 같은 묶음이 판에 따라 다르게 처리되는
131
+ * 셈이다.
132
+ *
133
+ * 플러그인 묶음은 기계를 옮겨 다닌다. 리눅스에서 받아 둔 것을 윈도우로
134
+ * 옮기거나, 리눅스에서 푼 것을 그대로 다시 묶어 나눠 주는 일이 흔하다.
135
+ * 한쪽에서만 막으면 다른 쪽으로 새어 나갈 길이 남는다.
136
+ *
137
+ * 그래서 역슬래시를 **어느 판에서든** 칸막이로 본다. 이 방향으로만 틀리면
138
+ * 손해는 「이름에 역슬래시가 든 파일을 못 푼다」 뿐이고, 그런 이름이 정직한
139
+ * 플러그인에 있을 까닭이 없다. 반대 방향으로 틀리면 남의 글자 한 줄로
140
+ * 폴더 밖에 파일이 써진다.
141
+ */
142
+ const 이름꼴 = 원래.replace(/\\/g, '/');
143
+ // 드라이브 글자(`C:`)도 마찬가지다. 리눅스에서는 그냥 이름이라 통과하는데,
144
+ // 그 묶음을 윈도우에서 풀면 그 자리로 곧장 나간다.
145
+ if (/^[a-zA-Z]:/.test(이름꼴)) return false;
146
+ const 뿌리절대 = resolve(String(뿌리 ?? ''));
147
+ const 갈곳 = resolve(뿌리절대, 이름꼴);
148
+ return 갈곳 === 뿌리절대 || 갈곳.startsWith(뿌리절대 + sep);
149
+ }
150
+
151
+ /** 풀 폴더 밖을 가리키는 것만 골라 준다. 하나라도 있으면 그 묶음은 안 푼다. */
152
+ export function 밖을가리키는것(뿌리, files) {
153
+ return (files ?? []).filter((f) => !안쪽인가(뿌리, f?.name));
154
+ }