deel-local-cli 1.8.0 → 1.12.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.
- package/README.ko.md +10 -25
- package/README.md +43 -51
- package/bin/deel.js +56 -13
- package/package.json +60 -60
- package/src/acp/serve.js +810 -732
- package/src/agent/budget.js +156 -153
- package/src/agent/compact.js +420 -296
- package/src/agent/effort.js +308 -177
- package/src/agent/evidence.js +2 -0
- package/src/agent/evolve.js +263 -213
- package/src/agent/filemem.js +155 -0
- package/src/agent/grade.js +51 -1
- package/src/agent/loop.js +1707 -1349
- package/src/agent/memory.js +156 -152
- package/src/agent/mention.js +210 -210
- package/src/agent/session.js +1049 -832
- package/src/agent/store.js +186 -9
- package/src/agent/threads.js +26 -1
- package/src/backend/adapter.js +1159 -615
- package/src/backend/cachemark.js +150 -0
- package/src/backend/detect.js +327 -288
- package/src/backend/learn.js +24 -0
- package/src/backend/mcp.js +96 -5
- package/src/backend/probe.js +131 -61
- package/src/backend/quota.js +250 -133
- package/src/backend/retry.js +30 -6
- package/src/backend/tokens.js +37 -0
- package/src/backend/toolfit.js +352 -0
- package/src/backend/wire.js +600 -0
- package/src/commands.js +3135 -2918
- package/src/config.js +47 -2
- package/src/i18n/en.js +544 -466
- package/src/i18n/index.js +18 -0
- package/src/i18n/ja.js +497 -417
- package/src/i18n/ko.js +591 -466
- package/src/i18n/zh.js +497 -417
- package/src/lsp/client.js +49 -5
- package/src/oneshot.js +620 -471
- package/src/pack/sbom.js +30 -4
- package/src/pack/selfpack.js +25 -9
- package/src/pack/sheet.en.js +288 -0
- package/src/pack/tar.js +65 -2
- package/src/plugins/manage.js +46 -9
- package/src/providers/bedrock.js +17 -0
- package/src/repl.js +2602 -2286
- package/src/safety/audit.js +92 -8
- package/src/safety/authcmd.js +14 -3
- package/src/safety/guard.js +143 -0
- package/src/safety/keystore.js +62 -39
- package/src/safety/undo.js +24 -6
- package/src/tools/fsutil.js +265 -250
- package/src/tools/index.js +239 -34
- package/src/tools/jobs.js +158 -29
- package/src/tools/verify.js +358 -328
- package/src/tools/webfetch.js +92 -9
- package/src/ui/md.js +201 -5
- package/src/ui/motion.js +0 -1
- package/src/ui/pastechip.js +50 -3
- package/src/ui/pick.js +115 -0
- package/src/ui/screen.js +23 -3
- package/src/ui/status.js +613 -610
package/src/tools/verify.js
CHANGED
|
@@ -1,328 +1,358 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verify — 만든 것이 **실제로 되는지** 본다.
|
|
3
|
-
*
|
|
4
|
-
* ── 왜 이게 필요한가 ───────────────────────────────────────────────────
|
|
5
|
-
*
|
|
6
|
-
* 턴 끝에 이렇게 뜬다.
|
|
7
|
-
* ✓ index.html · 410줄 · 18.2KB
|
|
8
|
-
*
|
|
9
|
-
* 이건 파일이 **있다**는 증명이지 **된다**는 증명이 아니다. `<div>` 를 안 닫아
|
|
10
|
-
* 놨어도, `src="app.js"` 가 없는 파일을 가리켜도, JS 에 괄호가 하나 모자라도
|
|
11
|
-
* 똑같이 초록으로 뜬다. 사람은 그 초록을 믿고 다음 일로 넘어간다.
|
|
12
|
-
*
|
|
13
|
-
* 프롬프트에 "다 했으면 확인한다" 는 이미 넣어 뒀다. 그런데 확인할 **길**을
|
|
14
|
-
* 안 줬다. 그러면 그건 부탁이지 규칙이 아니다.
|
|
15
|
-
*
|
|
16
|
-
* ── 무엇을 확인하나 ────────────────────────────────────────────────────
|
|
17
|
-
*
|
|
18
|
-
* 돌려 볼 수 있는 것은 돌려 본다 (node --check · py_compile).
|
|
19
|
-
* 못 돌리는 것은 **읽어서** 본다 (HTML 태그 짝, 빠진 참조, CSS 중괄호
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.replace(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
줄.push(
|
|
291
|
-
줄.push(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Verify — 만든 것이 **실제로 되는지** 본다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이게 필요한가 ───────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 턴 끝에 이렇게 뜬다.
|
|
7
|
+
* ✓ index.html · 410줄 · 18.2KB
|
|
8
|
+
*
|
|
9
|
+
* 이건 파일이 **있다**는 증명이지 **된다**는 증명이 아니다. `<div>` 를 안 닫아
|
|
10
|
+
* 놨어도, `src="app.js"` 가 없는 파일을 가리켜도, JS 에 괄호가 하나 모자라도
|
|
11
|
+
* 똑같이 초록으로 뜬다. 사람은 그 초록을 믿고 다음 일로 넘어간다.
|
|
12
|
+
*
|
|
13
|
+
* 프롬프트에 "다 했으면 확인한다" 는 이미 넣어 뒀다. 그런데 확인할 **길**을
|
|
14
|
+
* 안 줬다. 그러면 그건 부탁이지 규칙이 아니다.
|
|
15
|
+
*
|
|
16
|
+
* ── 무엇을 확인하나 ────────────────────────────────────────────────────
|
|
17
|
+
*
|
|
18
|
+
* 돌려 볼 수 있는 것은 돌려 본다 (node --check · py_compile).
|
|
19
|
+
* 못 돌리는 것은 **읽어서** 본다 (HTML 태그 짝, 빠진 참조, CSS 중괄호 짝,
|
|
20
|
+
* JSON 파싱).
|
|
21
|
+
*
|
|
22
|
+
* 임의의 명령은 **여기서 안 돌린다.** 검사 스크립트는 무슨 짓이든 할 수 있어서,
|
|
23
|
+
* 그걸 돌리는 길은 Bash 하나여야 한다 — 승인 관문과 안전 검사가 거기에만 있다.
|
|
24
|
+
* 이 도구가 몰래 돌리면 strict 모드의 약속이 이 자리에서만 깨진다.
|
|
25
|
+
* 여기서는 "이 프로젝트엔 npm test 가 있다" 고 알려 주기만 한다.
|
|
26
|
+
*
|
|
27
|
+
* 그리고 제일 중요한 것 — **못 확인한 것은 못 확인했다고 말한다.**
|
|
28
|
+
* 조용히 넘기면 사람이 받는 신호는 '확인함' 과 구별되지 않는다. 그럴 바에는
|
|
29
|
+
* 이 도구가 없는 편이 낫다. 확인 못 한 것을 확인했다고 하는 것이 제일 나쁘다.
|
|
30
|
+
*/
|
|
31
|
+
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
32
|
+
import { execFile } from 'node:child_process';
|
|
33
|
+
import { extname, dirname, join, resolve } from 'node:path';
|
|
34
|
+
import { walk, SKIP_DIRS, 내부살림 } from './fsutil.js';
|
|
35
|
+
import { 건너뜀말 } from './ignore.js';
|
|
36
|
+
import { decode, looksBinary } from './encoding.js';
|
|
37
|
+
import { checkCommand } from '../safety/guard.js';
|
|
38
|
+
import { 말 } from '../i18n/index.js';
|
|
39
|
+
|
|
40
|
+
/* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
|
|
41
|
+
const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
|
|
42
|
+
|
|
43
|
+
/** 짝이 맞아야 하는 HTML 태그. 안 닫아도 되는 것(void)은 뺀다. */
|
|
44
|
+
const 안닫아도되는것 = new Set([
|
|
45
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link',
|
|
46
|
+
'meta', 'param', 'source', 'track', 'wbr', '!doctype', '!--',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* HTML 을 읽어 본다.
|
|
51
|
+
*
|
|
52
|
+
* 파서를 쓰지 않는다(의존성 0개). 대신 **틀렸다고 확신할 수 있는 것만** 잡는다 —
|
|
53
|
+
* 애매한 것은 넘긴다. 잘못된 경고를 내면 모델이 멀쩡한 파일을 고치기 시작하고,
|
|
54
|
+
* 그게 확인 안 하는 것보다 나쁘다.
|
|
55
|
+
*/
|
|
56
|
+
export function html보기(글, { 있는파일 = () => true } = {}) {
|
|
57
|
+
const 탈 = [];
|
|
58
|
+
const 쌓임 = [];
|
|
59
|
+
|
|
60
|
+
// 주석과 script/style 안엣것은 빼고 본다 — 그 안의 `<` 는 태그가 아니다.
|
|
61
|
+
const 뼈 = String(글)
|
|
62
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
63
|
+
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, '<$1></$1>');
|
|
64
|
+
|
|
65
|
+
for (const m of 뼈.matchAll(/<(\/?)([a-zA-Z!][a-zA-Z0-9-]*)([^>]*)>/g)) {
|
|
66
|
+
const 닫는가 = m[1] === '/';
|
|
67
|
+
const 이름 = m[2].toLowerCase();
|
|
68
|
+
const 뒤 = m[3] ?? '';
|
|
69
|
+
if (안닫아도되는것.has(이름) || 뒤.trimEnd().endsWith('/')) continue;
|
|
70
|
+
if (!닫는가) { 쌓임.push(이름); continue; }
|
|
71
|
+
// 닫는 태그. 짝이 맞는 자리를 찾는다.
|
|
72
|
+
const i = 쌓임.lastIndexOf(이름);
|
|
73
|
+
if (i < 0) { 탈.push(`</${이름}> 를 닫는데 여는 <${이름}> 가 없습니다`); continue; }
|
|
74
|
+
const 안닫힌것 = 쌓임.splice(i).slice(1);
|
|
75
|
+
for (const x of 안닫힌것) 탈.push(`<${x}> 를 안 닫았습니다 (</${이름}> 앞에서 끊겼습니다)`);
|
|
76
|
+
}
|
|
77
|
+
for (const x of 쌓임) 탈.push(`<${x}> 를 안 닫았습니다`);
|
|
78
|
+
|
|
79
|
+
// 가리키는 파일이 실제로 있나. 이게 "열었는데 아무것도 안 보인다" 의 첫째 원인이다.
|
|
80
|
+
for (const m of String(글).matchAll(/(?:src|href)\s*=\s*"([^"]+)"/g)) {
|
|
81
|
+
const 곳 = m[1];
|
|
82
|
+
if (/^(?:https?:|data:|mailto:|tel:|#|\/\/)/i.test(곳)) continue; // 바깥 것은 못 본다
|
|
83
|
+
if (!있는파일(곳.split(/[?#]/)[0])) 탈.push(`${곳} 을 가리키는데 그 파일이 없습니다`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return 탈;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** CSS 중괄호 짝. 이것만으로도 '스타일이 통째로 안 먹는' 경우는 거의 다 잡힌다. */
|
|
90
|
+
export function css보기(글) {
|
|
91
|
+
const 뼈 = String(글).replace(/\/\*[\s\S]*?\*\//g, '');
|
|
92
|
+
const 연것 = (뼈.match(/\{/g) ?? []).length;
|
|
93
|
+
const 닫은것 = (뼈.match(/\}/g) ?? []).length;
|
|
94
|
+
if (연것 === 닫은것) return [];
|
|
95
|
+
return [연것 > 닫은것
|
|
96
|
+
? `중괄호를 ${연것 - 닫은것}개 안 닫았습니다 (여는 것 ${연것} · 닫는 것 ${닫은것})`
|
|
97
|
+
: `닫는 중괄호가 ${닫은것 - 연것}개 더 많습니다 (여는 것 ${연것} · 닫는 것 ${닫은것})`];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** JSON 은 그냥 파싱해 본다 — 되면 되는 것이다. */
|
|
101
|
+
export function json보기(글) {
|
|
102
|
+
try { JSON.parse(글); return []; }
|
|
103
|
+
catch (e) { return [String(e.message)]; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 명령 하나를 돌린다. Bash 도구와 같은 안전 검사를 거친다. */
|
|
107
|
+
function 명령돌리기(cmd, cwd, 제한 = 60000) {
|
|
108
|
+
return new Promise((끝) => {
|
|
109
|
+
const shell = process.platform === 'win32'
|
|
110
|
+
? { file: process.env.COMSPEC ?? 'cmd.exe', args: ['/d', '/s', '/c', `"${cmd}"`], verbatim: true }
|
|
111
|
+
: { file: '/bin/sh', args: ['-c', cmd] };
|
|
112
|
+
execFile(shell.file, shell.args, {
|
|
113
|
+
cwd, timeout: 제한, maxBuffer: 4 * 1024 * 1024,
|
|
114
|
+
windowsHide: true, windowsVerbatimArguments: shell.verbatim === true,
|
|
115
|
+
encoding: 'buffer',
|
|
116
|
+
}, (err, so, se) => {
|
|
117
|
+
const 풀기 = (b) => (b && b.length ? decode(Buffer.from(b)).text : '');
|
|
118
|
+
const out = [풀기(so), 풀기(se)].filter(Boolean).join('\n').trim();
|
|
119
|
+
const 시그널 = err?.signal ?? null;
|
|
120
|
+
const code = 시그널 ? null : (err?.code ?? 0);
|
|
121
|
+
끝({ ok: !시그널 && code === 0, code, 시그널, out });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 폴더를 통째로 볼 때 한 번에 보는 파일 수.
|
|
128
|
+
*
|
|
129
|
+
* 상한이 있는 것 자체는 옳다 — 저장소가 크면 한 번의 Verify 가 몇 분이 된다.
|
|
130
|
+
* 다만 **넘친 것을 말해야** 한다. 안 말하면 마흔한 번째 파일이 깨져 있어도
|
|
131
|
+
* 「확인했습니다」 로 끝난다. 이 파일이 있는 까닭과 정면으로 어긋난다.
|
|
132
|
+
*/
|
|
133
|
+
const 한번에최대 = 40;
|
|
134
|
+
|
|
135
|
+
/** 이 확장자를 돌려 볼 명령이 있나. 없으면 null — 그러면 '못 돌려 봤다' 고 말한다. */
|
|
136
|
+
function 돌릴명령(확장) {
|
|
137
|
+
switch (확장) {
|
|
138
|
+
case '.js': case '.mjs': case '.cjs': return (p) => `node --check "${p}"`;
|
|
139
|
+
case '.py': return (p) => `python -m py_compile "${p}"`;
|
|
140
|
+
default: return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const 짧게 = (s, n = 300) => {
|
|
145
|
+
const t = String(s ?? '').trim();
|
|
146
|
+
return t.length > n ? `${t.slice(0, n)}…` : t;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const VERIFY_TOOL = {
|
|
150
|
+
schema: {
|
|
151
|
+
name: 'Verify',
|
|
152
|
+
description:
|
|
153
|
+
'만든 것이 실제로 되는지 확인한다. 일을 끝내기 전에 **반드시** 한 번 불러라.'
|
|
154
|
+
+ ' 파일이 있다는 것과 그 파일이 된다는 것은 다르다 — 안 닫힌 태그, 없는 파일을'
|
|
155
|
+
+ ' 가리키는 src, 괄호 하나 모자란 JS 는 파일 목록만 봐서는 안 보인다.'
|
|
156
|
+
+ ' 돌려 볼 수 있는 것은 돌려 보고(node --check · py_compile),'
|
|
157
|
+
+ ' 못 돌리는 것은 읽어서 본다(HTML 태그 짝 · 빠진 참조 · CSS 중괄호 · JSON).'
|
|
158
|
+
+ ' 확인 못 한 종류는 못 했다고 그대로 말해 준다.'
|
|
159
|
+
+ ' 검사·빌드를 돌리는 것은 Bash 로 해라 — 그건 사용자 승인을 거쳐야 하는 일이다.',
|
|
160
|
+
parameters: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
paths: {
|
|
164
|
+
type: 'array',
|
|
165
|
+
items: { type: 'string' },
|
|
166
|
+
description: '확인할 파일들. 없으면 작업 폴더에서 확인할 수 있는 것을 다 본다.',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
required: [],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async run(args, ctx) {
|
|
174
|
+
const 뿌리 = ctx.scope.root;
|
|
175
|
+
|
|
176
|
+
// 볼 파일 고르기. 안 주면 작업 폴더에서 확인할 수 있는 것을 찾는다.
|
|
177
|
+
let 볼것 = [];
|
|
178
|
+
// 상한에 걸려 안 본 것의 수. 0 이 아니면 아래에서 못한것 에 넣는다.
|
|
179
|
+
let 넘친수 = 0;
|
|
180
|
+
// .gitignore 로 안 본 것은 셈해 뒀다가 끝에 적는다. 조용히 빼면
|
|
181
|
+
// "빌드 산출물에 탈이 있는데 왜 확인이 통과냐" 를 사람이 못 푼다.
|
|
182
|
+
const 건너뜀 = { 폴더: 0, 파일: 0 };
|
|
183
|
+
// 훑기 상한에서 멈춘 것도 같이 챙긴다 — 안 본 것을 "탈 없음" 으로 읽히게 두면 안 된다.
|
|
184
|
+
let 잘림 = false;
|
|
185
|
+
let 상한 = 0;
|
|
186
|
+
const 셈더하기 = (목록) => {
|
|
187
|
+
건너뜀.폴더 += 목록.건너뜀?.폴더 ?? 0;
|
|
188
|
+
건너뜀.파일 += 목록.건너뜀?.파일 ?? 0;
|
|
189
|
+
if (목록.잘림) { 잘림 = true; 상한 = 목록.상한 ?? 상한; }
|
|
190
|
+
};
|
|
191
|
+
if (Array.isArray(args.paths) && args.paths.length) {
|
|
192
|
+
for (const p of args.paths) {
|
|
193
|
+
let abs;
|
|
194
|
+
try { abs = ctx.scope.resolve(p); } catch (e) { 볼것.push({ 없음: p, 왜: e.message }); continue; }
|
|
195
|
+
if (!existsSync(abs)) { 볼것.push({ 없음: p, 왜: '파일이 없습니다' }); continue; }
|
|
196
|
+
if (statSync(abs).isDirectory()) {
|
|
197
|
+
const 안것 = walk(abs, { skipDirs: SKIP_DIRS });
|
|
198
|
+
셈더하기(안것);
|
|
199
|
+
for (const f of 안것) 볼것.push({ path: f.path });
|
|
200
|
+
} else 볼것.push({ path: abs }); // 짚어 준 파일은 규칙과 상관없이 본다
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
const 전부 = walk(뿌리, { skipDirs: SKIP_DIRS });
|
|
204
|
+
셈더하기(전부);
|
|
205
|
+
const 볼만한것 = 전부
|
|
206
|
+
.filter((f) => ['.html', '.htm', '.css', '.json', '.js', '.mjs', '.cjs', '.py'].includes(extname(f.path).toLowerCase()));
|
|
207
|
+
볼것 = 볼만한것.slice(0, 한번에최대).map((f) => ({ path: f.path }));
|
|
208
|
+
/*
|
|
209
|
+
* 상한에 걸려 안 본 것은 **말한다.**
|
|
210
|
+
*
|
|
211
|
+
* 이 파일 머리말이 스스로 못 박아 둔 것이 있다 — 「못 확인한 것은 못
|
|
212
|
+
* 확인했다고 말한다. 확인 못 한 것을 확인했다고 하는 것이 제일 나쁘다.」
|
|
213
|
+
* .gitignore 로 건너뛴 것과 walk 상한은 그렇게 하고 있었는데, **이
|
|
214
|
+
* 상한만 아무 데도 안 적혔다.**
|
|
215
|
+
*
|
|
216
|
+
* 그래서 파일이 마흔 개를 넘으면, 마흔한 번째부터는 깨져 있어도
|
|
217
|
+
* 「확인했습니다」 로 끝났다. failed 도 거짓이라 루프까지 성공으로
|
|
218
|
+
* 넘어간다. 상한 자체보다 **말을 안 한 것**이 탈이다.
|
|
219
|
+
*/
|
|
220
|
+
넘친수 = 볼만한것.length - 볼것.length;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const 된것 = [];
|
|
224
|
+
const 탈난것 = [];
|
|
225
|
+
const 못한것 = [];
|
|
226
|
+
if (넘친수 > 0) {
|
|
227
|
+
못한것.push({
|
|
228
|
+
이름: `그 밖의 ${넘친수}개`,
|
|
229
|
+
왜: `한 번에 ${한번에최대}개까지만 봅니다 — 나머지는 paths 로 짚어서 다시 부르세요`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const x of 볼것) {
|
|
234
|
+
if (x.없음) { 탈난것.push({ 이름: x.없음, 탈: [x.왜] }); continue; }
|
|
235
|
+
const abs = x.path;
|
|
236
|
+
const 이름 = ctx.scope.show(abs);
|
|
237
|
+
const 확장 = extname(abs).toLowerCase();
|
|
238
|
+
|
|
239
|
+
if (내부살림(abs)) continue;
|
|
240
|
+
let buf;
|
|
241
|
+
try { buf = readFileSync(abs); } catch (e) { 탈난것.push({ 이름, 탈: [e.message] }); continue; }
|
|
242
|
+
if (looksBinary(buf)) { 못한것.push({ 이름, 왜: '바이너리 — 돌려 볼 수 없습니다' }); continue; }
|
|
243
|
+
const 글 = decode(buf).text;
|
|
244
|
+
|
|
245
|
+
// ── 돌려 볼 수 있는 것 ──────────────────────────────────────────
|
|
246
|
+
const 만들기 = 돌릴명령(확장);
|
|
247
|
+
if (만들기) {
|
|
248
|
+
const cmd = 만들기(abs);
|
|
249
|
+
try { checkCommand(cmd); }
|
|
250
|
+
catch { 못한것.push({ 이름, 왜: '안전 검사에 걸려 안 돌렸습니다' }); continue; }
|
|
251
|
+
const r = await 명령돌리기(cmd, 뿌리, 30000);
|
|
252
|
+
// 도구 자체가 이 컴퓨터에 없으면 '틀렸다' 가 아니라 '못 봤다' 이다.
|
|
253
|
+
// 파이썬이 안 깔린 PC 에서 py 파일을 전부 빨갛게 칠하면 아무도 안 믿는다.
|
|
254
|
+
if (!r.ok && /not recognized|command not found|찾을 수 없|No such file/i.test(r.out)) {
|
|
255
|
+
못한것.push({ 이름, 왜: `${확장} 을 확인할 도구가 이 컴퓨터에 없습니다` });
|
|
256
|
+
} else if (r.ok) 된것.push({ 이름, 어떻게: 확장 === '.py' ? 'py_compile' : 'node --check' });
|
|
257
|
+
else 탈난것.push({ 이름, 탈: [짧게(r.out) || `종료코드 ${r.code}`] });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── 읽어서 보는 것 ──────────────────────────────────────────────
|
|
262
|
+
let 탈 = null;
|
|
263
|
+
if (확장 === '.html' || 확장 === '.htm') {
|
|
264
|
+
탈 = html보기(글, { 있는파일: (곳) => existsSync(resolve(dirname(abs), 곳)) });
|
|
265
|
+
} else if (['.css', '.scss', '.less'].includes(확장)) {
|
|
266
|
+
탈 = css보기(글);
|
|
267
|
+
} else if (확장 === '.json') {
|
|
268
|
+
탈 = json보기(글);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (탈 === null) { 못한것.push({ 이름, 왜: `${확장 || '확장자 없음'} 은 아직 확인할 줄 모릅니다` }); continue; }
|
|
272
|
+
if (탈.length) 탈난것.push({ 이름, 탈 });
|
|
273
|
+
else 된것.push({ 이름, 어떻게: 확장 === '.json' ? 'JSON 파싱' : '읽어서 확인' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── 사실대로 적는다 ───────────────────────────────────────────────
|
|
277
|
+
const 줄 = [];
|
|
278
|
+
|
|
279
|
+
if (탈난것.length) {
|
|
280
|
+
줄.push(`탈난 것 ${탈난것.length}개 — 여기부터 고쳐라:`);
|
|
281
|
+
for (const x of 탈난것) {
|
|
282
|
+
줄.push(` ✗ ${x.이름}`);
|
|
283
|
+
for (const t of x.탈.slice(0, 5)) 줄.push(` ${t}`);
|
|
284
|
+
if (x.탈.length > 5) 줄.push(` … 그 밖에 ${x.탈.length - 5}개`);
|
|
285
|
+
}
|
|
286
|
+
줄.push('');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (된것.length) {
|
|
290
|
+
줄.push(`확인된 것 ${된것.length}개:`);
|
|
291
|
+
for (const x of 된것.slice(0, 20)) 줄.push(` ✓ ${x.이름} (${x.어떻게})`);
|
|
292
|
+
if (된것.length > 20) 줄.push(` … 그 밖에 ${된것.length - 20}개`);
|
|
293
|
+
줄.push('');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/*
|
|
297
|
+
* 못 확인한 것을 **반드시** 적는다.
|
|
298
|
+
*
|
|
299
|
+
* 이걸 빼면 "확인된 것 3개" 만 보이고, 사람도 모델도 그게 전부인 줄 안다.
|
|
300
|
+
* 그러면 이 도구는 확인해 주는 물건이 아니라 안심시켜 주는 물건이 된다.
|
|
301
|
+
*/
|
|
302
|
+
if (못한것.length) {
|
|
303
|
+
줄.push(`확인 못 한 것 ${못한것.length}개 (**됐다고 말하면 안 된다**):`);
|
|
304
|
+
for (const x of 못한것.slice(0, 15)) 줄.push(` ? ${x.이름} — ${x.왜}`);
|
|
305
|
+
if (못한것.length > 15) 줄.push(` … 그 밖에 ${못한것.length - 15}개`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!줄.length) 줄.push('확인할 수 있는 파일이 없었습니다. 무엇을 확인할지 paths 로 알려 주세요.');
|
|
309
|
+
|
|
310
|
+
/*
|
|
311
|
+
* 이 프로젝트가 스스로 정해 둔 확인 방법이 있으면 **알려만 준다.**
|
|
312
|
+
*
|
|
313
|
+
* 여기서 직접 돌리지 않는다. 검사 스크립트는 무슨 짓이든 할 수 있고,
|
|
314
|
+
* 그걸 돌리는 길은 Bash 하나뿐이어야 한다 — 거기에만 승인 관문과 안전
|
|
315
|
+
* 검사가 걸려 있기 때문이다. 이 도구가 몰래 돌리면 strict 모드에서
|
|
316
|
+
* "물어보고 실행한다" 는 약속이 이 자리에서만 깨진다.
|
|
317
|
+
*/
|
|
318
|
+
const 있는명령 = 프로젝트확인법(뿌리);
|
|
319
|
+
if (있는명령) {
|
|
320
|
+
줄.push('');
|
|
321
|
+
줄.push(`이 프로젝트에는 ${있는명령} 가 있습니다. 진짜로 도는지 보려면 Bash 로 돌려라.`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const 다됐나 = !탈난것.length;
|
|
325
|
+
return {
|
|
326
|
+
content: 줄.join('\n').trim() + 건너뜀말(건너뜀, 잘림, 상한),
|
|
327
|
+
summary: 이어(
|
|
328
|
+
탈난것.length ? 말('sum.broken', { n: 탈난것.length }) : '',
|
|
329
|
+
말('sum.verified', { n: 된것.length }),
|
|
330
|
+
!탈난것.length && 못한것.length ? 말('sum.unverified', { n: 못한것.length }) : '',
|
|
331
|
+
),
|
|
332
|
+
// 루프가 '아직 안 끝났다' 고 알 수 있게. 탈이 났는데 성공으로 넘기면
|
|
333
|
+
// 다음 걸음에서 모델이 "다 됐습니다" 로 답을 맺는다.
|
|
334
|
+
failed: !다됐나,
|
|
335
|
+
확인됨: 된것.length,
|
|
336
|
+
탈: 탈난것.length,
|
|
337
|
+
못확인: 못한것.length,
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 이 프로젝트가 스스로 정해 둔 확인 방법.
|
|
344
|
+
*
|
|
345
|
+
* package.json 에 test 가 있으면 그것이 답이다. 없는 것을 지어내지는 않는다 —
|
|
346
|
+
* 없으면 null 이고, 그러면 파일별 확인만 한 것이 전부다.
|
|
347
|
+
* 돌리지는 않는다. 알려 주기만 한다 (위 머리말 참고).
|
|
348
|
+
*/
|
|
349
|
+
function 프로젝트확인법(뿌리) {
|
|
350
|
+
const pkg = join(뿌리, 'package.json');
|
|
351
|
+
if (!existsSync(pkg)) return null;
|
|
352
|
+
try {
|
|
353
|
+
const j = JSON.parse(readFileSync(pkg, 'utf8'));
|
|
354
|
+
if (j.scripts?.test) return 'npm test';
|
|
355
|
+
if (j.scripts?.build) return 'npm run build';
|
|
356
|
+
} catch { /* 망가진 package.json 은 아래 파일 확인에서 잡힌다 */ }
|
|
357
|
+
return null;
|
|
358
|
+
}
|