deel-local-cli 1.5.0 → 1.5.2

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/src/ui/office.js CHANGED
@@ -28,7 +28,40 @@
28
28
  * 그래서 상자가 바뀐 줄만 보낸다(inputbox 의 그리기). 그 덕에 사무실을 켜도
29
29
  * 터미널로 나가는 양이 켜기 전과 비슷하다.
30
30
  */
31
- import { 색256가능 } from './ansi.js';
31
+ import { 느긋해질때 } from './working.js';
32
+
33
+ /*
34
+ * 방을 그릴 만큼 색이 되는 화면인가.
35
+ *
36
+ * ansi.js 의 색256가능() 은 여기서 못 쓴다. 그것이 보는 것은 TERM 이 dumb
37
+ * 인지 하나뿐인데, 상자 자체가 이미 dumb 를 거부한다(screen.js 상자쓸까).
38
+ * 즉 상자 안에서는 **늘 참**이라 있으나 마나 한 관문이었고, 16색짜리 콘솔에서도
39
+ * 사무실이 켜졌다.
40
+ *
41
+ * 그래서 여기서는 반대로 **확실할 때만** 참이라고 한다. 틀렸을 때 잃는 것이
42
+ * 양쪽으로 다르기 때문이다 — 못 켜면 그냥 사무실이 안 보이지만, 잘못 켜면
43
+ * 색 번호가 글자로 쏟아지거나 열두 줄이 갈색 죽이 된다. 모르면 안 켠다.
44
+ */
45
+ function 색충분한가() {
46
+ // 색을 끈 사람에게 방을 그리면 안 된다. 방은 색이 전부다.
47
+ if (process.env.NO_COLOR !== undefined) return false;
48
+ if (process.env.FORCE_COLOR === '1') return true;
49
+
50
+ const term = String(process.env.TERM ?? '');
51
+ if (term === 'dumb') return false;
52
+
53
+ const 컬러텀 = String(process.env.COLORTERM ?? '').toLowerCase();
54
+ if (컬러텀 === 'truecolor' || 컬러텀 === '24bit') return true;
55
+ if (/256/.test(term)) return true;
56
+
57
+ // 윈도우는 Node 20 이 Win10 이상을 요구하고, 거기 콘솔은 256색을 한다.
58
+ if (process.platform === 'win32') return true;
59
+ if (process.env.WT_SESSION) return true; // 윈도우 터미널
60
+ if (process.env.TERM_PROGRAM) return true; // iTerm · VS Code · Apple Terminal
61
+ if (process.env.ConEmuANSI === 'ON') return true;
62
+
63
+ return false;
64
+ }
32
65
 
33
66
  const ESC = '\x1b';
34
67
  const 투명 = -1;
@@ -45,7 +78,7 @@ const C = {
45
78
  화면: 45, 화면꺼짐: 239, 화면글: 123, 살: 223,
46
79
  머리: [52, 240, 94, 130], 옷: [67, 131, 71, 97],
47
80
  밤: 17, 별: 226, 낮: 111, 구름: 255,
48
- 종이: 255, 종이어둠: 250, 쪽지: 227, 쪽지끝: 120, 초록: 46,
81
+ 종이: 255, 종이어둠: 250, 쪽지: 227, 쪽지끝: 120, 초록: 46, 넘침: 208,
49
82
  };
50
83
 
51
84
  // ── 픽셀 판 ─────────────────────────────────────────────────────────────
