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.
- package/README.ko.md +1166 -1182
- package/README.md +1223 -1232
- package/bin/deel.js +55 -12
- package/package.json +3 -3
- package/src/acp/serve.js +40 -3
- package/src/agent/compact.js +314 -296
- package/src/agent/effort.js +27 -7
- package/src/agent/evidence.js +2 -0
- package/src/agent/filemem.js +141 -0
- package/src/agent/grade.js +51 -1
- package/src/agent/loop.js +202 -35
- package/src/agent/session.js +121 -5
- package/src/agent/store.js +186 -9
- package/src/agent/threads.js +26 -1
- package/src/backend/adapter.js +201 -18
- 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 +29 -5
- package/src/backend/toolfit.js +325 -0
- package/src/commands.js +107 -27
- package/src/config.js +47 -2
- package/src/i18n/en.js +56 -2
- package/src/i18n/index.js +18 -0
- package/src/i18n/ja.js +58 -2
- package/src/i18n/ko.js +103 -2
- package/src/i18n/zh.js +58 -2
- package/src/lsp/client.js +49 -5
- package/src/oneshot.js +81 -3
- 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/repl.js +264 -35
- 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/index.js +239 -34
- package/src/tools/jobs.js +158 -29
- package/src/tools/verify.js +33 -4
- 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/md.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// 실제로는 제목·목록·코드펜스가 다 짧은 줄이라 거의 다 그려진다. 안 그려지는
|
|
29
29
|
// 것은 긴 산문뿐인데, 산문은 원래 꾸밀 것이 없다.
|
|
30
30
|
import { c, width } from './ansi.js';
|
|
31
|
+
import { 접어쓰기 } from './wrap.js';
|
|
31
32
|
|
|
32
33
|
const 굵게 = (s) => c.bold(s);
|
|
33
34
|
const 코드 = (s) => c.hyellow(s);
|
|
@@ -40,6 +41,10 @@ const 코드 = (s) => c.hyellow(s);
|
|
|
40
41
|
const 회색앞 = c.gray('·').replace(/·[\s\S]*$/, '');
|
|
41
42
|
const 이어흐리게 = (s) => c.gray(String(s).replace(/\x1b\[0m/g, `\x1b[0m${회색앞}`));
|
|
42
43
|
|
|
44
|
+
// 굵게도 같은 문제를 겪는다 — 칸 안의 코드가 색을 끝내면서 굵기까지 꺼 버린다.
|
|
45
|
+
const 굵게앞 = c.bold('·').replace(/·[\s\S]*$/, '');
|
|
46
|
+
const 이어굵게 = (s) => c.bold(String(s).replace(/\x1b\[0m/g, `\x1b[0m${굵게앞}`));
|
|
47
|
+
|
|
43
48
|
/**
|
|
44
49
|
* 한 줄 안쪽 꾸밈. `코드` → **굵게** → *기울임* → ~~취소~~ → [글](주소) 차례.
|
|
45
50
|
*
|
|
@@ -146,6 +151,143 @@ export function 한줄(줄, 상태 = { 펜스: false }, 폭 = 80) {
|
|
|
146
151
|
return { 글: 인라인(s), 상태 };
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
/*
|
|
155
|
+
* ── 표 ──────────────────────────────────────────────────────────────────
|
|
156
|
+
*
|
|
157
|
+
* 표만은 **모아서** 그린다. 칸 너비는 그 열에서 제일 긴 칸이 정하는데, 그건
|
|
158
|
+
* 마지막 줄까지 봐야 안다. 줄마다 바로 그리면 칸이 안 맞아서 세로줄이
|
|
159
|
+
* 지그재그로 흐른다 — 여태 나오던 모양이 그것이다.
|
|
160
|
+
*
|
|
161
|
+
* 다 모으는 것이 아니라 **표인 동안만** 모은다. 표가 아닌 줄이 하나 오면 그
|
|
162
|
+
* 자리에서 표를 그려 내보내고 하던 대로 돌아간다. 그래서 산문이 늦어지는 일은
|
|
163
|
+
* 없고, 표는 몇 줄 안 되고 붙어서 오므로 사람 눈에는 한 덩이로 뜬다.
|
|
164
|
+
*
|
|
165
|
+
* 모으다가 표가 아니었던 것으로 드러나면(가름줄이 없으면) 모아 둔 줄을 옛
|
|
166
|
+
* 방식으로 한 줄씩 내보낸다. 글이 사라지는 것이 제일 나쁘다.
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
/*
|
|
170
|
+
* 붙들기의 상한 둘.
|
|
171
|
+
*
|
|
172
|
+
* 표라고 우기는 것이 몇백 줄이거나 한 줄이 끝없이 길면, 그동안 화면은 멈춘
|
|
173
|
+
* 것처럼 보인다. 모양보다 '살아 있다' 가 먼저라는 이 파일의 규칙은 표에도
|
|
174
|
+
* 그대로 적용된다.
|
|
175
|
+
*/
|
|
176
|
+
const 표붙들상한 = 200; // 몇 줄까지 모아 두나
|
|
177
|
+
const 표줄최대배 = 8; // 한 줄이 창 폭의 몇 배까지면 줄 끝을 기다리나
|
|
178
|
+
|
|
179
|
+
/** `| 가 | 나 |` 모양인가. 바깥 세로줄을 요구한다 — 산문에 든 `|` 를 안 삼키려고. */
|
|
180
|
+
const 표줄인가 = (s) => /^\s*\|.*\|\s*$/.test(String(s ?? '').replace(/\r+$/, ''));
|
|
181
|
+
|
|
182
|
+
/** `---` `:---` `---:` `:---:` — 가름줄의 한 칸인가. */
|
|
183
|
+
const 가름칸인가 = (칸) => /^:?-+:?$/.test(String(칸).trim());
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 한 줄을 칸으로 가른다.
|
|
187
|
+
*
|
|
188
|
+
* 그냥 split('|') 로 하면 두 자리에서 깨진다. `\|` 로 적은 세로줄(칸 안에
|
|
189
|
+
* 세로줄을 쓰려면 이 방법뿐이다)과, `` `a | b` `` 처럼 인라인 코드 안에 든
|
|
190
|
+
* 세로줄이다. 둘 다 칸을 가르면 안 되는 자리다.
|
|
191
|
+
*/
|
|
192
|
+
function 칸가르기(줄) {
|
|
193
|
+
const s = String(줄 ?? '').replace(/\r+$/, '').trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
194
|
+
const 칸들 = [];
|
|
195
|
+
let 지금 = '';
|
|
196
|
+
let 코드안 = false;
|
|
197
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
198
|
+
const ch = s[i];
|
|
199
|
+
if (ch === '\\' && s[i + 1] === '|') { 지금 += '|'; i += 1; continue; }
|
|
200
|
+
if (ch === '`') { 코드안 = !코드안; 지금 += ch; continue; }
|
|
201
|
+
if (ch === '|' && !코드안) { 칸들.push(지금); 지금 = ''; continue; }
|
|
202
|
+
지금 += ch;
|
|
203
|
+
}
|
|
204
|
+
칸들.push(지금);
|
|
205
|
+
return 칸들.map((x) => x.trim());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 가름줄이 알려 주는 정렬. `---:` 는 오른쪽, `:---:` 는 가운데. 숫자 칸에 쓴다. */
|
|
209
|
+
function 기울기(칸) {
|
|
210
|
+
const t = String(칸).trim();
|
|
211
|
+
if (t.startsWith(':') && t.endsWith(':')) return '가운데';
|
|
212
|
+
if (t.endsWith(':')) return '오른쪽';
|
|
213
|
+
return '왼쪽';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 칸을 제 너비까지 밀어 준다. width() 가 제어문자를 안 세므로 색이 끼어도 맞는다. */
|
|
217
|
+
function 채우기(글, 폭, 쪽) {
|
|
218
|
+
const 남은 = Math.max(0, 폭 - width(글));
|
|
219
|
+
if (쪽 === '오른쪽') return ' '.repeat(남은) + 글;
|
|
220
|
+
if (쪽 === '가운데') {
|
|
221
|
+
const 왼 = Math.floor(남은 / 2);
|
|
222
|
+
return ' '.repeat(왼) + 글 + ' '.repeat(남은 - 왼);
|
|
223
|
+
}
|
|
224
|
+
return 글 + ' '.repeat(남은);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 모아 둔 표 줄들을 칸 맞춰 그린다. 표가 아니면 null 을 돌려준다.
|
|
229
|
+
*
|
|
230
|
+
* 창보다 넓으면 **제일 넓은 칸부터** 한 칸씩 줄인다. 골고루 줄이면 `번호` 같은
|
|
231
|
+
* 짧은 칸까지 같이 좁아져서 거기부터 글자가 접힌다 — 정작 줄여야 할 것은 긴
|
|
232
|
+
* 설명 칸이다. 줄인 칸은 잘라 버리지 않고 접는다(접어쓰기). 표에서 글자가
|
|
233
|
+
* 사라지면 그 줄은 아예 못 읽는다.
|
|
234
|
+
*/
|
|
235
|
+
export function 표그리기(줄들, 폭 = 80) {
|
|
236
|
+
const 행들 = (줄들 ?? []).map(칸가르기);
|
|
237
|
+
// 가름줄이 있어야 표다. 첫 줄일 수는 없다 — 머리줄이 그 위에 있어야 한다.
|
|
238
|
+
const 가름 = 행들.findIndex((행) => 행.length > 1 && 행.every(가름칸인가));
|
|
239
|
+
if (가름 < 1) return null;
|
|
240
|
+
|
|
241
|
+
const 열수 = Math.max(...행들.map((행) => 행.length));
|
|
242
|
+
const 쪽들 = Array.from({ length: 열수 }, (_, i) => 기울기(행들[가름][i] ?? ''));
|
|
243
|
+
|
|
244
|
+
// 가름줄은 자리만 알려 주고 화면에는 안 나간다 — 우리가 진짜 선을 긋는다.
|
|
245
|
+
const 몸 = 행들
|
|
246
|
+
.filter((_, i) => i !== 가름)
|
|
247
|
+
.map((행) => Array.from({ length: 열수 }, (_, i) => 인라인(행[i] ?? '')));
|
|
248
|
+
if (!몸.length) return null;
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* 치레가 먹는 폭: 칸마다 세로줄 하나와 좌우 한 칸씩(3), 그리고 맨 오른쪽
|
|
252
|
+
* 세로줄 하나. 이걸 빼고 남는 것이 글자가 쓸 수 있는 폭이다.
|
|
253
|
+
*/
|
|
254
|
+
const 치레 = 3 * 열수 + 1;
|
|
255
|
+
const 쓸수있는 = Math.max(열수, 폭 - 치레);
|
|
256
|
+
const 너비 = Array.from({ length: 열수 }, (_, i) => Math.max(1, ...몸.map((행) => width(행[i]))));
|
|
257
|
+
let 합 = 너비.reduce((a, b) => a + b, 0);
|
|
258
|
+
while (합 > 쓸수있는) {
|
|
259
|
+
let 큰 = 0;
|
|
260
|
+
for (let i = 1; i < 너비.length; i += 1) if (너비[i] > 너비[큰]) 큰 = i;
|
|
261
|
+
if (너비[큰] <= 1) break;
|
|
262
|
+
너비[큰] -= 1;
|
|
263
|
+
합 -= 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const 가로 = (왼, 사이, 오른) =>
|
|
267
|
+
c.gray(왼 + 너비.map((w) => '─'.repeat(w + 2)).join(사이) + 오른);
|
|
268
|
+
const 세로 = c.gray('│');
|
|
269
|
+
|
|
270
|
+
const 한행 = (행) => {
|
|
271
|
+
// 접힌 칸이 여럿이면 그 행은 여러 줄이 된다. 제일 높은 칸에 키를 맞춘다.
|
|
272
|
+
const 접힌 = 행.map((칸, i) => (칸 === '' ? [''] : 접어쓰기(칸, 너비[i])));
|
|
273
|
+
const 높이 = Math.max(1, ...접힌.map((x) => x.length));
|
|
274
|
+
const 나갈것 = [];
|
|
275
|
+
for (let k = 0; k < 높이; k += 1) {
|
|
276
|
+
const 칸들 = 접힌.map((x, i) => 채우기(x[k] ?? '', 너비[i], 쪽들[i]));
|
|
277
|
+
나갈것.push(`${세로} ${칸들.join(` ${세로} `)} ${세로}`);
|
|
278
|
+
}
|
|
279
|
+
return 나갈것;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const 화면 = [가로('┌', '┬', '┐')];
|
|
283
|
+
몸.forEach((행, i) => {
|
|
284
|
+
// 머리줄만 굵게. 어디까지가 이름이고 어디부터가 값인지가 한눈에 갈린다.
|
|
285
|
+
화면.push(...한행(i === 0 ? 행.map(이어굵게) : 행));
|
|
286
|
+
화면.push(i === 몸.length - 1 ? 가로('└', '┴', '┘') : 가로('├', '┼', '┤'));
|
|
287
|
+
});
|
|
288
|
+
return 화면;
|
|
289
|
+
}
|
|
290
|
+
|
|
149
291
|
/**
|
|
150
292
|
* 흘러오는 토막을 받아 그릴 수 있게 된 줄만 내놓는다.
|
|
151
293
|
*
|
|
@@ -164,15 +306,50 @@ export class 마크다운 {
|
|
|
164
306
|
this.상태 = { 펜스: false };
|
|
165
307
|
// 이 줄은 이미 날것으로 흘려보내기 시작했다 — 뒤늦게 꾸미면 앞뒤가 안 맞는다.
|
|
166
308
|
this.날것중 = false;
|
|
309
|
+
// 그리려고 붙들고 있는 표 줄들. 표가 끝나는 순간 칸을 맞춰 한꺼번에 나간다.
|
|
310
|
+
this.표줄들 = [];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** 붙들고 있던 표를 내보낸다. 표가 아니었으면 옛 방식으로 한 줄씩 내보낸다. */
|
|
314
|
+
#표비우기() {
|
|
315
|
+
const 줄들 = this.표줄들;
|
|
316
|
+
this.표줄들 = [];
|
|
317
|
+
if (!줄들.length) return [];
|
|
318
|
+
const 그린것 = 표그리기(줄들, this.폭);
|
|
319
|
+
if (그린것) return 그린것;
|
|
320
|
+
const 나갈것 = [];
|
|
321
|
+
for (const 줄 of 줄들) {
|
|
322
|
+
const r = 한줄(줄, this.상태, this.폭);
|
|
323
|
+
this.상태 = r.상태;
|
|
324
|
+
나갈것.push(r.글);
|
|
325
|
+
}
|
|
326
|
+
return 나갈것;
|
|
167
327
|
}
|
|
168
328
|
|
|
169
329
|
#그리기(줄) {
|
|
170
330
|
// 이 줄은 이미 날것으로 나가기 시작했다. 뒤늦게 꾸미면 앞뒤가 안 맞으므로
|
|
171
331
|
// 남은 만큼을 그대로 이어 붙이고 줄만 끝낸다.
|
|
172
|
-
if (this.날것중) {
|
|
332
|
+
if (this.날것중) {
|
|
333
|
+
this.날것중 = false;
|
|
334
|
+
return [{ 이어붙임: String(줄).replace(/\r/g, ''), 끝났나: true }];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/*
|
|
338
|
+
* 표 줄은 붙든다. 다만 **코드 울타리 안에서는 안 한다** — 거기서는 세로줄도
|
|
339
|
+
* 그냥 글자다. SQL 이나 아스키 그림을 표로 읽어 다시 그리면 코드가 망가진다.
|
|
340
|
+
*
|
|
341
|
+
* 끝없이 붙들지는 않는다. 표라고 우기는 것이 몇백 줄이면 화면이 그동안
|
|
342
|
+
* 멈춘 것처럼 보인다. 그때는 붙들기를 그만두고 흘려보낸다.
|
|
343
|
+
*/
|
|
344
|
+
if (!this.상태.펜스 && 표줄인가(줄)) {
|
|
345
|
+
this.표줄들.push(줄);
|
|
346
|
+
return this.표줄들.length >= 표붙들상한 ? this.#표비우기() : [];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const 앞 = this.#표비우기();
|
|
173
350
|
const r = 한줄(줄, this.상태, this.폭);
|
|
174
351
|
this.상태 = r.상태;
|
|
175
|
-
return r
|
|
352
|
+
return [...앞, r.글];
|
|
176
353
|
}
|
|
177
354
|
|
|
178
355
|
넣기(토막) {
|
|
@@ -185,13 +362,31 @@ export class 마크다운 {
|
|
|
185
362
|
// \r\n 으로 온 줄은 여기서 \r 를 뗀다. 남겨서 내보내면 터미널에서 커서가
|
|
186
363
|
// 줄 맨 앞으로 돌아가 앞에 세운 세로줄 위에 덮어쓴다 — 글자가 먹힌 것처럼
|
|
187
364
|
// 보인다. 토막이 \r 와 \n 사이에서 끊겨 와도 여기로 모여서 걸린다.
|
|
188
|
-
나갈것.push(this.#그리기(this.담긴것.slice(0, i)));
|
|
365
|
+
나갈것.push(...this.#그리기(this.담긴것.slice(0, i)));
|
|
189
366
|
this.담긴것 = this.담긴것.slice(i + 1);
|
|
190
367
|
continue;
|
|
191
368
|
}
|
|
369
|
+
/*
|
|
370
|
+
* 표 줄은 길어도 안 흘려보낸다.
|
|
371
|
+
*
|
|
372
|
+
* 표 한 줄은 창보다 넓은 것이 보통이다(칸이 셋만 돼도 넘는다). 여기서
|
|
373
|
+
* 날것으로 내보내면 그 줄은 표로 모이지도 못하고 세로줄이 그대로 뜬다 —
|
|
374
|
+
* 고치려던 바로 그 모양이다. 그래서 세로줄로 시작하는 줄은 줄이 끝날
|
|
375
|
+
* 때까지 기다린다.
|
|
376
|
+
*
|
|
377
|
+
* 그래도 상한은 둔다. 표가 아닌데 세로줄로 시작하는 긴 글(로그 한 덩이
|
|
378
|
+
* 같은 것)이 오면 화면이 멈춘 것처럼 보이면 안 된다.
|
|
379
|
+
*/
|
|
380
|
+
const 표기다림 = !this.상태.펜스
|
|
381
|
+
&& (this.표줄들.length > 0 || /^\s*\|/.test(this.담긴것))
|
|
382
|
+
&& width(this.담긴것) <= this.폭 * 표줄최대배;
|
|
383
|
+
if (표기다림) break;
|
|
384
|
+
|
|
192
385
|
// 줄이 안 끝나는데 너무 길어졌다. 여기까지 날것으로 흘려보낸다 —
|
|
193
386
|
// 멈춘 것처럼 보이는 것보다 안 꾸며진 글이 흐르는 편이 낫다.
|
|
194
387
|
if (width(this.담긴것) > this.폭) {
|
|
388
|
+
// 붙들고 있던 표부터 내보낸다. 안 그러면 표가 뒤에 나와서 순서가 뒤집힌다.
|
|
389
|
+
나갈것.push(...this.#표비우기());
|
|
195
390
|
// 날것으로 나가는 길에는 한줄() 이 안 끼므로 여기서 \r 를 턴다.
|
|
196
391
|
나갈것.push({ 이어붙임: this.담긴것.replace(/\r/g, ''), 첫조각: !this.날것중, 끝났나: false });
|
|
197
392
|
this.담긴것 = '';
|
|
@@ -208,12 +403,13 @@ export class 마크다운 {
|
|
|
208
403
|
if (!this.담긴것) {
|
|
209
404
|
const 닫을것 = this.날것중 ? [{ 이어붙임: '', 끝났나: true }] : [];
|
|
210
405
|
this.날것중 = false;
|
|
211
|
-
|
|
406
|
+
// 표로 끝나는 답이 흔하다(마지막이 정리 표). 여기서 안 비우면 통째로 사라진다.
|
|
407
|
+
return [...(닫을것.length ? 닫을것 : []), ...this.#표비우기()];
|
|
212
408
|
}
|
|
213
409
|
const 남은것 = this.#그리기(this.담긴것);
|
|
214
410
|
this.담긴것 = '';
|
|
215
411
|
this.날것중 = false;
|
|
216
|
-
return [
|
|
412
|
+
return [...남은것, ...this.#표비우기()];
|
|
217
413
|
}
|
|
218
414
|
}
|
|
219
415
|
|
package/src/ui/motion.js
CHANGED
package/src/ui/pastechip.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// 바이트 그대로다. 되돌릴 수 없는 접기는 접기가 아니라 잘라내기다.
|
|
22
22
|
import { 접어쓰기 } from './wrap.js';
|
|
23
23
|
import { 안쪽최대 } from './inputbox.js';
|
|
24
|
+
import { 말, 말모두 } from '../i18n/index.js';
|
|
24
25
|
|
|
25
26
|
/** 사람이 읽는 크기. 47줄이 몇 KB 인지 알아야 뭘 붙였는지 감이 온다. */
|
|
26
27
|
export function 크기말(바이트) {
|
|
@@ -40,6 +41,24 @@ export function 크기말(바이트) {
|
|
|
40
41
|
export function 접을까(글, { 폭 = 80, 최대 = 안쪽최대 } = {}) {
|
|
41
42
|
const 원 = String(글 ?? '');
|
|
42
43
|
if (!원) return false;
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* 줄이 둘 이상이면 무조건 접는다 — 상자가 그것밖에 못 보여 주기 때문이다.
|
|
47
|
+
*
|
|
48
|
+
* readline 의 `rl.line` 에는 개행을 못 넣는다. 넣으면 보낼 때 줄 순서가
|
|
49
|
+
* 뒤집힌다(newline.test.js 가 지키는 자리다). 그래서 붙여넣은 나머지 줄은
|
|
50
|
+
* 상자 밖 버퍼에 따로 두는데, 상자는 `rl.line` 만 그린다. 결과는 이렇다.
|
|
51
|
+
*
|
|
52
|
+
* 넉 줄을 붙인다 → 「4줄을 붙여넣었습니다」 가 뜨고
|
|
53
|
+
* 상자에는 **맨 밑 한 줄만** 보인다
|
|
54
|
+
*
|
|
55
|
+
* 여덟 줄까지는 들어가니까 안 접어도 된다고 여겼던 것이 틀렸다. 들어갈
|
|
56
|
+
* 자리가 있느냐가 아니라 **그릴 수 있느냐**가 갈림이다. 못 그릴 바에는
|
|
57
|
+
* 표 한 줄로 접는 편이 낫다 — 몇 줄 몇 바이트인지는 그대로 보이고,
|
|
58
|
+
* 앞뒤에 덧붙인 말도 안 가려진다.
|
|
59
|
+
*/
|
|
60
|
+
if (원.includes('\n')) return true;
|
|
61
|
+
|
|
43
62
|
const 쓸폭 = Math.max(4, Number(폭) || 80);
|
|
44
63
|
let 줄수 = 0;
|
|
45
64
|
for (const 한줄 of 원.split('\n')) {
|
|
@@ -49,10 +68,30 @@ export function 접을까(글, { 폭 = 80, 최대 = 안쪽최대 } = {}) {
|
|
|
49
68
|
return false;
|
|
50
69
|
}
|
|
51
70
|
|
|
52
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* 대화에 **남길** 글. 표는 작을 때만 펴서 남긴다.
|
|
73
|
+
*
|
|
74
|
+
* 상자에 그릴 때와 대화에 남길 때는 셈이 다르다. 상자는 `rl.line` 이라
|
|
75
|
+
* 여러 줄을 못 그리니 몇 줄이든 접어야 한다. 대화는 그냥 찍는 자리라
|
|
76
|
+
* 못 그릴 것이 없다 — 오히려 접으면 나중에 스크롤을 올렸을 때 무엇을
|
|
77
|
+
* 보냈는지가 사라진다.
|
|
78
|
+
*
|
|
79
|
+
* 그렇다고 마흔 줄을 다 찍으면 화면이 붙인 글로 뒤덮인다. 그래서 상자에
|
|
80
|
+
* 들어갈 만한 크기(안쪽최대)까지만 펴고, 그보다 크면 표를 남긴다 — 표에는
|
|
81
|
+
* 줄 수와 크기가 적혀 있어서 그 자체가 기록이 된다.
|
|
82
|
+
*/
|
|
83
|
+
export function 남길글(글, 붙인것들) {
|
|
84
|
+
const 원 = String(글 ?? '');
|
|
85
|
+
const 펴진것 = 펼치기(원, 붙인것들);
|
|
86
|
+
if (펴진것 === 원) return 원;
|
|
87
|
+
return 펴진것.split('\n').length <= 안쪽최대 ? 펴진것 : 원;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `[붙여넣기 #1 · 47줄 · 2.1KB]` — 낱말은 고른 말을 따른다. */
|
|
53
91
|
export function 표만들기(번호, 글) {
|
|
54
92
|
const 원 = String(글 ?? '');
|
|
55
|
-
|
|
93
|
+
const 줄 = 말('paste.chipLines', { n: 원.split('\n').length });
|
|
94
|
+
return `[${말('paste.chip')} #${번호} · ${줄} · ${크기말(Buffer.byteLength(원, 'utf8'))}]`;
|
|
56
95
|
}
|
|
57
96
|
|
|
58
97
|
/*
|
|
@@ -64,7 +103,15 @@ export function 표만들기(번호, 글) {
|
|
|
64
103
|
* /g 를 안 붙인다 — 붙이면 lastIndex 가 남아서 같은 무늬로 test() 를
|
|
65
104
|
* 두 번 부를 때 두 번째가 조용히 거짓이 된다. 훑는 자리에서만 새로 만든다.
|
|
66
105
|
*/
|
|
67
|
-
|
|
106
|
+
/*
|
|
107
|
+
* 낱말은 **네 말을 다 받는다.**
|
|
108
|
+
*
|
|
109
|
+
* 표를 찍은 뒤에 `/lang en` 을 치면, 지금 말로만 찾는 무늬는 앞서 찍어 둔
|
|
110
|
+
* 표를 못 알아본다. 그러면 사람이 붙인 40줄이 표 글자 그대로 모델에게
|
|
111
|
+
* 가고, 아무 데도 안 적히는 채로 그렇게 된다. 어느 말로 찍힌 표든 편다.
|
|
112
|
+
*/
|
|
113
|
+
const 낱말들 = 말모두('paste.chip').map((w) => String(w).replace(/[.*+?^${}()|[\]\\]/g, (ch) => `\\${ch}`));
|
|
114
|
+
export const 표무늬 = new RegExp(`\\[(?:${낱말들.join('|')}) #(\\d+) · [^[\\]]*\\]`);
|
|
68
115
|
|
|
69
116
|
/** 글 안의 표를 원문으로 되돌린다. 모르는 번호는 사람이 친 글로 보고 둔다. */
|
|
70
117
|
export function 펼치기(글, 붙인것들) {
|
package/src/ui/pick.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// 고른 번호 + 하고 싶은 말, 한 줄에서 갈라 읽기.
|
|
2
|
+
//
|
|
3
|
+
// ── 사람 말 ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 「사용자에게 계획 물어볼 때 번호만 입력하는 게 아니라, 번호를 고르고
|
|
6
|
+
// 의견이 있으면 의견도 입력할 수 있게」
|
|
7
|
+
//
|
|
8
|
+
// 여태는 둘 중 하나였다. 숫자만 치면 그 줄을 고른 것이고, 그 밖의 글은
|
|
9
|
+
// **고른 것 없이** 통째로 자유 답이었다. 그래서 「2번인데 파일은 나눠서」
|
|
10
|
+
// 라고 치면 2번을 고른 사실이 사라진다. 사람은 골랐다고 생각하는데
|
|
11
|
+
// 모델에게는 고른 것이 없는 문장 하나만 간다.
|
|
12
|
+
//
|
|
13
|
+
// 실제로는 이렇게 답하고 싶어진다 —
|
|
14
|
+
//
|
|
15
|
+
// 2 파일은 나눠서 해줘 (고르고 + 덧붙이고)
|
|
16
|
+
// 2: 파일은 나눠서 해줘
|
|
17
|
+
// 2, 파일은 나눠서 해줘
|
|
18
|
+
// 2 - 파일은 나눠서 해줘
|
|
19
|
+
// 1,3 (둘 다)
|
|
20
|
+
// 1 3 둘 다 하되 순서는 반대로
|
|
21
|
+
//
|
|
22
|
+
// ── 어디서 멈추나 ───────────────────────────────────────────────────────
|
|
23
|
+
//
|
|
24
|
+
// 숫자로 읽는 것은 **머리에 붙은, 목록 안에 있는 수**뿐이다. 그 뒤로는 다
|
|
25
|
+
// 사람 말이다. 이 선이 없으면 「3개만 해줘」 가 3번을 고른 것이 된다 —
|
|
26
|
+
// 사람이 고른 적 없는 것을 골랐다고 하는 셈이라, 안 받느니만 못하다.
|
|
27
|
+
// 그래서 수 바로 뒤에 글자가 붙어 있으면(3개) 수로 안 읽는다.
|
|
28
|
+
//
|
|
29
|
+
// 목록에 없는 수도 안 읽는다. 선택지가 둘인데 「5분만 기다려」 라고 치면
|
|
30
|
+
// 그건 고른 것이 아니라 하는 말이다.
|
|
31
|
+
|
|
32
|
+
/** 수 뒤에 올 수 있는 것 — 끝, 빈칸, 그리고 흔한 구분표. `1.5` 는 수가 아니다. */
|
|
33
|
+
const 수뒤 = /^(\d{1,3})(?:\s*[.)::\]](?!\d)|\s*[,,、·]|\s|$)/;
|
|
34
|
+
|
|
35
|
+
/** 수와 말 사이에 사람이 흔히 넣는 것들. 말 앞머리에서 떼어 낸다. */
|
|
36
|
+
const 사이표 = /^[\s,,、·::\-–—>]+/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 답 한 줄을 「고른 번호들」과 「덧붙인 말」로 가른다.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} 답 사람이 친 그대로
|
|
42
|
+
* @param {string[]} 고를것 보여 준 선택지
|
|
43
|
+
* @returns {{번호들: number[], 덧말: string, 글: string}}
|
|
44
|
+
* 글 — 모델에게 보낼 한 줄. 고른 것이 없으면 친 글 그대로다.
|
|
45
|
+
*/
|
|
46
|
+
export function 고른것풀기(답, 고를것 = []) {
|
|
47
|
+
const 원 = String(답 ?? '').trim();
|
|
48
|
+
const 목록 = Array.isArray(고를것) ? 고를것 : [];
|
|
49
|
+
if (!원) return { 번호들: [], 덧말: '', 글: '' };
|
|
50
|
+
|
|
51
|
+
const 번호들 = [];
|
|
52
|
+
let 남은 = 원;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const m = 수뒤.exec(남은);
|
|
55
|
+
if (!m) break;
|
|
56
|
+
const n = Number(m[1]);
|
|
57
|
+
// 목록 밖의 수는 고른 것이 아니라 하는 말이다.
|
|
58
|
+
if (!Number.isInteger(n) || n < 1 || n > 목록.length) break;
|
|
59
|
+
// 같은 것을 두 번 쳐도 한 번만 센다 — 「1 1」 을 두 갈래로 읽을 까닭이 없다.
|
|
60
|
+
if (!번호들.includes(n)) 번호들.push(n);
|
|
61
|
+
남은 = 남은.slice(m[0].length).replace(사이표, '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const 덧말 = 남은.trim();
|
|
65
|
+
// 아무 수도 못 읽었으면 여태와 똑같다 — 친 글이 그대로 답이다.
|
|
66
|
+
if (!번호들.length) return { 번호들: [], 덧말: '', 글: 원 };
|
|
67
|
+
|
|
68
|
+
const 고른 = 번호들.map((n) => String(목록[n - 1] ?? '').trim()).filter(Boolean);
|
|
69
|
+
const 글 = 고른.join(' · ') + (덧말 ? ` — ${덧말}` : '');
|
|
70
|
+
return { 번호들, 덧말, 글 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 계획 상자의 답을 읽는다.
|
|
75
|
+
*
|
|
76
|
+
* 여기 세 갈래가 있다.
|
|
77
|
+
* · 그냥 진행 — ⏎ · y · yes · ㅇ · 진행
|
|
78
|
+
* · 진행하되 한마디 — `y 검사도 같이 넣어줘`
|
|
79
|
+
* · 그만 — n · no · ㄴ
|
|
80
|
+
* · 그 밖의 글 — 그 말대로 다시 짜라 (여태 그대로)
|
|
81
|
+
* · `3 이건 빼자` — 3단계에 대한 말로 붙여서 다시 짜라
|
|
82
|
+
*
|
|
83
|
+
* ⏎ 하나로 진행하는 것은 안 건드린다. 계획을 읽고 「그래」 하는 데
|
|
84
|
+
* 두 글자를 더 치게 하면, 그 두 글자 때문에 안 읽고 넘기게 된다.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} 답
|
|
87
|
+
* @param {number} 단계수 계획이 몇 단계인가 (번호를 가려내는 데 쓴다)
|
|
88
|
+
* @returns {{갈래: '진행'|'그만'|'다시', 덧말: string, 단계: number|null}}
|
|
89
|
+
*/
|
|
90
|
+
export function 계획답풀기(답, 단계수 = 0) {
|
|
91
|
+
const 원 = String(답 ?? '').trim();
|
|
92
|
+
if (!원) return { 갈래: '진행', 덧말: '', 단계: null };
|
|
93
|
+
|
|
94
|
+
const 첫낱말 = 원.split(/\s+/)[0].toLowerCase();
|
|
95
|
+
const 나머지 = 원.slice(원.split(/\s+/)[0].length).replace(사이표, '').trim();
|
|
96
|
+
|
|
97
|
+
if (['n', 'no', 'ㄴ', '아니', '아니요', '취소'].includes(첫낱말)) {
|
|
98
|
+
return { 갈래: '그만', 덧말: '', 단계: null };
|
|
99
|
+
}
|
|
100
|
+
if (['y', 'yes', 'ㅇ', '네', '예', '진행', 'ok', 'go'].includes(첫낱말)) {
|
|
101
|
+
return { 갈래: '진행', 덧말: 나머지, 단계: null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 「3 이건 빼자」 — 그 단계에 대고 하는 말이다.
|
|
105
|
+
const m = 수뒤.exec(원);
|
|
106
|
+
if (m) {
|
|
107
|
+
const n = Number(m[1]);
|
|
108
|
+
if (Number.isInteger(n) && n >= 1 && n <= 단계수) {
|
|
109
|
+
const 말 = 원.slice(m[0].length).replace(사이표, '').trim();
|
|
110
|
+
if (말) return { 갈래: '다시', 덧말: 말, 단계: n };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { 갈래: '다시', 덧말: 원, 단계: null };
|
|
115
|
+
}
|
package/src/ui/screen.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* 그대로 맞다. 스크롤·복사·`| tee` 도 그대로다.
|
|
32
32
|
*/
|
|
33
33
|
import { c, say, cursor, box } from './ansi.js';
|
|
34
|
+
import { 말 } from '../i18n/index.js';
|
|
34
35
|
import { statusLine, contextWarning, quotaWarning } from './status.js';
|
|
35
36
|
import { spin } from './spinner.js';
|
|
36
37
|
|
|
@@ -81,6 +82,8 @@ export class LineScreen {
|
|
|
81
82
|
* 이벤트가 들고 온 깊이를 화면에 알려 주는 쪽이 안 빠뜨린다.
|
|
82
83
|
*/
|
|
83
84
|
this.깊이 = 0;
|
|
85
|
+
// 이번 턴에 '멈추는 중' 을 이미 적었나. 같은 줄을 되풀이해 찍지 않는다.
|
|
86
|
+
this.멈춘다고했나 = false;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
// ── 흘러가는 글 ────────────────────────────────────────────────────────
|
|
@@ -190,9 +193,26 @@ export class LineScreen {
|
|
|
190
193
|
* 대신 기다림()·생각() 이 그 자리를 이미 맡고 있다 — 여기서 또 그리면
|
|
191
194
|
* 같은 말이 두 번 나간다. 파이프·기록에서는 돌아가는 표시 자체가 잡음이다.
|
|
192
195
|
*/
|
|
193
|
-
일시작() { }
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
일시작() { this.멈춘다고했나 = false; }
|
|
197
|
+
/*
|
|
198
|
+
* 다만 '멈춤' 은 예외다 — 여기만 한 줄을 낸다.
|
|
199
|
+
*
|
|
200
|
+
* ESC 를 눌러도 줄 화면(`--no-tui`·파이프)에서는 **아무것도 안 나왔다.**
|
|
201
|
+
* 멈추는 데는 잠깐이 걸리는데(돌던 자식을 죽이고, 안 돈 도구 자리를 채우고,
|
|
202
|
+
* 대화를 성하게 닫는다) 그 사이 화면이 아까 하던 말을 그대로 하고 있으면
|
|
203
|
+
* 사람은 안 먹었다고 판단한다. 그러면 또 누르고, 그러다 Ctrl+C 로 손이
|
|
204
|
+
* 가고, 두 번 누르면 대화가 통째로 닫힌다 — 상자 화면에서 문구를 붙박이로
|
|
205
|
+
* 둔 것과 똑같은 이유다.
|
|
206
|
+
*
|
|
207
|
+
* 한 번만 낸다. ESC 를 세 번 누르면 같은 줄이 세 번 찍히는데, 그건
|
|
208
|
+
* 신호가 아니라 잡음이다.
|
|
209
|
+
*/
|
|
210
|
+
일바꿈(갈래) {
|
|
211
|
+
if (갈래 !== '멈춤' || this.멈춘다고했나) return;
|
|
212
|
+
this.멈춘다고했나 = true;
|
|
213
|
+
this.줄(` ${c.gray(말('run.stopping'))}`);
|
|
214
|
+
}
|
|
215
|
+
일끝() { this.멈춘다고했나 = false; }
|
|
196
216
|
// 줄 화면에는 사무실이 없다 — 받아만 두고 아무것도 안 한다.
|
|
197
217
|
할일갱신() { }
|
|
198
218
|
하위갱신() { }
|