deel-local-cli 2.0.3 → 2.1.1

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.
@@ -49,6 +49,7 @@
49
49
  // 그래서 **무엇을 뺐는지 이름을 돌려준다.** 값은 절대 안 돌려준다.
50
50
  // 부르는 쪽이 그 이름을 도구 결과에 적고, 되살리는 법 한 줄을 같이 적는다.
51
51
  import { 열쇠환경인가 } from '../config.js';
52
+ import { 웹훅주소인가 } from './secrets.js';
52
53
 
53
54
  /**
54
55
  * 이 마디가 있으면 뺀다.
@@ -115,9 +116,14 @@ function 이름마디들(이름) {
115
116
  */
116
117
  const 자격든주소 = /^[a-z][a-z0-9+.-]*:\/\/[^\s/?#@:]*:[^\s/?#@]+@/i;
117
118
 
118
- /** 이 값이 자격이 든 주소인가. 이름으로는 못 거르는 DATABASE_URL 같은 것을 거른다. */
119
+ /**
120
+ * 이 값이 자격이 든 주소인가. 이름으로는 못 거르는 DATABASE_URL 같은 것을 거른다.
121
+ *
122
+ * 웹훅 주소·Sentry DSN 도 본다 — 주소 자체가 열쇠인데 이름(SLACK_WEBHOOK_URL · TEAMS_URL)도
123
+ * 꼴(사람:암호@)도 그렇다고 말하지 않는다. 무늬는 가리기와 한 벌이다(secrets.js 의 웹훅주소인가).
124
+ */
119
125
  function 비밀값인가(값) {
120
- return typeof 값 === 'string' && 자격든주소.test(값.trim());
126
+ return typeof 값 === 'string' && (자격든주소.test(값.trim()) || 웹훅주소인가(값));
121
127
  }
122
128
 
123
129
  /**
@@ -1696,6 +1696,106 @@ function 여러파일쓰기(목록, ctx) {
1696
1696
  * 결과 모양을 바꾸면 안 된다. changed·diff·tier 를 보고 있는 자리가 셋이다:
1697
1697
  * loop.js 의 잘린 인자 살려쓰기, repl.js 의 바뀐 자리 그리기, 되돌리기 스냅샷.
1698
1698
  */
1699
+ /*
1700
+ * old_string 을 찾아 new_string 으로 바꾼 **글**만 만든다. 파일은 안 건드린다.
1701
+ *
1702
+ * 고치기(한군데고치기)와 승인 미리보기(바뀔내용)가 이 셈 하나를 같이 쓴다. 둘이 따로
1703
+ * 셈하면 사람이 승인 창에서 본 것과 실제로 쓰인 것이 갈린다 — 들여쓰기를 맞춘 자리,
1704
+ * CRLF 로 바꾼 자리가 미리보기에만 없으면 「본 대로 허락했다」 가 거짓이 된다.
1705
+ */
1706
+ function 새글만들기(text, args) {
1707
+ const m = findMatch(text, args.old_string, { replaceAll: !!args.replace_all });
1708
+ if (!m.ok) return { m, next: null };
1709
+ // CRLF 만 쓰는 파일이면 넣는 글도 CRLF 로 (edit-match.js 의 CRLF뿐인가 머리말).
1710
+ const 줄끝맞춤 = CRLF뿐인가(text) ? CRLF로 : (s) => s;
1711
+ const next = applySpans(text, m.spans, (matched) => 줄끝맞춤(
1712
+ m.tier === 'exact' ? args.new_string
1713
+ // 정규화로 찾았으면 넣는 글도 파일의 꼴(NFC·NFD)로 (edit-match.js 의 꼴맞추기).
1714
+ : m.tier === 'nfc' ? 꼴맞추기(args.new_string, matched)
1715
+ : reindent(args.new_string, matched, args.old_string)));
1716
+ return { m, next };
1717
+ }
1718
+
1719
+ // 미리보기에 실을 만큼. 넘는 파일은 한 줄 물음으로 물러선다 — 승인 창에 몇 MB 를
1720
+ // 그리거나 편집기로 보내는 동안 사람은 멈춘 화면만 본다.
1721
+ const 미리보기상한 = 2_000_000;
1722
+
1723
+ /**
1724
+ * 이 도구 호출이 **실행되면** 무엇이 어떻게 바뀌는가. 승인을 묻기 전에 보여 줄 거리다.
1725
+ *
1726
+ * 사내 검토에서 나온 물음이 이것이었다 — 「고치기 전에 미리보기를 보여 주고 사람이
1727
+ * 확인하는가」. 여태 승인 창은 `Edit src/a.js` 한 줄만 보여 줬다. 무엇이 바뀌는지
1728
+ * 모르고 누르는 y 는 확인이 아니라 습관이다.
1729
+ *
1730
+ * 파일은 **읽기만** 한다. 못 만들면(경로 밖·없는 파일·못 찾는 old_string·문서 파일)
1731
+ * 빈 배열이다 — 그 경우 도구가 어차피 실패로 끝나거나, 물음은 예전처럼 한 줄로 뜬다.
1732
+ *
1733
+ * @returns {Array<{경로:string, 보일경로:string, 전:string|null, 후:string}>}
1734
+ */
1735
+ export function 바뀔내용(이름, 인자, ctx) {
1736
+ const a = 인자 ?? {};
1737
+ /*
1738
+ * 실제 도구가 거절하는 자리는 미리보기도 안 읽는다. deel 자신의 살림(기록·설정·열쇠)은 Write·Edit 이
1739
+ * 먼저 거절하는데(내부살림), 미리보기가 그걸 모르고 읽으면 승인 창에 「지금 내용」 으로 열쇠가 찍히고
1740
+ * 편집기로까지 간다 — 도구는 안 돌았는데 읽기는 일어난 셈이다. 문서 파일은 글로 못 보인다.
1741
+ */
1742
+ const 글파일인가 = (abs) => !(isExcelPath(abs) || isDocPath(abs) || isPdfPath(abs) || isFigPath(abs) || 내부살림(abs));
1743
+ const 읽기 = (abs) => (existsSync(abs) ? readTextFull(abs).text : null);
1744
+ const 담기 = (abs, 전, 후) => ({ 경로: abs, 보일경로: ctx.scope.show(abs), 전, 후 });
1745
+ try {
1746
+ if (이름 === 'Write' || 이름 === 'Append') {
1747
+ if (typeof a.file_path !== 'string' || typeof a.content !== 'string') return [];
1748
+ const abs = ctx.scope.resolve(a.file_path);
1749
+ // 실제 도구는 «가림:…» 표를 파일로 되돌리는 쓰기를 거절한다 — 안 일어날 변경은 안 그린다.
1750
+ if (!글파일인가(abs) || 표몇군데(a.content)) return [];
1751
+ const 전 = 읽기(abs);
1752
+ /*
1753
+ * Append 는 CRLF 만 쓰는 파일에 붙일 때 조각도 CRLF 로 바꿔 쓴다. 그 판정도 실제처럼
1754
+ * **앞머리**로 한다(아래 Append 의 앞글). LF 로 이어 붙여 그리면 본 것과 쓰인 것이 다르다.
1755
+ */
1756
+ const 붙일글 = 이름 === 'Append' && 전 !== null && CRLF뿐인가(전.slice(0, 인코딩볼바이트)) ? CRLF로(a.content) : a.content;
1757
+ const 후 = 이름 === 'Append' ? (전 ?? '') + 붙일글 : a.content;
1758
+ if ((전?.length ?? 0) + 후.length > 미리보기상한) return [];
1759
+ return [담기(abs, 전, 후)];
1760
+ }
1761
+ if (이름 === 'Edit') {
1762
+ // 여러 군데면 실제 고치기처럼 적은 순서대로 차례로 대 보고, 못 찾은 것은 건너뛴다
1763
+ // (여러군데고치기 가 그렇게 한다 — 된 것만 쓰고 안 된 것은 실패로 알린다).
1764
+ const 목록 = Array.isArray(a.edits) ? a.edits.filter((x) => x && typeof x === 'object') : [a];
1765
+ const 파일들 = new Map();
1766
+ for (const x of 목록) {
1767
+ if (typeof x.file_path !== 'string' || typeof x.old_string !== 'string' || typeof x.new_string !== 'string') continue;
1768
+ if (x.old_string === x.new_string) continue;
1769
+ let abs;
1770
+ try { abs = ctx.scope.resolve(x.file_path); } catch { continue; }
1771
+ // 아직 안 읽은 파일은 실제 Edit 이 거절한다(「먼저 Read 로」). 미리보기가 대신 읽어 보여 주면
1772
+ // 모델이 안 본 파일의 내용이 승인 창에 먼저 뜬다 — 그 호출은 어차피 실패로 끝난다.
1773
+ if (!글파일인가(abs) || !ctx.seen?.has(abs)) continue;
1774
+ // 가린 표를 되돌리는 고침은 실제 Edit 이 거절한다(한군데고치기) — 그 한 군데는 안 일어난다.
1775
+ if (표몇군데(x.new_string)) continue;
1776
+ if (!파일들.has(abs)) {
1777
+ const 글 = 읽기(abs);
1778
+ if (글 === null) continue;
1779
+ /*
1780
+ * 너무 커서 못 그리는 파일이 **하나라도** 있으면 미리보기를 통째로 안 낸다. 그 파일만 빼고
1781
+ * 그리면 사람은 작은 파일만 바뀌는 줄 알고 허락한다 — 반쪽 미리보기는 없는 것보다 나쁘다.
1782
+ * 물음은 예전처럼 한 줄로 뜬다 (2차 눈 판정).
1783
+ */
1784
+ if (글.length > 미리보기상한) return [];
1785
+ 파일들.set(abs, { 전: 글, 후: 글 });
1786
+ }
1787
+ const 판 = 파일들.get(abs);
1788
+ const { m, next } = 새글만들기(판.후, x);
1789
+ if (m.ok) 판.후 = next;
1790
+ // 고친 **뒤**도 잰다 — replace_all 한 번에 수십 MB 가 되면 동기 diff 가 승인 창을 붙잡는다.
1791
+ if (판.후.length > 미리보기상한) return [];
1792
+ }
1793
+ return [...파일들].filter(([, 판]) => 판.후 !== 판.전).map(([abs, 판]) => 담기(abs, 판.전, 판.후));
1794
+ }
1795
+ } catch { /* 미리보기는 거드는 것이다 — 못 만들면 물음은 한 줄로 뜬다 */ }
1796
+ return [];
1797
+ }
1798
+
1699
1799
  function 한군데고치기(args, ctx) {
1700
1800
  /*
1701
1801
  * ── 글이 아닌 old_string·new_string 은 **받지 않는다** ──────────────────
@@ -1737,7 +1837,7 @@ function 한군데고치기(args, ctx) {
1737
1837
 
1738
1838
  const 읽음 = readTextFull(abs);
1739
1839
  const text = 읽음.text;
1740
- const m = findMatch(text, args.old_string, { replaceAll: !!args.replace_all });
1840
+ const { m, next } = 새글만들기(text, args);
1741
1841
 
1742
1842
  if (!m.ok) {
1743
1843
  if (m.reason === 'ambiguous') {
@@ -1757,14 +1857,6 @@ function 한군데고치기(args, ctx) {
1757
1857
  return { error: `찾지 못했습니다.${hint}` };
1758
1858
  }
1759
1859
 
1760
- // CRLF 만 쓰는 파일이면 넣는 글도 CRLF 로 (edit-match.js 의 CRLF뿐인가 머리말).
1761
- const 줄끝맞춤 = CRLF뿐인가(text) ? CRLF로 : (s) => s;
1762
- const next = applySpans(text, m.spans, (matched) => 줄끝맞춤(
1763
- m.tier === 'exact' ? args.new_string
1764
- // 정규화로 찾았으면 넣는 글도 파일의 꼴(NFC·NFD)로 (edit-match.js 의 꼴맞추기).
1765
- : m.tier === 'nfc' ? 꼴맞추기(args.new_string, matched)
1766
- : reindent(args.new_string, matched, args.old_string)));
1767
-
1768
1860
  // 읽은 그 인코딩으로 되돌려 쓴다. 안 바뀐 앞뒤는 읽은 바이트 그대로 (encoding.js 의 바꾼데만쓰기).
1769
1861
  const 만든것 = 바꾼데만쓰기(읽음.buf, text, next, 읽음.encoding);
1770
1862
  /*
package/src/tools/lsp.js CHANGED
@@ -23,12 +23,21 @@
23
23
  * 그대로 보여 주고 고르게 한다 — 하나를 골라 주고 아닌 척하지 않는다.
24
24
  */
25
25
  import { readFileSync } from 'node:fs';
26
+ import { decode } from './encoding.js';
26
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
28
  import { 얻기, 색인중일까 } from '../lsp/client.js';
28
29
  import { 갈래, 프로젝트갈래 } from '../lsp/servers.js';
29
30
  import { 찾을개수 } from '../agent/budget.js';
30
31
  import { 말, 세말 } from '../i18n/index.js';
31
32
 
33
+ /*
34
+ * 소스를 읽는다 — Read·Edit 와 같은 자(encoding.js)로.
35
+ *
36
+ * 날 UTF-8 로 읽으면 EUC-KR·CP949 소스의 한글 이름이 깨진 글자(U+FFFD)가 되어 「그 줄에
37
+ * 없다」 로 실패하고, 서버에 보여 주는 내용(didOpen)도 깨진 글이 된다 (2.1.1 검수).
38
+ */
39
+ const 소스읽기 = (abs) => decode(readFileSync(abs)).text;
40
+
32
41
  /**
33
42
  * 한 자리를 사람이 읽을 한 줄로. 그 줄의 글까지 붙여야 열어 보지 않고도 안다.
34
43
  *
@@ -51,7 +60,7 @@ function 한줄(scope, uri, 범위, 줄표 = new Map()) {
51
60
  // 윈도우는 서버가 드라이브 글자를 소문자로 적어 온다(c%3A) — 같은 파일을 두 번 읽지 않게.
52
61
  const 열쇠 = process.platform === 'win32' ? abs.toLowerCase() : abs;
53
62
  if (!줄표.has(열쇠)) {
54
- try { 줄표.set(열쇠, readFileSync(abs, 'utf8').split(/\r?\n/)); } catch { 줄표.set(열쇠, null); /* 못 읽으면 자리만 준다 */ }
63
+ try { 줄표.set(열쇠, 소스읽기(abs).split(/\r?\n/)); } catch { 줄표.set(열쇠, null); /* 못 읽으면 자리만 준다 */ }
55
64
  }
