deel-local-cli 1.1.1 → 1.4.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.en.md +826 -19
- package/README.md +689 -14
- package/bin/deel.js +73 -4
- package/package.json +7 -3
- package/src/acp/jsonrpc.js +230 -0
- package/src/acp/map.js +219 -0
- package/src/acp/serve.js +556 -0
- package/src/agent/budget.js +0 -14
- package/src/agent/card.js +110 -0
- package/src/agent/compact.js +95 -0
- package/src/agent/evidence.js +186 -0
- package/src/agent/evolve.js +213 -0
- package/src/agent/grade.js +20 -0
- package/src/agent/loop.js +172 -14
- package/src/agent/models.js +169 -0
- package/src/agent/modes.js +49 -1
- package/src/agent/pins.js +140 -0
- package/src/agent/project.js +10 -4
- package/src/agent/session.js +359 -11
- package/src/agent/store.js +31 -0
- package/src/agent/threads.js +154 -0
- package/src/commands.js +751 -55
- package/src/config.js +10 -1
- package/src/i18n/en.js +244 -0
- package/src/i18n/index.js +126 -0
- package/src/i18n/ko.js +231 -0
- package/src/lsp/client.js +459 -0
- package/src/lsp/diag.js +112 -0
- package/src/lsp/rpc.js +84 -0
- package/src/lsp/servers.js +218 -0
- package/src/oneshot.js +23 -0
- package/src/pack/sbom.js +218 -0
- package/src/pack/selfpack.js +19 -3
- package/src/repl.js +273 -22
- package/src/safety/guard.js +37 -2
- package/src/safety/secrets.js +205 -0
- package/src/safety/undo.js +11 -4
- package/src/setup.js +2 -2
- package/src/skills/discover.js +1 -1
- package/src/tools/desc.en.js +219 -0
- package/src/tools/edit-match.js +16 -5
- package/src/tools/excel-com.js +1 -1
- package/src/tools/excel.js +1 -1
- package/src/tools/fsutil.js +0 -8
- package/src/tools/index.js +149 -6
- package/src/tools/lsp.js +327 -0
- package/src/tools/task.js +30 -2
- package/src/tools/todo.js +19 -3
- package/src/ui/ansi.js +42 -2
- package/src/ui/approve.js +25 -21
- package/src/ui/inputbox.js +69 -13
- package/src/ui/intro.js +174 -0
- package/src/ui/level.js +11 -5
- package/src/ui/md.js +227 -0
- package/src/ui/notify.js +101 -0
- package/src/ui/prompt.js +1 -1
- package/src/ui/screen.js +1 -1
- package/src/ui/status.js +190 -34
- package/src/ui/working.js +0 -3
- package/src/ui/wrap.js +1 -8
package/src/ui/ansi.js
CHANGED
|
@@ -54,6 +54,12 @@ export const cursor = {
|
|
|
54
54
|
show: () => ON && process.stdout.write('\x1b[?25h'),
|
|
55
55
|
up: (n = 1) => ON && process.stdout.write(`\x1b[${n}A`),
|
|
56
56
|
clearLine: () => ON && process.stdout.write('\x1b[2K\r'),
|
|
57
|
+
// 여러 조각을 한 덩이로 묶어 한 번에 내보낼 때 쓰는 글자값.
|
|
58
|
+
//
|
|
59
|
+
// 나눠 쓰면 그 사이가 화면에 그대로 보인다. 커서를 숨기기 전에 지우는 글이
|
|
60
|
+
// 먼저 나가면, 커서가 상자 안을 훑고 지나가는 것이 눈에 띈다.
|
|
61
|
+
숨김: ON ? '\x1b[?25l' : '',
|
|
62
|
+
보임: ON ? '\x1b[?25h' : '',
|
|
57
63
|
};
|
|
58
64
|
|
|
59
65
|
// 한글·한자·가나는 터미널에서 두 칸을 차지한다. 표 정렬이 이걸 모르면 어긋난다.
|
|
@@ -142,6 +148,42 @@ export function gauge(ratio, cells = 10) {
|
|
|
142
148
|
return tone('▰'.repeat(filled)) + c.gray('▱'.repeat(cells - filled));
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
/**
|
|
152
|
+
* 눈금이 있는 게이지.
|
|
153
|
+
*
|
|
154
|
+
* 게이지가 차는 것은 보이는데 **언제 무슨 일이 나는지**는 안 보였다. 55% 를
|
|
155
|
+
* 넘으면 오래된 도구 결과를 접기 시작하고, 80% 를 넘으면 대화를 요약한다.
|
|
156
|
+
* 둘 다 사람 눈에는 갑자기 일어나는 일이라 — 어느 날 갑자기 "앞선 대화를
|
|
157
|
+
* 줄였습니다" 가 뜨고, 모델이 방금 읽은 파일을 잊는다.
|
|
158
|
+
*
|
|
159
|
+
* 그래서 그 자리에 눈금을 미리 그어 둔다. 막대가 다가가는 것이 보이면
|
|
160
|
+
* 사람이 먼저 손을 쓸 수 있다 — 못 박아 두거나(/pin), 갈래를 새로 파거나.
|
|
161
|
+
* 지나간 눈금은 안 그린다. 이미 일어난 일을 계속 가리킬 이유가 없고,
|
|
162
|
+
* 남겨 두면 막대가 눈금에 가려 어디까지 찼는지가 흐려진다.
|
|
163
|
+
*
|
|
164
|
+
* @param {number} ratio
|
|
165
|
+
* @param {number} cells
|
|
166
|
+
* @param {number[]} 눈금 0~1 사이 자리들
|
|
167
|
+
*/
|
|
168
|
+
export function 눈금게이지(ratio, cells = 10, 눈금 = []) {
|
|
169
|
+
const n = Number(ratio);
|
|
170
|
+
const r = Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;
|
|
171
|
+
const filled = Math.round(r * cells);
|
|
172
|
+
const tone = r > 0.8 ? c.hred : r > 0.55 ? c.hyellow : c.hgreen;
|
|
173
|
+
const 눈금칸 = new Set(
|
|
174
|
+
(Array.isArray(눈금) ? 눈금 : [])
|
|
175
|
+
.map((v) => Math.min(cells - 1, Math.max(0, Math.floor(Number(v) * cells))))
|
|
176
|
+
.filter((v) => Number.isFinite(v)),
|
|
177
|
+
);
|
|
178
|
+
let out = '';
|
|
179
|
+
for (let i = 0; i < cells; i++) {
|
|
180
|
+
if (i < filled) out += tone('▰');
|
|
181
|
+
else if (눈금칸.has(i)) out += c.white('┆');
|
|
182
|
+
else out += c.gray('▱');
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
145
187
|
const BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
|
|
146
188
|
|
|
147
189
|
/**
|
|
@@ -175,5 +217,3 @@ export const mark = {
|
|
|
175
217
|
tree: c.gray('└'),
|
|
176
218
|
branch: c.gray('├'),
|
|
177
219
|
};
|
|
178
|
-
|
|
179
|
-
export const COLOR_ON = ON;
|
package/src/ui/approve.js
CHANGED
|
@@ -13,29 +13,33 @@
|
|
|
13
13
|
* 그래서 세 가지를 준다 — 눈에 띄는 글자, 사람 말 이름, 무엇을 묻는지 한 줄.
|
|
14
14
|
*/
|
|
15
15
|
import { c } from './ansi.js';
|
|
16
|
+
import { 말 } from '../i18n/index.js';
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* 이름과 한 줄 설명은 화면 말(i18n)에서 읽는다. 글자와 색은 여기 남긴다 —
|
|
20
|
+
* ⏵⏵ 는 어느 말로도 ⏵⏵ 이고, 노랑은 어느 말로도 노랑이다.
|
|
21
|
+
*
|
|
22
|
+
* 게터로 둔다. 켤 때 한 번 읽어 굳히면 /lang 으로 바꿔도 상태줄의 승인
|
|
23
|
+
* 표시만 옛 말로 남는데, 하필 그게 안전 표시라 제일 안 어긋나야 할 자리다.
|
|
24
|
+
*/
|
|
25
|
+
/*
|
|
26
|
+
* 펼치기(...)로 붙이면 **그 자리에서 값이 굳는다.** 게터를 펼치면 게터가
|
|
27
|
+
* 아니라 그때 읽은 글자가 복사되기 때문이다. 그러면 /lang 으로 바꿔도
|
|
28
|
+
* 승인 표시만 옛 말로 남는데, 하필 그게 안전 표시라 제일 안 어긋나야 할
|
|
29
|
+
* 자리다. defineProperty 로 게터인 채로 붙인다.
|
|
30
|
+
*/
|
|
31
|
+
function 마디(키, 바탕) {
|
|
32
|
+
const 것 = { ...바탕 };
|
|
33
|
+
for (const [자리, 뒤] of [['이름', 'name'], ['짧은이름', 'short'], ['한줄', 'line']]) {
|
|
34
|
+
Object.defineProperty(것, 자리, { get: () => 말(`approve.${키}.${뒤}`), enumerable: true });
|
|
35
|
+
}
|
|
36
|
+
return 것;
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
export const 승인 = {
|
|
18
|
-
auto: {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
짧은이름: '자동',
|
|
22
|
-
한줄: '안 묻고 고칩니다. 되돌리기(/undo)가 안전망입니다',
|
|
23
|
-
색: c.hyellow,
|
|
24
|
-
},
|
|
25
|
-
confirm: {
|
|
26
|
-
글자: '⏵',
|
|
27
|
-
이름: '위험만 확인',
|
|
28
|
-
짧은이름: '위험만',
|
|
29
|
-
한줄: '되돌릴 수 없는 명령만 물어봅니다. 파일은 안 묻고 고칩니다',
|
|
30
|
-
색: c.white,
|
|
31
|
-
},
|
|
32
|
-
strict: {
|
|
33
|
-
글자: '⏸',
|
|
34
|
-
이름: '모두 확인',
|
|
35
|
-
짧은이름: '모두',
|
|
36
|
-
한줄: '파일을 고치거나 명령을 돌리기 전에 매번 물어봅니다',
|
|
37
|
-
색: c.hgreen,
|
|
38
|
-
},
|
|
40
|
+
auto: 마디('auto', { 글자: '⏵⏵', 색: c.hyellow }),
|
|
41
|
+
confirm: 마디('confirm', { 글자: '⏵', 색: c.white }),
|
|
42
|
+
strict: 마디('strict', { 글자: '⏸', 색: c.hgreen }),
|
|
39
43
|
};
|
|
40
44
|
|
|
41
45
|
export const 차례 = ['auto', 'confirm', 'strict'];
|
package/src/ui/inputbox.js
CHANGED
|
@@ -34,12 +34,24 @@
|
|
|
34
34
|
* 값으로 내놓아야 한 칸도 안 어긋나는지 잴 수 있다.
|
|
35
35
|
*/
|
|
36
36
|
import { c, width, cursor, clip } from './ansi.js';
|
|
37
|
+
import { isLocalHost } from '../safety/network.js';
|
|
37
38
|
import { statusLine, contextWarning } from './status.js';
|
|
38
39
|
import { 접어쓰기 } from './wrap.js';
|
|
39
40
|
import { 문구고르기, 걸린시간, 느긋해질때, 문구주기 } from './working.js';
|
|
40
41
|
import { 그림고르기 } from './motion.js';
|
|
41
42
|
import { 최대추천 } from './complete.js';
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 이 연결이 이 컴퓨터 밖으로 나가나.
|
|
46
|
+
*
|
|
47
|
+
* 못 읽는 주소면 **거짓**으로 본다. 여기서 애매한 것을 노랗게 칠해 버리면,
|
|
48
|
+
* 진짜로 밖으로 나갈 때의 노랑이 아무 뜻도 없어진다 — 늘 노란 테두리는
|
|
49
|
+
* 테두리가 회색인 것과 같다.
|
|
50
|
+
*/
|
|
51
|
+
function 바깥으로가나(session) {
|
|
52
|
+
try { return !isLocalHost(new URL(session.conn.base).hostname); } catch { return false; }
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
const 줄끝지움 = '\x1b[K';
|
|
44
56
|
const 위로 = (n) => (n > 0 ? `\x1b[${n}A` : '');
|
|
45
57
|
// 돌아가는 표시를 한 칸 넘기는 주기. 사람 눈에 부드럽고, 다시 그리는 값은 싸다.
|
|
@@ -64,8 +76,23 @@ export const 안쪽최대 = 8;
|
|
|
64
76
|
* @returns {{줄들: string[], 커서: {위: number, 열: number}}}
|
|
65
77
|
* 커서.위 = 마지막 줄에서 몇 줄 위인가, 커서.열 = 1부터 세는 칸
|
|
66
78
|
*/
|
|
67
|
-
export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상태 = '', 경고 = '', 곁말 = '', 일감 = null, 추천 = [] } = {}) {
|
|
79
|
+
export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상태 = '', 경고 = '', 곁말 = '', 일감 = null, 추천 = [], 바깥 = false } = {}) {
|
|
68
80
|
const 칸 = Math.max(20, 폭);
|
|
81
|
+
/*
|
|
82
|
+
* ── 테두리가 곧 경계선이다 ───────────────────────────────────────────
|
|
83
|
+
*
|
|
84
|
+
* 이 상자는 사람이 글을 치는 자리이자, 그 글이 어디까지 가는지의 울타리다.
|
|
85
|
+
* 이 컴퓨터 안으로만 갈 때는 늘 보던 회색 테두리 그대로 두고, 바깥
|
|
86
|
+
* 게이트웨이로 붙어 있을 때만 **노랗게** 그린다.
|
|
87
|
+
*
|
|
88
|
+
* 상태줄 글자 한 칸(⌂/↗)으로도 되지 않느냐 싶지만, 그건 좁으면 밀릴 수 있고
|
|
89
|
+
* 무엇보다 사람이 안 본다. 테두리는 안 볼 수가 없다 — 치는 글을 감싸고 있어서,
|
|
90
|
+
* 눈이 글자를 따라가는 내내 시야에 들어온다. 칸도 한 칸 안 먹는다.
|
|
91
|
+
*
|
|
92
|
+
* 시끄러운 것이 맞고, 그게 뜻이다. 내 소스가 밖으로 나가는 중이라면
|
|
93
|
+
* 화면이 그만큼은 말해 줘야 한다.
|
|
94
|
+
*/
|
|
95
|
+
const 테 = 바깥 ? c.hyellow : c.gray;
|
|
69
96
|
const 안쪽 = Math.max(4, 칸 - 앞머리 - 2); // 오른쪽 ' │' 두 칸
|
|
70
97
|
const 줄들 = [];
|
|
71
98
|
|
|
@@ -73,7 +100,7 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
73
100
|
if (경고) 줄들.push(` ${경고}`);
|
|
74
101
|
|
|
75
102
|
const 가로 = '─'.repeat(칸 - 3);
|
|
76
|
-
줄들.push(
|
|
103
|
+
줄들.push(테(` ╭${가로}╮`));
|
|
77
104
|
|
|
78
105
|
/*
|
|
79
106
|
* 일하는 중.
|
|
@@ -98,7 +125,7 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
98
125
|
const 남 = 일안쪽 - width(왼쪽) - width(오른쪽);
|
|
99
126
|
// 자리가 모자라면 오른쪽(걸린 시간·중단 안내)을 버린다. 왼쪽이 본문이다.
|
|
100
127
|
const 안 = 남 >= 2 ? `${왼쪽}${' '.repeat(남)}${오른쪽}` : 왼쪽;
|
|
101
|
-
줄들.push(` ${
|
|
128
|
+
줄들.push(` ${테('│')} ${안}${' '.repeat(Math.max(0, 일안쪽 - width(안)))} ${테('│')}`);
|
|
102
129
|
|
|
103
130
|
/*
|
|
104
131
|
* 일하는 도중에 미리 쳐 두는 자리.
|
|
@@ -114,11 +141,11 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
114
141
|
const 대기줄들 = 접어쓰기(일감.대기, 안쪽).slice(0, 3);
|
|
115
142
|
for (const [i, 한줄] of 대기줄들.entries()) {
|
|
116
143
|
const 표 = i === 0 ? c.hcyan('❯') : ' ';
|
|
117
|
-
줄들.push(` ${
|
|
144
|
+
줄들.push(` ${테('│')} ${표} ${한줄}${' '.repeat(Math.max(0, 안쪽 - width(한줄)))} ${테('│')}`);
|
|
118
145
|
}
|
|
119
146
|
}
|
|
120
147
|
|
|
121
|
-
줄들.push(
|
|
148
|
+
줄들.push(테(` ╰${가로}╯`));
|
|
122
149
|
if (일감.대기) 줄들.push(` ${c.gray('Enter 를 치면 지금 일이 끝난 뒤에 보냅니다')}`);
|
|
123
150
|
else if (곁말) 줄들.push(` ${c.gray(곁말)}`);
|
|
124
151
|
|
|
@@ -141,9 +168,9 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
141
168
|
for (const [i, 한줄] of 보일줄.entries()) {
|
|
142
169
|
const 표 = i === 0 ? (넘침 ? c.gray('…') : c.hcyan('❯')) : ' ';
|
|
143
170
|
const 남 = 안쪽 - width(한줄);
|
|
144
|
-
줄들.push(` ${
|
|
171
|
+
줄들.push(` ${테('│')} ${표} ${한줄}${' '.repeat(Math.max(0, 남))} ${테('│')}`);
|
|
145
172
|
}
|
|
146
|
-
줄들.push(
|
|
173
|
+
줄들.push(테(` ╰${가로}╯`));
|
|
147
174
|
|
|
148
175
|
/*
|
|
149
176
|
* 자동완성 추천은 상자 **아래**에 놓는다.
|
|
@@ -372,7 +399,6 @@ export class InputBox {
|
|
|
372
399
|
process.stdout.write('\n');
|
|
373
400
|
this.줄중간 = false;
|
|
374
401
|
}
|
|
375
|
-
this.지우기();
|
|
376
402
|
|
|
377
403
|
const { 줄들, 커서: 자리 } = 프레임({
|
|
378
404
|
글,
|
|
@@ -383,12 +409,40 @@ export class InputBox {
|
|
|
383
409
|
곁말: this.곁말,
|
|
384
410
|
일감,
|
|
385
411
|
추천,
|
|
412
|
+
// 테두리 색이 경계선을 말한다. 못 읽는 주소면 평소대로 둔다 —
|
|
413
|
+
// 애매한 것을 노랗게 칠하면, 진짜로 밖으로 나갈 때의 노랑이 안 먹힌다.
|
|
414
|
+
바깥: this.session ? 바깥으로가나(this.session) : false,
|
|
386
415
|
});
|
|
387
416
|
|
|
388
|
-
cursor.hide();
|
|
389
|
-
process.stdout.write(줄들.map((l) => l + 줄끝지움).join('\n') + '\n');
|
|
390
417
|
/*
|
|
391
|
-
*
|
|
418
|
+
* ── 지우지 않고 덮어쓴다. 그리고 한 번에 내보낸다 ────────────────────
|
|
419
|
+
*
|
|
420
|
+
* 예전에는 이랬다: 지우기() 로 상자를 싹 지우고 → 새로 그리고 → 커서를
|
|
421
|
+
* 되돌린다. 세 번을 따로 내보냈다. 터미널은 받는 대로 그리므로 **그 사이가
|
|
422
|
+
* 사람 눈에 보인다** — 상자가 한 번 비었다가 다시 차는 것이 백스페이스를
|
|
423
|
+
* 누를 때마다 반복되어, 화면이 깜빡 꺼지는 것처럼 느껴졌다. 글자를 지울
|
|
424
|
+
* 때는 한 번에 한 글자씩 여러 번 누르므로 특히 심했다.
|
|
425
|
+
*
|
|
426
|
+
* 그래서 두 가지를 바꿨다.
|
|
427
|
+
* 1) 지우고 다시 그리지 않는다. 상자 맨 윗줄로 올라가 **덮어쓴다.**
|
|
428
|
+
* 줄 끝마다 줄끝지움이 붙으므로 전에 있던 글자가 남을 자리가 없다.
|
|
429
|
+
* 빈 화면이 스쳐 가는 순간 자체가 사라진다.
|
|
430
|
+
* 2) 지우기·그리기·커서 되돌리기를 한 덩이로 묶어 **한 번만** 쓴다.
|
|
431
|
+
* 중간 상태가 화면에 못 나간다.
|
|
432
|
+
*
|
|
433
|
+
* 상자가 전보다 짧아졌을 때만 남는 줄이 생긴다. 그때만 그 줄을 지운다.
|
|
434
|
+
*/
|
|
435
|
+
const 이전줄 = this.그린줄;
|
|
436
|
+
// 커서는 상자 아래에서 커서위 번째 줄에 있다. 맨 윗줄까지 남은 거리.
|
|
437
|
+
const 윗줄까지 = 이전줄 ? Math.max(0, 이전줄 - 1 - this.커서위) : 0;
|
|
438
|
+
const 남은줄 = Math.max(0, 이전줄 - 줄들.length);
|
|
439
|
+
|
|
440
|
+
let 나갈것 = cursor.숨김 + '\r' + 위로(윗줄까지);
|
|
441
|
+
나갈것 += 줄들.map((l) => l + 줄끝지움).join('\n') + '\n';
|
|
442
|
+
for (let i = 0; i < 남은줄; i++) 나갈것 += 줄끝지움 + '\n';
|
|
443
|
+
/*
|
|
444
|
+
* 방금 찍은 마지막 줄 **다음** 줄에 커서가 있다(남은 줄을 지웠으면 그만큼
|
|
445
|
+
* 더 아래). 거기서 되짚어 올라간다.
|
|
392
446
|
*
|
|
393
447
|
* 오른쪽으로 옮길 때 한 칸이 어긋나기 쉽다. \r 은 커서를 1칸째에 놓고,
|
|
394
448
|
* \x1b[nC 는 거기서 n칸 **더** 간다 — 그래서 1부터 세는 목표 칸으로
|
|
@@ -396,8 +450,10 @@ export class InputBox {
|
|
|
396
450
|
* 백스페이스가 엉뚱한 자리를 지우는 것처럼 보인다.
|
|
397
451
|
*/
|
|
398
452
|
const 오른쪽 = Math.max(0, 자리.열 - 1);
|
|
399
|
-
|
|
400
|
-
cursor
|
|
453
|
+
나갈것 += 위로(남은줄 + 자리.위 + 1) + '\r' + (오른쪽 ? `\x1b[${오른쪽}C` : '');
|
|
454
|
+
나갈것 += cursor.보임;
|
|
455
|
+
process.stdout.write(나갈것);
|
|
456
|
+
|
|
401
457
|
this.그린줄 = 줄들.length;
|
|
402
458
|
this.커서위 = 자리.위;
|
|
403
459
|
}
|
package/src/ui/intro.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 켤 때 한 번 도는 글자 모션.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇을 보여 주나 ────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 사람은 `deel` 이라고 치고 들어온다. 그 글자가 그 자리에서 **deel-local** 로
|
|
7
|
+
* 자라고, 그 아래로 선이 그어지며 닫힌다.
|
|
8
|
+
*
|
|
9
|
+
* deel▏
|
|
10
|
+
* deel-lo▏
|
|
11
|
+
* deel-local
|
|
12
|
+
* deel-local
|
|
13
|
+
* ─────
|
|
14
|
+
* deel-local
|
|
15
|
+
* ────────── 이 컴퓨터 안에서만
|
|
16
|
+
*
|
|
17
|
+
* 꾸미기만은 아니다. 이 프로그램이 하는 말이 딱 하나인데 — **소스가 이 컴퓨터
|
|
18
|
+
* 밖으로 안 나간다** — 그 말을 켜는 첫 0.5초에 그림으로 한 번 하는 것이다.
|
|
19
|
+
* 자라는 글자가 이름이고, 닫히는 선이 경계다. 바깥 게이트웨이로 붙어 있으면
|
|
20
|
+
* 같은 선이 노랗게 그어진다. 그러면 사람이 그 자리에서 안다.
|
|
21
|
+
*
|
|
22
|
+
* ── 왜 이 글자들만 쓰나 ─────────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* 이모지를 안 쓴다. 터미널마다 한 칸이 되기도 두 칸이 되기도 해서, 다시
|
|
25
|
+
* 그릴 때 지워야 할 폭이 어긋나고 앞 그림이 조각으로 남는다. ● ○ 같은
|
|
26
|
+
* 기호도 동아시아 로캘에서 두 칸으로 잡히는 것이 섞여 있다.
|
|
27
|
+
* 여기서는 ASCII 와 '─' 만 쓴다 — 어디서나 한 칸이다. (ui/motion.js 와 같은 이유)
|
|
28
|
+
*
|
|
29
|
+
* ── 잘 때를 아는 것 ─────────────────────────────────────────────────────
|
|
30
|
+
*
|
|
31
|
+
* 파이프·CI·기록으로 나갈 때는 **한 장만** 찍고 만다. 다시 그리기는 커서를
|
|
32
|
+
* 위로 올려 지우는 것인데, 앞자리가 터미널이 아니면 그 제어문자가 글에
|
|
33
|
+
* 그대로 섞여 남는다. 로그를 열었더니 `\x1b[2A` 가 널려 있는 꼴이 된다.
|
|
34
|
+
*/
|
|
35
|
+
import { c, cursor, width } from './ansi.js';
|
|
36
|
+
import { 말 } from '../i18n/index.js';
|
|
37
|
+
|
|
38
|
+
const 앞 = 'deel';
|
|
39
|
+
const 꼬리 = '-local';
|
|
40
|
+
const 이름폭 = 앞.length + 꼬리.length; // 10
|
|
41
|
+
const 들여 = ' ';
|
|
42
|
+
|
|
43
|
+
/** 모션을 끄는 스위치. 그림 돌림표(motion.js)와 같은 스위치를 함께 본다. */
|
|
44
|
+
export const 끔 = () => !!(process.env.DEEL_NO_INTRO || process.env.DEEL_NO_MOTION);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 한 틀에 얼마나 머무나.
|
|
48
|
+
*
|
|
49
|
+
* 다 합쳐 1초를 안 넘긴다. 켤 때마다 보는 것이라, 두 번째부터는 기다림이 된다.
|
|
50
|
+
* 멋있는 것보다 **짧은 것**이 낫다 — 하루에 열 번 켜는 사람이 기준이다.
|
|
51
|
+
*/
|
|
52
|
+
export const 틀주기 = 38;
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* 틀 차례.
|
|
56
|
+
*
|
|
57
|
+
* 값으로 적어 둔다. 그려 놓고 눈으로만 맞추면 나중에 아무도 못 고치고,
|
|
58
|
+
* 검사도 '무엇이 보여야 하나' 를 말할 수가 없다.
|
|
59
|
+
*/
|
|
60
|
+
function 차례짓기() {
|
|
61
|
+
const out = [];
|
|
62
|
+
// 1) 꼬리가 한 글자씩 자란다. 커서가 뒤를 따라간다.
|
|
63
|
+
for (let i = 0; i <= 꼬리.length; i++) out.push({ 글: i, 선: 0, 곁: false, 커서: true });
|
|
64
|
+
// 2) 커서가 멎는다. 여기서 한 박자 쉬는 것이 다음 선을 눈에 띄게 한다.
|
|
65
|
+
out.push({ 글: 꼬리.length, 선: 0, 곁: false, 커서: false });
|
|
66
|
+
// 3) 아래로 선이 그어진다 — 경계가 닫히는 자리다.
|
|
67
|
+
for (let n = 2; n <= 이름폭; n += 2) out.push({ 글: 꼬리.length, 선: n, 곁: false, 커서: false });
|
|
68
|
+
// 4) 닫히고 나서야 곁말이 붙는다. 순서가 뜻이다 — 닫힌 다음에 하는 말이라야 믿을 만하다.
|
|
69
|
+
out.push({ 글: 꼬리.length, 선: 이름폭, 곁: true, 커서: false });
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const 차례 = 차례짓기();
|
|
74
|
+
|
|
75
|
+
/** 몇 틀짜리인가. */
|
|
76
|
+
export const 틀수 = 차례.length;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 한 틀을 줄들로. 언제나 **두 줄**이다.
|
|
80
|
+
*
|
|
81
|
+
* 줄 수가 틀마다 달라지면 다시 그릴 때 몇 줄을 올려야 하는지가 틀마다 달라지고,
|
|
82
|
+
* 한 번 어긋나면 앞 그림이 화면에 조각으로 남는다. 그래서 아직 안 그린 줄도
|
|
83
|
+
* 빈 줄로 자리를 잡아 둔다.
|
|
84
|
+
*
|
|
85
|
+
* @param {number} n 몇 번째 틀 (범위를 벗어나면 마지막 틀)
|
|
86
|
+
* @param {object} o
|
|
87
|
+
* @param {boolean} [o.바깥] 바깥으로 나가는 연결인가
|
|
88
|
+
* @param {string} [o.곁말] 선 옆에 붙는 한 줄
|
|
89
|
+
* @returns {string[]} 두 줄
|
|
90
|
+
*/
|
|
91
|
+
export function 틀(n, { 바깥 = false, 곁말 = '' } = {}) {
|
|
92
|
+
const f = 차례[Math.max(0, Math.min(차례.length - 1, Math.trunc(Number(n) || 0)))];
|
|
93
|
+
const 색 = 바깥 ? c.hyellow : c.hgreen;
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* 방금 들어온 글자만 굵게, 앞엣것은 제 색으로 가라앉힌다.
|
|
97
|
+
*
|
|
98
|
+
* 다 같은 밝기로 그리면 글자가 '길어지는' 것으로만 보인다. 마지막 한 글자가
|
|
99
|
+
* 한 틀 동안 도드라졌다 가라앉아야 **찍히는** 것으로 보인다 — 사람이 자판을
|
|
100
|
+
* 두드릴 때 눈이 좇는 것이 그 마지막 글자라서 그렇다.
|
|
101
|
+
*/
|
|
102
|
+
const 자란글 = 꼬리.slice(0, f.글);
|
|
103
|
+
const 앞선글 = 색(자란글.slice(0, -1));
|
|
104
|
+
const 방금글 = 자란글 ? c.bold(색(자란글.slice(-1))) : '';
|
|
105
|
+
const 남은칸 = ' '.repeat(이름폭 - 앞.length - 자란글.length);
|
|
106
|
+
// 커서는 글자 자리를 안 먹는다 — 남은 칸 안에서 그린다. 그래야 글이 안 밀린다.
|
|
107
|
+
const 커서글 = f.커서 ? c.gray('▏') : ' ';
|
|
108
|
+
// 다 자란 뒤에는 굵기를 뺀다. 굵은 채로 굳으면 '아직 찍히는 중' 으로 보인다.
|
|
109
|
+
const 이름글 = f.커서 ? `${앞선글}${방금글}` : 색(자란글);
|
|
110
|
+
const 첫줄 = `${들여}${c.bold(c.white(앞))}${이름글}${커서글}${남은칸.slice(1)}`;
|
|
111
|
+
|
|
112
|
+
const 선 = f.선 ? 색('─'.repeat(f.선)) : '';
|
|
113
|
+
const 곁 = f.곁 && 곁말 ? ` ${c.gray(곁말)}` : '';
|
|
114
|
+
const 둘째줄 = f.선 ? `${들여}${선}${곁}` : '';
|
|
115
|
+
|
|
116
|
+
return [첫줄, 둘째줄];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 다 돈 마지막 모습. 모션을 껐을 때 이것만 찍는다. */
|
|
120
|
+
export function 마지막틀(opts = {}) { return 틀(틀수 - 1, opts); }
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 실제로 돌린다.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} o
|
|
126
|
+
* @param {(s:string)=>void} [o.쓰기] 기본은 stdout. 검사가 갈아 끼운다.
|
|
127
|
+
* @param {boolean} [o.움직임] 거짓이면 마지막 한 장만
|
|
128
|
+
* @param {number} [o.주기]
|
|
129
|
+
* @param {boolean} [o.바깥]
|
|
130
|
+
* @param {string} [o.곁말]
|
|
131
|
+
* @param {(ms:number)=>Promise<void>} [o.쉬기] 검사에서 진짜로 안 기다리게
|
|
132
|
+
* @returns {Promise<number>} 그린 틀 수 (한 장만 찍었으면 1)
|
|
133
|
+
*/
|
|
134
|
+
export async function 보이기({
|
|
135
|
+
쓰기 = (s) => process.stdout.write(s),
|
|
136
|
+
움직임 = null,
|
|
137
|
+
주기 = 틀주기,
|
|
138
|
+
바깥 = false,
|
|
139
|
+
곁말 = '',
|
|
140
|
+
쉬기 = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
141
|
+
} = {}) {
|
|
142
|
+
const 옵션 = { 바깥, 곁말 };
|
|
143
|
+
const 돌릴까 = 움직임 ?? (!끔() && !!process.stdout.isTTY);
|
|
144
|
+
|
|
145
|
+
if (!돌릴까) {
|
|
146
|
+
// 한 장만. 커서를 안 올리므로 파이프·기록에 제어문자가 안 섞인다.
|
|
147
|
+
for (const l of 마지막틀(옵션)) 쓰기(`${l}\n`);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
cursor.hide();
|
|
152
|
+
try {
|
|
153
|
+
for (let i = 0; i < 틀수; i++) {
|
|
154
|
+
const 줄들 = 틀(i, 옵션);
|
|
155
|
+
// 첫 틀은 자리를 새로 잡고, 그 뒤로는 그 두 줄만 지우고 다시 쓴다.
|
|
156
|
+
const 앞머리 = i === 0 ? '' : '\x1b[2A';
|
|
157
|
+
쓰기(`${앞머리}\x1b[2K${줄들[0]}\n\x1b[2K${줄들[1]}\n`);
|
|
158
|
+
if (i < 틀수 - 1) await 쉬기(주기);
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
// 무슨 일이 있어도 커서는 되돌린다. 여기서 안 되돌리면 그 뒤로 사람이
|
|
162
|
+
// 치는 글에 커서가 안 보인다 — 프로그램이 멈춘 것처럼 보인다.
|
|
163
|
+
cursor.show();
|
|
164
|
+
}
|
|
165
|
+
return 틀수;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 곁말은 부르는 쪽이 정한다. 여기 있는 것은 기본값이다. */
|
|
169
|
+
export function 기본곁말(바깥) {
|
|
170
|
+
return 말(바깥 ? 'intro.outside' : 'intro.inside');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 폭 계산이 어긋나지 않는지 부르는 쪽에서 재 볼 수 있게 열어 둔다.
|
|
174
|
+
export const 이름너비 = () => width(앞 + 꼬리);
|
package/src/ui/level.js
CHANGED
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
// 치면 그대로 먹는다. 목록에 안 띄울 뿐이다 — 처음 켠 사람에게 명령 열여덟
|
|
11
11
|
// 개를 들이밀면 아무것도 못 고른다.
|
|
12
12
|
|
|
13
|
+
import { 말 } from '../i18n/index.js';
|
|
14
|
+
|
|
13
15
|
export const LEVELS = {
|
|
14
16
|
쉬움: {
|
|
15
17
|
id: '쉬움',
|
|
16
18
|
en: 'beginner',
|
|
17
|
-
name
|
|
18
|
-
hint
|
|
19
|
+
get name() { return 말('level.beginner.name'); },
|
|
20
|
+
get hint() { return 말('level.beginner.hint'); },
|
|
19
21
|
// 목록에 띄울 것. 나머지는 쳐도 먹지만 안 보인다.
|
|
20
22
|
show: [
|
|
21
23
|
'help', 'work', 'auto', 'code', 'plan', 'ask',
|
|
@@ -23,7 +25,11 @@ export const LEVELS = {
|
|
|
23
25
|
// 가 되는데, 초보일수록 그 원인을 못 찾는다. 명령 하나로 끝나는 문제다.
|
|
24
26
|
// diff 는 초보에게 특히 필요하다. auto 모드는 안 물어보고 고치니,
|
|
25
27
|
// '무엇이 바뀌었나' 를 볼 통로가 없으면 되돌릴지 말지도 못 정한다.
|
|
26
|
-
|
|
28
|
+
// bell 도 초보 목록에 둔다. 로컬 모델은 한 턴이 몇 분씩 걸려서 다들 다른 창으로
|
|
29
|
+
// 가는데, 알림이 있는 줄 모르면 그 몇 분을 화면만 보고 앉아 있게 된다.
|
|
30
|
+
// lang 은 반드시 초보 목록에 있어야 한다. 화면이 온통 못 읽는 말인 사람에게
|
|
31
|
+
// '전부 보려면 /level developer' 라고 적어 봐야 그 줄도 못 읽는다.
|
|
32
|
+
'model', 'ctx', 'scan', 'diff', 'undo', 'bell', 'lang', 'clear', 'sessions', 'cost', 'level', 'exit',
|
|
27
33
|
],
|
|
28
34
|
// 첫 실행에서 훑어 추천까지 해 준다
|
|
29
35
|
autoScan: true,
|
|
@@ -34,8 +40,8 @@ export const LEVELS = {
|
|
|
34
40
|
개발자: {
|
|
35
41
|
id: '개발자',
|
|
36
42
|
en: 'developer',
|
|
37
|
-
name
|
|
38
|
-
hint
|
|
43
|
+
get name() { return 말('level.developer.name'); },
|
|
44
|
+
get hint() { return 말('level.developer.hint'); },
|
|
39
45
|
show: null, // null = 전부
|
|
40
46
|
autoScan: false,
|
|
41
47
|
plainErrors: false,
|