@@ -170,13 +203,22 @@ export function 사무실줄들(상태, 폭) {
170
203
  const 그림 = ['FFFFFFFFFFFFFFFFFF'];
171
204
  for (let r = 0; r < 3; r++) 그림.push(`F${'W'.repeat(16)}F`);
172
205
  그림.push('FFFFFFFFFFFFFFFFFF');
206
+ /*
207
+ * 쪽지는 다섯 장뿐이라 열 개짜리 목록은 눌러 담아야 한다. 그런데 끝난
208
+ * 것부터 채우면 「열 개 다 끝남」과 「열 개 끝나고 하나 남음」이 똑같이
209
+ * 초록 다섯 장으로 나온다 — 남은 일이 화면에서 사라진다.
210
+ *
211
+ * 그래서 남은 일이 하나라도 있으면 **노란 쪽지 한 장은 반드시 남긴다.**
212
+ * 눌러 담더라도 '아직 안 끝났다' 는 사실만은 안 지운다.
213
+ */
173
214
  const 총 = Math.min(5, 할일 + 끝난일);
215
+ const 초록수 = 할일 > 0 ? Math.min(끝난일, 총 - 1) : Math.min(끝난일, 총);
174
216
  for (let n = 0; n < 총; n++) {
175
217
  const x = 2 + n * 3;
176
218
  for (const r of [1, 2, 3]) {
177
219
  const 줄 = 그림[r].split('');
178
- 줄[x] = n < 끝난일 ? 'D' : 'N';
179
- 줄[x + 1] = n < 끝난일 ? 'D' : 'N';
220
+ 줄[x] = n < 초록수 ? 'D' : 'N';
221
+ 줄[x + 1] = n < 초록수 ? 'D' : 'N';
180
222
  그림[r] = 줄.join('');
181
223
  }
182
224
  }
@@ -185,14 +227,23 @@ export function 사무실줄들(상태, 폭) {
185
227
 
186
228
  // 서버 불빛 14×5 — 모델 호출. 오른쪽 벽 끝.
187
229
  {
230
+ /*
231
+ * 불은 열두 개뿐이다. 여섯 번 부른 것과 백 번 부른 것이 똑같이 다 켜진
232
+ * 채로 나오면, 이 불은 어느 순간부터 아무것도 안 말하게 된다.
233
+ *
234
+ * 다 차면 맨 끝 불을 다른 색으로 바꾼다 — '여기서부터는 더 있다' 는 뜻이다.
235
+ * 없는 눈금을 지어내지 않으면서 넘쳤다는 것만 알린다.
236
+ */
237
+ const 켠수 = Math.min(12, 부른횟수 * 2);
238
+ const 넘쳤나 = 부른횟수 * 2 > 12;
188
239
  const 그림 = ['RRRRRRRRRRRRRR'];
189
240
  for (let r = 0; r < 3; r++) {
190
241
  let 줄 = 'R';
191
- for (let k = 0; k < 12; k++) 줄 += k < Math.min(12, 부른횟수 * 2) ? 'L' : 'l';
242
+ for (let k = 0; k < 12; k++) 줄 += (넘쳤나 && k === 11) ? 'O' : (k < 켠수 ? 'L' : 'l');
192
243
  그림.push(줄 + 'R');
193
244
  }
194
245
  그림.push('RRRRRRRRRRRRRR');
195
- p.올리기(가로 - 15, 0, 그림, { R: C.검정, L: C.초록, l: C.쇠진 });
246
+ p.올리기(가로 - 15, 0, 그림, { R: C.검정, L: C.초록, l: C.쇠진, O: C.넘침 });
196
247
  }
197
248
 
198
249
  // 창문 — 벽에 남는 자리만큼. 컨텍스트가 차면 밤이 된다.
@@ -236,6 +287,15 @@ export function 사무실줄들(상태, 폭) {
236
287
  */
237
288
  const 왼쪽 = 15;
238
289
  const 자리수 = Math.max(1, Math.floor((가로 - 왼쪽 - 2) / (자리폭 + 1)));
290
+ /*
291
+ * 자리보다 사람이 많으면 그냥 잘려 나갔다 — 좁은 화면에서 둘이 도는 것과
292
+ * 셋이 도는 것이 **똑같은 그림**이 됐다. 이 화면의 규칙이 '전부 진짜 숫자'
293
+ * 인데 그러면 자리 수만 거짓이 된다.
294
+ *
295
+ * 못 그리는 것을 억지로 그릴 수는 없으니, 대신 마지막 자리의 책상 불을
296
+ * 넘침 색으로 바꿔 '여기 뒤로 더 있다' 를 남긴다.
297
+ */
298
+ const 넘친자리 = 일꾼들.length > 자리수;
239
299
  const 짝 = Math.floor(틱 / 5) % 2 === 0;
240
300
  for (let i = 0; i < 자리수; i++) {
241
301
  const x = 왼쪽 + i * (자리폭 + 1);
@@ -246,6 +306,8 @@ export function 사무실줄들(상태, 폭) {
246
306
  : 그사람.갈래 === '읽기' ? (짝 ? '읽기A' : '읽기B')
247
307
  : 그사람.갈래 === '졸기' ? '졸기' : '대기';
248
308
  p.올리기(x, y, 자세들[자세], 자리색(자세, C.머리[i % 4], C.옷[i % 4]));
309
+ // 마지막 자리인데 뒤에 더 있으면, 책상 모서리에 넘침 표시를 남긴다.
310
+ if (넘친자리 && i === 자리수 - 1) p.칠하기(x + 자리폭 - 2, y + 자리높이 - 3, 2, 2, C.넘침);
249
311
  // 조는 사람 머리 위 z
250
312
  if (자세 === '졸기' && 짝) p.칠하기(x + 13, y + 2, 2, 1, C.종이);
251
313
  // 고친 파일은 책상 위 종이로
@@ -265,9 +327,22 @@ export function 사무실줄들(상태, 폭) {
265
327
  * · 그림을 끈 사람 (DEEL_NO_MOTION) — 화면 읽기 프로그램을 쓰는 자리다
266
328
  * · 화면이 낮은 터미널 — 사무실 12줄 + 상자 5줄이면 대화가 들어갈 자리가
267
329
  * 없다. 사무실을 보려고 도구를 못 쓰게 되는 것은 앞뒤가 바뀐 것이다.
330
+ * · 화면이 **좁은** 터미널 — 아래 최소폭 참고. 이건 안 지키면 화면이 깨진다.
268
331
  */
269
332
  export const 최소높이 = 28;
270
333
 
334
+ /*
335
+ * 방은 늘 60칸을 쓴다(가로 = Math.max(60, 폭)). 더 좁게 그리면 책상과 사람이
336
+ * 뭉개져서 방으로 안 보인다 — 줄여야 할 것은 사람이 아니라 여백인데, 60칸이
337
+ * 여백을 다 뺀 자리다.
338
+ *
339
+ * 그래서 60칸보다 좁은 터미널에서는 **안 켠다**. 켜면 열두 줄이 스물네 줄로
340
+ * 접히는데 상자는 열두 줄로 세고 그만큼만 커서를 올린다. 어긋난 자리에 다음
341
+ * 판을 덮어써서 대화가 통째로 지워진다 — 50칸에서 200판을 돌리면 테두리도
342
+ * 대화도 남지 않는다. 좁으면 그냥 안 보여 주는 편이 낫다.
343
+ */
344
+ export const 최소폭 = 60;
345
+
271
346
  /**
272
347
  * 사람이 켜 달라고 했나 (화면이 되는지는 안 본다).
273
348
  *
@@ -280,10 +355,11 @@ export function 켜달라했나() {
280
355
  return v === '1' || v === 'on' || v === '켬' || v === '사무실';
281
356
  }
282
357
 
283
- export function 켜나(rows = process.stdout.rows) {
358
+ export function 켜나(rows = process.stdout.rows, cols = process.stdout.columns) {
284
359
  if (!켜달라했나()) return false;
285
360
  if (process.env.DEEL_NO_MOTION) return false;
286
- if (!색256가능()) return false;
361
+ if (!색충분한가()) return false;
362
+ if ((cols ?? 0) < 최소폭) return false;
287
363
  return (rows ?? 0) >= 최소높이;
288
364
  }
289
365
 
@@ -311,13 +387,34 @@ export function 상태만들기(session, 일감, 틱 = 0, 할일들 = [], 도는
311
387
  */
312
388
  const 일꾼들 = [];
313
389
  if (일감) {
314
- const 오래됐나 = 일감.갈래 === '느긋';
390
+ /*
391
+ * 오래 끌면 존다.
392
+ *
393
+ * 여기서 직접 재야 한다. 부르는 쪽(inputbox)이 넘겨 주는 일감.갈래 는
394
+ * '생각'·'답'·'하위'·'접기'·도구이름 중 하나일 뿐, **'느긋' 이 되는
395
+ * 길이 없다.** 상자 안 돌림표는 자기 자리에서 따로 재고 있었으므로,
396
+ * 문서에 적어 둔 '45초 넘으면 존다' 가 방에서는 한 번도 일어나지
397
+ * 않았다 — 졸기 자세 코드가 통째로 죽어 있었다.
398
+ */
399
+ const 지난 = Number.isFinite(일감.시작) ? Date.now() - 일감.시작 : 0;
400
+ const 오래됐나 = 일감.갈래 === '느긋' || 지난 >= 느긋해질때;
315
401
  const 갈래 = 오래됐나 ? '졸기'
316
402
  : ['읽기', '웹'].includes(일감.갈래) ? '읽기'
317
403
  : ['쓰기', '명령', '답'].includes(일감.갈래) ? '쓰기' : '대기';
318
- // 하위 작업이 돌면 그만큼 자리가 더 찬다. 부모 몫 하나는 늘 있다.
319
- const = 1 + Math.max(0, 도는하위);
320
- for (let i = 0; i < 수; i++) 일꾼들.push({ 갈래 });
404
+ /*
405
+ * 하위 작업이 돌면 그만큼 자리가 더 찬다. 부모 몫 하나는 늘 있다.
406
+ *
407
+ * 자세는 **부모 것만** 진짜다. 하위가 지금 읽는 중인지 쓰는 중인지는
408
+ * 여기까지 안 올라온다 — 세는 것은 개수뿐이다. 그런데 예전에는 그 자리에도
409
+ * 부모의 자세를 그대로 복사해 놨다. 부모가 자판을 두드리면 읽고 있던
410
+ * 하위까지 자판을 두드렸다. 자리 수는 진짜인데 자세는 지어낸 것이라,
411
+ * 이 화면의 규칙(전부 진짜 숫자)에 어긋나는 유일한 자리였다.
412
+ *
413
+ * 그래서 하위 자리는 '대기' 로 앉힌다. 모르는 것을 그럴듯하게 채우느니
414
+ * 모른다고 두는 편이 낫다 — 자리가 찼다는 사실은 그대로 보인다.
415
+ */
416
+ 일꾼들.push({ 갈래 });
417
+ for (let i = 0; i < Math.max(0, 도는하위); i++) 일꾼들.push({ 갈래: '대기' });
321
418
  }
322
419
 
323
420
  return {
package/src/ui/status.js CHANGED
@@ -408,7 +408,15 @@ function 급머리말(session) {
408
408
  }));
409
409
  }
410
410
 
411
- export function headerLines(session, found) {
411
+ /**
412
+ * 켤 때 한 번 보여 주는 머리말.
413
+ *
414
+ * @param {boolean} 상자쓰나 입력 상자를 쓰는 화면인가. Shift+Tab · Alt+Enter ·
415
+ * 줄 끝 백틱은 전부 상자 안에서만 도는 것이라(repl.js 의 `if (상자쓰나)`),
416
+ * 줄 화면(`--no-tui`·파이프·CI)에서 이 줄들을 그대로 찍으면 **되지도 않는
417
+ * 조작법을 알려 주는 셈**이 된다. 안 되는 것을 알려 주는 안내는 없느니만 못하다.
418
+ */
419
+ export function headerLines(session, found, 상자쓰나 = true) {
412
420
  const conn = session.conn;
413
421
  const 능력 = [
414
422
  conn.streaming ? c.green(말('head.streaming')) : c.gray(말('head.noStreaming')),
@@ -463,9 +471,11 @@ export function headerLines(session, found) {
463
471
  // 켤 때 한 번은 사람 말로 알려 준다. 상태줄의 `⏵⏵ 자동 승인` 이 무슨
464
472
  // 뜻인지 여기서 한 번 읽고 나면 그 다음부터는 글자만 봐도 안다.
465
473
  `${c.gray(자리(말('head.approve')))}${승인표시(session.mode)} ${c.gray('— ' + 승인고르기(session.mode).한줄)}`,
466
- `${빈자리}${c.gray(말('head.shiftTab'))}${c.gray(말('head.shiftTabHint'))}${c.gray(말('head.tabHint'))}`,
467
- `${빈자리}${c.gray(말('head.newlineHint'))}`,
468
474
  ];
475
+ if (상자쓰나) {
476
+ lines.push(`${빈자리}${c.gray(말('head.shiftTab'))}${c.gray(말('head.shiftTabHint'))}${c.gray(말('head.tabHint'))}`);
477
+ lines.push(`${빈자리}${c.gray(말('head.newlineHint'))}`);
478
+ }
469
479
  if (found.skills.length || found.commands.length) {
470
480
  lines.push(`${c.gray(자리(말('head.thisPC')))}${c.white(말('head.skills', { n: found.skills.length }))}${c.gray(' · ')}${c.white(말('head.commands', { n: found.commands.length }))}${c.gray(' · ')}${c.white(말('head.plugins', { n: found.plugins.length }))}`);
471
481
  }
package/src/ui/wrap.js CHANGED
@@ -4,6 +4,39 @@
4
4
  // 여기로 옮긴다 — 입력 상자가 긴 입력을 접을 때 같은 것이 필요하다.
5
5
  import { width } from './ansi.js';
6
6
 
7
+ /*
8
+ * i 자리에서 **눈에 보이는 한 덩이**를 통째로 떼어 온다.
9
+ *
10
+ * 글자를 s[i] 로 한 칸씩 읽으면 UTF-16 코드 단위로 잘린다. 😀 같은 글자는
11
+ * 두 칸을 차지하므로 하필 그 사이에서 줄이 접히면 반쪽짜리 서로게이트가
12
+ * 양쪽 줄에 하나씩 남는다 — 접어쓰기("a😀", 2) 가 ["a\ud83d","\ude00"] 를
13
+ * 내놨다. 화면에는 물음표 두 개로 찍히고, 폭 계산도 그때부터 어긋난다.
14
+ *
15
+ * 그래서 코드 포인트로 읽고, 뒤에 붙는 것들(살색 조절·변형 선택자·결합
16
+ * 부호·ZWJ 로 이어붙인 다음 글자)까지 한 덩이로 본다. 완전한 문자소 분할은
17
+ * 아니지만 — 그건 표가 있어야 한다 — 실제로 깨지던 자리는 이걸로 다 막힌다.
18
+ */
19
+ function 한덩이(s, i) {
20
+ let 끝 = i + String.fromCodePoint(s.codePointAt(i)).length;
21
+ for (;;) {
22
+ if (끝 >= s.length) return s.slice(i, 끝);
23
+ const cp = s.codePointAt(끝);
24
+ // ZWJ 는 다음 글자까지 끌고 온다 (👨‍👩‍👧 같은 이어붙인 그림글자)
25
+ if (cp === 0x200d) {
26
+ const 다음 = 끝 + 1;
27
+ if (다음 >= s.length) return s.slice(i, 끝);
28
+ 끝 = 다음 + String.fromCodePoint(s.codePointAt(다음)).length;
29
+ continue;
30
+ }
31
+ const 딸린것 = (cp >= 0xfe00 && cp <= 0xfe0f) // 변형 선택자
32
+ || (cp >= 0x1f3fb && cp <= 0x1f3ff) // 살색 조절
33
+ || (cp >= 0x0300 && cp <= 0x036f) // 결합 부호
34
+ || cp === 0x20e3; // 키캡
35
+ if (!딸린것) return s.slice(i, 끝);
36
+ 끝 += String.fromCodePoint(cp).length;
37
+ }
38
+ }
39
+
7
40
  /**
8
41
  * 색을 유지하면서 폭에 맞춰 접는다.
9
42
  *
@@ -43,6 +76,7 @@ export function 접어쓰기(글, 폭) {
43
76
  let 지금 = '';
44
77
  let w = 0;
45
78
  let 색 = ''; // 지금 켜져 있는 SGR
79
+
46
80
  for (let i = 0; i < s.length;) {
47
81
  // 색 코드는 통째로 옮긴다. 폭에는 안 센다.
48
82
  if (s[i] === '\x1b') {
@@ -54,7 +88,7 @@ export function 접어쓰기(글, 폭) {
54
88
  continue;
55
89
  }
56
90
  }
57
- const ch = s[i];
91
+ const ch = 한덩이(s, i);
58
92
  const cw = width(ch);
59
93
  // 첫 줄은 원래 폭, 접힌 줄부터는 들여쓴 만큼 좁아진다.
60
94
  const 한도 = 줄들.length === 0 ? 폭 : 이어폭 + 들여.length;
@@ -65,7 +99,7 @@ export function 접어쓰기(글, 폭) {
65
99
  }
66
100
  지금 += ch;
67
101
  w += cw;
68
- i += 1;
102
+ i += ch.length;
69
103
  }
70
104
  if (지금.trim() !== '' || 줄들.length === 0) 줄들.push(지금 + (색 ? '\x1b[0m' : ''));
71
105
  return 줄들;