56
65
  let 글 = (줄표.get(열쇠)?.[줄번호 - 1] ?? '').trim();
57
66
  if (글.length > 160) 글 = 글.slice(0, 160) + '…';
@@ -105,7 +114,7 @@ async function 자리잡기(서버, scope, { 이름, 파일, 줄 }) {
105
114
  let abs;
106
115
  try { abs = scope.resolve(파일); } catch (e) { return { 오류: e.message }; }
107
116
  let 줄들;
108
- try { 줄들 = readFileSync(abs, 'utf8').split(/\r?\n/); } catch { return { 오류: `못 읽었습니다: ${파일}` }; }
117
+ try { 줄들 = 소스읽기(abs).split(/\r?\n/); } catch { return { 오류: `못 읽었습니다: ${파일}` }; }
109
118
  서버.보여주기(abs, 줄들.join('\n'));
110
119
 
111
120
  const 볼줄 = Number.isFinite(줄) && 줄 > 0 ? [줄 - 1] : 줄들.map((_, i) => i);
@@ -168,7 +177,7 @@ async function 자리잡기(서버, scope, { 이름, 파일, 줄 }) {
168
177
  if (!position) {
169
178
  try {
170
179
  const abs = fileURLToPath(첫.uri);
171
- const 줄들 = readFileSync(abs, 'utf8').split(/\r?\n/);
180
+ const 줄들 = 소스읽기(abs).split(/\r?\n/);
172
181
  for (let i = 0; i < 줄들.length; i++) {
173
182
  const 칸 = 칸찾기(줄들[i], 이름);
174
183
  if (칸 >= 0) { position = { line: i, character: 칸 }; break; }
@@ -181,7 +190,7 @@ async function 자리잡기(서버, scope, { 이름, 파일, 줄 }) {
181
190
  // 서버가 아무것도 못 준다.
182
191
  try {
183
192
  const abs = fileURLToPath(첫.uri);
184
- const 줄글 = readFileSync(abs, 'utf8').split(/\r?\n/)[position.line] ?? '';
193
+ const 줄글 = 소스읽기(abs).split(/\r?\n/)[position.line] ?? '';
185
194
  const 칸 = 칸찾기(줄글, 이름);
186
195
  if (칸 >= 0) position = { line: position.line, character: 칸 };
187
196
  서버.보여주기(abs);
package/src/ui/approve.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * 그래서 세 가지를 준다 — 눈에 띄는 글자, 사람 말 이름, 무엇을 묻는지 한 줄.
14
14
  */
15
15
  import { c } from './ansi.js';
16
+ import { diffLines, renderDiff, shortStat } from './diff.js';
16
17
  import { 말 } from '../i18n/index.js';
17
18
 
18
19
  /*
@@ -56,11 +57,66 @@ export function 고르기(이름) {
56
57
  * 한 번 누를 때마다 더 많이 물어보게 되니, 잘못 눌러도 위험해지지 않는다.
57
58
  * 반대로 돌면 Shift+Tab 한 번에 '안 묻고 고침' 으로 떨어진다.
58
59
  */
59
- export function 다음(이름) {
60
+ export function 다음(이름, 바닥 = 'auto') {
61
+ // 관리 정책이 바닥을 걸었으면 그 위에서만 돈다 (safety/policy.js 의 승인바닥).
62
+ // 전부 돌게 두면 바닥 아래 칸에 들어갔다가 다시 끌어올려져, 눌러도 안 바뀐 것처럼 보인다.
63
+ const 돌칸 = 차례.slice(Math.max(0, 차례.indexOf(바닥)));
60
64
  // 모르는 값이면 화면에 떠 있는 것(고르기가 내주는 기본)을 기준으로 삼는다.
61
65
  // 안 그러면 보이는 것과 한 칸 어긋나서, 눌러도 안 바뀐 것처럼 보인다.
62
- const i = 차례.indexOf(승인[이름] ? 이름 : 기본);
63
- return 차례[(i + 1) % 차례.length];
66
+ const i = 돌칸.indexOf(승인[이름] ? 이름 : 기본);
67
+ return 돌칸[(i + 1) % 돌칸.length];
68
+ }
69
+
70
+ /**
71
+ * 자율(auto) 모드에서 사람 답을 **얼마나** 기다리나 — 초. 설정의 `askWait`, 기본 60.
72
+ *
73
+ * auto 는 「다 맡긴다」 는 모드다. 그런데 그 안에서도 사람 답을 끝없이 기다리는 자리가
74
+ * 둘 남아 있었다 — 모델이 되묻는 것(Ask 도구)과, 「계획하고 만들어줘」 에 뜨는 계획 승인.
75
+ * 맡겨 두고 자리를 뜬 사람에게 그건 멈춘 것과 같다. 그래서 auto 에서만 시한을 둔다.
76
+ * 시한이 지나면 되묻기는 「스스로 판단하라」 로, 계획은 「그대로 진행」 으로 간다.
77
+ *
78
+ * 0 이면 아예 안 묻고 간다. 도구 승인(confirm·strict 의 「실행할까요?」)에는 **안 건다** —
79
+ * 그 모드를 고른 사람은 답을 기다리라고 고른 것이다. 관리 정책이 승인 바닥을 걸면 모드가
80
+ * auto 가 아니므로 여기도 안 걸린다.
81
+ */
82
+ export const 자율대기기본 = 60;
83
+ export function 자율대기(cfg) {
84
+ /*
85
+ * 숫자와 숫자 글만 받는다. Number() 에 통째로 넘기면 true 가 1초, false 와 빈칸(' ')이
86
+ * 0 — 「안 묻고 간다」 — 가 된다. 그렇게 적은 사람이 바란 것이 무엇이든 그건 아니다 (2차 눈 판정).
87
+ */
88
+ const v = cfg?.askWait;
89
+ const n = typeof v === 'number' ? v : typeof v === 'string' && v.trim() ? Number(v) : NaN;
90
+ if (!Number.isFinite(n) || n < 0) return 자율대기기본;
91
+ return Math.min(3600, Math.floor(n));
92
+ }
93
+
94
+ /**
95
+ * 승인 물음 밑에 그릴 「바뀔 내용」 (tools/index.js 의 바뀔내용).
96
+ *
97
+ * 무엇이 바뀌는지 모르고 누르는 y 는 확인이 아니라 습관이다. 그래서 물음 밑에 파일마다
98
+ * 머리 한 줄과 바뀐 자리를 그린다. 여러 파일이면 줄 수를 나눠 쓴다 — 한 파일이 다 먹으면
99
+ * 뒤 파일은 이름조차 안 보이고, 사람은 한 파일만 바뀌는 줄 알고 허락한다.
100
+ */
101
+ export function 미리보기줄들(미리보기, { 줄수 = 40 } = {}) {
102
+ if (!Array.isArray(미리보기) || !미리보기.length) return [];
103
+ /*
104
+ * 파일 이름 줄은 **다 그린다**(위 머리말). 바뀐 줄은 한도를 나눠 쓰되, 한 파일 몫이 3줄도 안
105
+ * 되면 이름 줄과 셈(+3 -1)만 그린다. 여태 몫에 하한 6 을 두어 파일 50개면 400줄이 나왔다 —
106
+ * 물음이 화면 밖으로 밀려 사람은 무엇에 y 를 치는지 못 본다 (2차 눈 판정).
107
+ */
108
+ const 몫 = Math.floor(줄수 / 미리보기.length);
109
+ const out = [];
110
+ for (const 것 of 미리보기) {
111
+ let d = null;
112
+ try { d = diffLines(것.전, 것.후); } catch { d = null; }
113
+ const 셈 = d ? shortStat(d) : '';
114
+ // 같은 내용으로 다시 쓰는 부름이면 그렇다고 적는다 — 이름 한 줄만 뜨면 사람은 무언가 바뀌는 줄 안다.
115
+ const 딸린말 = 것.전 === null ? ` (${말('approve.newFile')})` : 것.전 === 것.후 ? ` (${말('approve.noChange')})` : '';
116
+ out.push(` ${c.white(것.보일경로)}${딸린말 ? c.gray(딸린말) : ''}${셈 ? ` ${셈}` : ''}`);
117
+ if (d && 몫 >= 3) out.push(...renderDiff(d, { maxLines: 몫, indent: ' ' }));
118
+ }
119
+ return out;
64
120
  }
65
121
 
66
122
  /** 상태줄에 넣을 조각. 짧게=true 면 좁은 창용. */