deel-local-cli 1.8.0 → 1.10.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 (49) hide show
  1. package/README.ko.md +1166 -1182
  2. package/README.md +1223 -1232
  3. package/bin/deel.js +55 -12
  4. package/package.json +3 -3
  5. package/src/acp/serve.js +40 -3
  6. package/src/agent/compact.js +314 -296
  7. package/src/agent/effort.js +27 -7
  8. package/src/agent/evidence.js +2 -0
  9. package/src/agent/filemem.js +141 -0
  10. package/src/agent/grade.js +51 -1
  11. package/src/agent/loop.js +202 -35
  12. package/src/agent/session.js +121 -5
  13. package/src/agent/store.js +186 -9
  14. package/src/agent/threads.js +26 -1
  15. package/src/backend/adapter.js +201 -18
  16. package/src/backend/learn.js +24 -0
  17. package/src/backend/mcp.js +96 -5
  18. package/src/backend/probe.js +131 -61
  19. package/src/backend/quota.js +29 -5
  20. package/src/backend/toolfit.js +325 -0
  21. package/src/commands.js +107 -27
  22. package/src/config.js +47 -2
  23. package/src/i18n/en.js +56 -2
  24. package/src/i18n/index.js +18 -0
  25. package/src/i18n/ja.js +58 -2
  26. package/src/i18n/ko.js +103 -2
  27. package/src/i18n/zh.js +58 -2
  28. package/src/lsp/client.js +49 -5
  29. package/src/oneshot.js +81 -3
  30. package/src/pack/sbom.js +30 -4
  31. package/src/pack/selfpack.js +25 -9
  32. package/src/pack/sheet.en.js +288 -0
  33. package/src/pack/tar.js +65 -2
  34. package/src/plugins/manage.js +46 -9
  35. package/src/repl.js +264 -35
  36. package/src/safety/audit.js +92 -8
  37. package/src/safety/authcmd.js +14 -3
  38. package/src/safety/guard.js +143 -0
  39. package/src/safety/keystore.js +62 -39
  40. package/src/safety/undo.js +24 -6
  41. package/src/tools/index.js +239 -34
  42. package/src/tools/jobs.js +158 -29
  43. package/src/tools/verify.js +33 -4
  44. package/src/tools/webfetch.js +92 -9
  45. package/src/ui/md.js +201 -5
  46. package/src/ui/motion.js +0 -1
  47. package/src/ui/pastechip.js +50 -3
  48. package/src/ui/pick.js +115 -0
  49. package/src/ui/screen.js +23 -3
package/src/pack/tar.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // tar 읽기. GitHub 가 주는 tarball 을 풀기 위한 최소 구현.
2
2
  // git 이 없는 기기에서도 플러그인을 받을 수 있어야 해서 필요하다.
3
3
  import { gunzipSync } from 'node:zlib';
4
+ import { resolve, sep } from 'node:path';
4
5
 
5
6
  const BLOCK = 512;
6
7
 
@@ -15,12 +16,43 @@ function octal(buf, off, len) {
15
16
  return s ? parseInt(s, 8) : 0;
16
17
  }
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
+
18
34
  /**
19
35
  * gzip 된 tar 를 풀어 파일 목록을 돌려준다.
36
+ * @param {Buffer} gz
37
+ * @param {object} o
38
+ * @param {number} o.상한 푼 크기 상한. 검사가 작은 값으로 부르려고 열어 뒀다 —
39
+ * 256MB 를 진짜로 만들어 재면 그 검사 하나가 기계를 잡아먹는다.
20
40
  * @returns {Array<{name:string, data:Buffer, mode:number}>}
21
41
  */
22
- export function untargz(gz) {
23
- const buf = gunzipSync(gz);
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
+ }
24
56
  const out = [];
25
57
  let pos = 0;
26
58
  let longName = null;
@@ -67,3 +99,34 @@ export function stripTop(files) {
67
99
  if (!files.every((f) => f.name.startsWith(first + '/'))) return files;
68
100
  return files.map((f) => ({ ...f, name: f.name.slice(first.length + 1) }));
69
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 뿌리절대 = resolve(String(뿌리 ?? ''));
125
+ const 갈곳 = resolve(뿌리절대, String(이름 ?? ''));
126
+ return 갈곳 === 뿌리절대 || 갈곳.startsWith(뿌리절대 + sep);
127
+ }
128
+
129
+ /** 풀 폴더 밖을 가리키는 것만 골라 준다. 하나라도 있으면 그 묶음은 안 푼다. */
130
+ export function 밖을가리키는것(뿌리, files) {
131
+ return (files ?? []).filter((f) => !안쪽인가(뿌리, f?.name));
132
+ }
@@ -9,7 +9,7 @@ import {
9
9
  existsSync, mkdirSync, writeFileSync, readFileSync, rmSync,
10
10
  readdirSync, statSync,
11
11
  } from 'node:fs';
12
- import { untargz, stripTop } from '../pack/tar.js';
12
+ import { untargz, stripTop, 밖을가리키는것 } from '../pack/tar.js';
13
13
  import { makeZip } from '../pack/zip.js';
14
14
  import { allowTemporarily, isOffline, NetBlocked } from '../safety/network.js';
15
15
  import { 원시요청, 몸읽기 } from '../backend/http.js';
@@ -54,6 +54,45 @@ function run(cmd, args, opts = {}) {
54
54
  });
55
55
  }
56
56
 
57
+ /**
58
+ * 받은 묶음을 폴더에 푼다.
59
+ *
60
+ * ── 왜 따로 떼어 놨나 ───────────────────────────────────────────────────
61
+ *
62
+ * 이 몇 줄이 **남이 준 글자로 디스크에 파일을 쓰는 자리**다. 이 프로그램에서
63
+ * 제일 조심해야 하는 대목인데, fetchInto 안에 묻혀 있어서 검사가 한 번도 안
64
+ * 지나갔다. 거기까지 가려면 진짜로 네트워크에서 tarball 을 받아야 했기 때문이다.
65
+ *
66
+ * 그래서 「받는 일」 과 「푸는 일」 을 갈랐다. 푸는 일은 이제 파일 목록만 주면
67
+ * 그대로 재 볼 수 있다 — 남이 노리고 만든 묶음을 먹여 보는 것을 포함해서.
68
+ *
69
+ * ── 무엇을 막나 ─────────────────────────────────────────────────────────
70
+ *
71
+ * tar 안의 이름은 남이 적은 글자다. `../../../..` 하나면 플러그인 폴더 밖에
72
+ * 파일을 쓴다. 아래 mkdirSync 가 recursive 라 없는 폴더까지 만들어 가며 나간다.
73
+ * 이 프로그램이 파는 문장이 「작업 폴더 밖은 안 만진다」 인데, 플러그인을 받는
74
+ * 이 길에만 그 문장을 지키는 코드가 없었다.
75
+ *
76
+ * 나쁜 것만 골라 버리지 않고 **묶음째 거절한다.** 밖을 가리키는 이름이 든
77
+ * 묶음은 실수가 아니라 노린 것이고, 나머지를 풀어 줄 까닭이 없다.
78
+ * 그리고 **rmSync 앞에서** 막는다 — 거절할 묶음 때문에 이미 깔린 것을
79
+ * 지워 버리면 안 된다.
80
+ */
81
+ export function 묶음풀기(dest, files) {
82
+ if (!files?.length) return { error: '받은 묶음이 비어 있습니다' };
83
+ const 밖엣것 = 밖을가리키는것(dest, files);
84
+ if (밖엣것.length) {
85
+ return { error: `묶음 안에 플러그인 폴더 밖을 가리키는 이름이 있습니다 — 풀지 않았습니다: ${밖엣것[0].name}` };
86
+ }
87
+ rmSync(dest, { recursive: true, force: true });
88
+ for (const f of files) {
89
+ const p = join(dest, f.name);
90
+ mkdirSync(dirname(p), { recursive: true });
91
+ writeFileSync(p, f.data);
92
+ }
93
+ return { 푼것: files.length };
94
+ }
95
+
57
96
  // 받는 방법 두 가지. git 이 있으면 clone, 없으면 tarball 을 내려받아 푼다.
58
97
  async function fetchInto(spec, dest, onStep) {
59
98
  if (await has('git')) {
@@ -96,14 +135,12 @@ async function fetchInto(spec, dest, onStep) {
96
135
  for (const 닫기 of 열어둔) 닫기();
97
136
  }
98
137
  if (!gz) return { error: '받은 묶음이 너무 큽니다 (64MB 넘음) — 플러그인 저장소가 맞는지 확인하세요' };
99
- const files = stripTop(untargz(gz));
100
- if (!files.length) return { error: '받은 묶음이 비어 있습니다' };
101
- rmSync(dest, { recursive: true, force: true });
102
- for (const f of files) {
103
- const p = join(dest, f.name);
104
- mkdirSync(dirname(p), { recursive: true });
105
- writeFileSync(p, f.data);
106
- }
138
+ // 받는 크기만 막으면 압축 폭탄에 그대로 당한다 (pack/tar.js 푼것상한 머리말).
139
+ // 던지는 것을 여기서 받아 화면에 올린다 받으면 명령이 통째로 죽는다.
140
+ let 푼것;
141
+ try { 푼것 = 묶음풀기(dest, stripTop(untargz(gz))); }
142
+ catch (err) { return { error: `받은 묶음을 풀지 못했습니다 — ${String(err?.message ?? err)}` }; }
143
+ if (푼것.error) return { error: 푼것.error };
107
144
  return { how: 'tarball', branch };
108
145
  }
109
146
  return { error: '받지 못했습니다 — 저장소 주소나 가지 이름을 확인하세요' };