deel-local-cli 1.5.7 → 1.6.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 +24 -13
- package/README.md +23 -12
- package/bin/deel.js +10 -0
- package/package.json +2 -2
- package/src/acp/map.js +143 -0
- package/src/acp/serve.js +160 -13
- package/src/agent/commit.js +511 -0
- package/src/agent/compact.js +269 -226
- package/src/agent/loop.js +154 -12
- package/src/agent/mention.js +56 -10
- package/src/agent/review.js +192 -0
- package/src/agent/session.js +20 -2
- package/src/agent/threads.js +1 -1
- package/src/backend/adapter.js +342 -280
- package/src/backend/azure.js +151 -0
- package/src/backend/ctxsize.js +19 -0
- package/src/backend/detect.js +106 -0
- package/src/backend/http.js +350 -30
- package/src/backend/probe.js +48 -1
- package/src/backend/proxy.js +151 -0
- package/src/backend/quota.js +133 -0
- package/src/backend/retry.js +132 -0
- package/src/backend/vision.js +185 -0
- package/src/commands.js +341 -8
- package/src/completion.js +264 -0
- package/src/config.js +117 -1
- package/src/i18n/en.js +13 -1
- package/src/i18n/index.js +22 -3
- package/src/i18n/ja.js +266 -0
- package/src/i18n/ko.js +12 -0
- package/src/i18n/zh.js +266 -0
- package/src/oneshot.js +28 -3
- package/src/pack/sbom.js +25 -0
- package/src/pack/selfpack.js +9 -3
- package/src/plugins/manage.js +34 -7
- package/src/repl.js +113 -15
- package/src/report.js +26 -2
- package/src/safety/guard.js +82 -3
- package/src/safety/keystore.js +237 -0
- package/src/safety/network.js +76 -14
- package/src/safety/policy.js +209 -0
- package/src/safety/secrets.js +32 -0
- package/src/setup.js +40 -7
- package/src/tools/clipboard.js +178 -0
- package/src/tools/fastgrep.js +217 -0
- package/src/tools/fsutil.js +55 -12
- package/src/tools/ignore.js +192 -0
- package/src/tools/index.js +282 -39
- package/src/tools/jobs.js +4 -12
- package/src/tools/outline.js +3 -1
- package/src/tools/pdf.js +1326 -0
- package/src/tools/shell.js +123 -0
- package/src/tools/verify.js +20 -4
- package/src/tools/webfetch.js +18 -5
- package/src/ui/inputbox.js +2 -2
- package/src/ui/screen.js +4 -1
- package/src/ui/status.js +22 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// 이미 깔려 있는 빠른 찾기 도구를 빌려 쓴다.
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 필요한가 ────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// `Grep` 은 자바스크립트로 파일을 하나씩 열어 읽는다. 의존성 0개라는 약속을
|
|
6
|
+
// 지키려면 그래야 했다. 그런데 파일이 5만 개쯤 되는 저장소에서는 한 번 찾는
|
|
7
|
+
// 데 수십 초가 간다. 그동안 화면은 멈춰 있고 사람은 기다린다.
|
|
8
|
+
//
|
|
9
|
+
// 그런데 개발자 PC 에는 `rg`(ripgrep)가 이미 깔려 있는 경우가 많다. VS Code
|
|
10
|
+
// 가 같이 깔고, git 이 있으면 `git grep` 도 있다. **깔지는 않고, 있으면 쓴다.**
|
|
11
|
+
// 이건 의존성이 아니다 — 없으면 예전 길로 가고 결과는 같다.
|
|
12
|
+
//
|
|
13
|
+
// ── 무엇을 조심하나 ────────────────────────────────────────────────────
|
|
14
|
+
//
|
|
15
|
+
// 1) **어느 엔진으로 찾았는지 말한다.** rg 와 자바스크립트 정규식은 문법이
|
|
16
|
+
// 조금 다르다(rg 는 Rust regex — 되돌아보기가 없다). 결과가 다르게 나왔을
|
|
17
|
+
// 때 무엇으로 찾은 것인지 모르면 사람은 코드를 의심한다.
|
|
18
|
+
//
|
|
19
|
+
// 2) **범위 밖으로 못 나간다.** 찾을 자리는 언제나 scope 가 준 절대경로
|
|
20
|
+
// 하나뿐이고, 무늬는 인자로만 넘긴다. 셸을 안 거치므로 무늬 안의
|
|
21
|
+
// 따옴표·세미콜론이 명령이 되지 못한다.
|
|
22
|
+
//
|
|
23
|
+
// 3) **못 찾으면 조용히 예전 길로.** 엔진이 죽거나 낯선 문법에 화를 내면
|
|
24
|
+
// 결과가 없는 것이 아니라 우리가 못 물어본 것이다. 그때 '일치 없음' 을
|
|
25
|
+
// 돌려주면 사람은 없는 줄 안다. 그래서 실패는 실패로 두고 JS 로 다시 찾는다.
|
|
26
|
+
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* 정규식으로 찾을 것이 없는 파일들.
|
|
31
|
+
*
|
|
32
|
+
* **여기가 한 벌뿐인 원본이다.** Grep 의 자바스크립트 길은 이 목록으로 정규식을
|
|
33
|
+
* 만들고, rg 는 이 목록으로 `--iglob !*.png` 를 만든다. 두 군데에 따로 적어 두면
|
|
34
|
+
* 언젠가 한쪽만 고쳐지고, 그때부터 같은 명령이 PC 마다 다른 답을 낸다 —
|
|
35
|
+
* rg 가 깔린 사람만 8MB 번들 속 글자를 찾게 되는 식으로. 그건 오류도 안 나고
|
|
36
|
+
* 아무도 눈치 못 챈다.
|
|
37
|
+
*/
|
|
38
|
+
export const 안볼확장자 = [
|
|
39
|
+
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'webp', 'svgz', 'pdf',
|
|
40
|
+
'zip', 'gz', 'tgz', '7z', 'rar',
|
|
41
|
+
'exe', 'dll', 'so', 'dylib', 'bin', 'dat',
|
|
42
|
+
'db', 'sqlite', 'sqlite3',
|
|
43
|
+
'woff', 'woff2', 'ttf', 'otf', 'eot',
|
|
44
|
+
'mp3', 'mp4', 'wav', 'avi', 'mov', 'mkv',
|
|
45
|
+
'class', 'jar', 'pyc', 'pyo', 'o', 'a', 'lib', 'pack', 'idx',
|
|
46
|
+
'map', 'min.js', 'min.css', 'lock',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** 위 목록으로 만든 정규식. 자바스크립트 길이 쓴다. */
|
|
50
|
+
export const 안볼정규식 = new RegExp(`\\.(${안볼확장자.map((x) => x.replace(/\./g, '\\.')).join('|')})$`, 'i');
|
|
51
|
+
|
|
52
|
+
/** 위 목록으로 만든 rg 옵션. 대소문자를 안 가린다(정규식의 `i` 와 맞춘다). */
|
|
53
|
+
export function 안볼글로브() {
|
|
54
|
+
return 안볼확장자.flatMap((x) => ['--iglob', `!*.${x}`]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* 있는지 한 번만 본다.
|
|
59
|
+
*
|
|
60
|
+
* 매번 `rg --version` 을 돌리면 찾기 한 번에 프로세스가 하나 더 뜬다.
|
|
61
|
+
* 없는 PC 에서는 그 실패가 매번 수십 ms 씩 쌓인다.
|
|
62
|
+
*/
|
|
63
|
+
let 본것 = null;
|
|
64
|
+
|
|
65
|
+
function 있나(이름, 인자) {
|
|
66
|
+
const r = spawnSync(이름, 인자, { encoding: 'utf8', timeout: 4000, windowsHide: true });
|
|
67
|
+
return !r.error && r.status === 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function 엔진찾기({ 다시 = false, env = process.env } = {}) {
|
|
71
|
+
if (본것 && !다시) return 본것;
|
|
72
|
+
// 끄고 싶을 때가 있다 — 결과가 다르다고 의심될 때 같은 자리에서 견주려면.
|
|
73
|
+
if (env.DEEL_GREP === 'js') { 본것 = { rg: false, gitgrep: false, 왜: 'DEEL_GREP=js 로 꺼 두었습니다' }; return 본것; }
|
|
74
|
+
본것 = {
|
|
75
|
+
rg: 있나('rg', ['--version']),
|
|
76
|
+
gitgrep: 있나('git', ['--version']),
|
|
77
|
+
왜: null,
|
|
78
|
+
};
|
|
79
|
+
return 본것;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 검사가 원래대로 돌려놓을 때. */
|
|
83
|
+
export function 엔진잊기() { 본것 = null; return null; }
|
|
84
|
+
|
|
85
|
+
/** 이 폴더가 git 저장소 안인가. `git grep` 은 저장소 안에서만 돈다. */
|
|
86
|
+
export function 저장소인가(폴더) {
|
|
87
|
+
const r = spawnSync('git', ['-C', 폴더, 'rev-parse', '--is-inside-work-tree'], {
|
|
88
|
+
encoding: 'utf8', timeout: 4000, windowsHide: true,
|
|
89
|
+
});
|
|
90
|
+
return !r.error && r.status === 0 && /true/.test(r.stdout ?? '');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* rg 로 찾는다.
|
|
95
|
+
*
|
|
96
|
+
* 돌려주는 것은 **줄 목록**이다. 세는 일과 자르는 일은 부르는 쪽이 한다 —
|
|
97
|
+
* 그래야 JS 로 찾았을 때와 결과 모양이 같아진다. 모양이 갈리면 '엔진에 따라
|
|
98
|
+
* 화면이 다른' 프로그램이 되는데, 그건 이 도구가 피하려는 것이다.
|
|
99
|
+
*
|
|
100
|
+
* @returns {{ok:true, 줄들:string[], 잘림:boolean} | {ok:false, 왜:string}}
|
|
101
|
+
*/
|
|
102
|
+
export function rg로찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, timeout = 20000 }) {
|
|
103
|
+
const 인자 = [
|
|
104
|
+
'--line-number',
|
|
105
|
+
'--no-heading',
|
|
106
|
+
'--with-filename',
|
|
107
|
+
'--color', 'never',
|
|
108
|
+
// 아주 큰 파일은 안 본다(JS 길과 같은 규칙). 글이 아닌 파일은 rg 가 기본으로 건너뛴다.
|
|
109
|
+
'--max-filesize', '2M',
|
|
110
|
+
/*
|
|
111
|
+
* git 저장소가 아니어도 .gitignore 를 지킨다.
|
|
112
|
+
*
|
|
113
|
+
* rg 는 기본으로 **저장소 안에서만** .gitignore 를 본다. 저장소 밖에서는
|
|
114
|
+
* 그 파일을 글자로만 여기고 out/ · secret.txt 를 그냥 뒤진다. 우리 JS 길은
|
|
115
|
+
* 저장소든 아니든 지키므로, 이 한 줄이 없으면 같은 폴더에서 엔진에 따라
|
|
116
|
+
* 답이 갈린다 — 그것도 **비밀을 더 보는 쪽으로**.
|
|
117
|
+
*/
|
|
118
|
+
'--no-require-git',
|
|
119
|
+
];
|
|
120
|
+
if (대소문자무시) 인자.push('--ignore-case');
|
|
121
|
+
if (glob) 인자.push('--glob', glob);
|
|
122
|
+
// 자바스크립트 길이 안 여는 파일은 rg 도 안 열게 한다. 같은 목록에서 나온다.
|
|
123
|
+
인자.push(...안볼글로브());
|
|
124
|
+
/*
|
|
125
|
+
* .deelignore 도 지켜야 한다.
|
|
126
|
+
*
|
|
127
|
+
* rg 는 .gitignore 는 알지만 .deelignore 는 모른다. 그대로 두면 "git 에는
|
|
128
|
+
* 안 적고 deel 만 건너뛸 것" 이 rg 가 깔린 PC 에서만 조용히 검색된다.
|
|
129
|
+
* 같은 명령이 PC 마다 다른 답을 내는 셈이라, 이건 빠른 것보다 나쁘다.
|
|
130
|
+
*/
|
|
131
|
+
if (무시파일) 인자.push('--ignore-file', 무시파일);
|
|
132
|
+
// `--` 뒤로 넘겨서 무늬가 옵션으로 안 읽히게 한다. `-foo` 같은 무늬가 실제로 있다.
|
|
133
|
+
인자.push('--regexp', 무늬, '--', 자리);
|
|
134
|
+
|
|
135
|
+
const r = spawnSync('rg', 인자, {
|
|
136
|
+
encoding: 'utf8', timeout, windowsHide: true, maxBuffer: 32 * 1024 * 1024,
|
|
137
|
+
});
|
|
138
|
+
// rg 는 못 찾으면 1, 진짜 탈이 나면 2 를 준다. 1 은 성공(빈 결과)이다.
|
|
139
|
+
if (r.error) return { ok: false, 왜: r.error.message };
|
|
140
|
+
if (r.status === 2) return { ok: false, 왜: (r.stderr ?? '').split('\n')[0] || 'rg 가 무늬를 못 읽었습니다' };
|
|
141
|
+
const 줄들 = String(r.stdout ?? '').split('\n').filter(Boolean);
|
|
142
|
+
return { ok: true, 줄들: 줄들.slice(0, 최대), 잘림: 줄들.length > 최대 };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `git grep` 으로 찾는다. rg 가 없고 여기가 저장소일 때. */
|
|
146
|
+
export function git로찾기({ 무늬, 자리, 대소문자무시 = false, 최대 = 5000, timeout = 20000 }) {
|
|
147
|
+
const 인자 = ['-C', 자리, '--no-pager', 'grep', '--line-number', '--no-color', '-I', '-E'];
|
|
148
|
+
if (대소문자무시) 인자.push('-i');
|
|
149
|
+
// 여기도 같은 목록으로 뺀다. git 은 경로무늬 앞머리에 마법을 붙여 빼낸다.
|
|
150
|
+
인자.push('-e', 무늬, '--', '.', ...안볼확장자.map((x) => `:(exclude,icase)*.${x}`));
|
|
151
|
+
const r = spawnSync('git', 인자, {
|
|
152
|
+
encoding: 'utf8', timeout, windowsHide: true, maxBuffer: 32 * 1024 * 1024,
|
|
153
|
+
});
|
|
154
|
+
if (r.error) return { ok: false, 왜: r.error.message };
|
|
155
|
+
if (r.status !== 0 && r.status !== 1) {
|
|
156
|
+
return { ok: false, 왜: (r.stderr ?? '').split('\n')[0] || 'git grep 이 무늬를 못 읽었습니다' };
|
|
157
|
+
}
|
|
158
|
+
const 줄들 = String(r.stdout ?? '').split('\n').filter(Boolean);
|
|
159
|
+
// git grep 은 저장소 기준 상대경로를 준다. 절대경로로 맞춰 준다 — 아래에서 한 모양으로 쓴다.
|
|
160
|
+
const 고친것 = 줄들.map((l) => {
|
|
161
|
+
const i = l.indexOf(':');
|
|
162
|
+
if (i < 0) return l;
|
|
163
|
+
return join(자리, l.slice(0, i)) + l.slice(i);
|
|
164
|
+
});
|
|
165
|
+
return { ok: true, 줄들: 고친것.slice(0, 최대), 잘림: 고친것.length > 최대 };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* `경로:줄번호:내용` 한 줄을 가른다.
|
|
170
|
+
*
|
|
171
|
+
* 윈도우 경로에는 `C:` 가 있어서 **첫 번째 콜론으로 자르면 안 된다.**
|
|
172
|
+
* `C:\a\b.js:12:const x = 1;` 을 첫 콜론에서 자르면 파일 이름이 `C` 가 된다.
|
|
173
|
+
*
|
|
174
|
+
* 그 일은 아래 정규식이 이미 막는다. `(.*?)` 는 게으른 짝짓기라 짧은 쪽부터
|
|
175
|
+
* 보지만, 그 뒤에 **`:숫자:` 가 반드시 와야 한다.** `C` 다음은 `:\Users…`
|
|
176
|
+
* 라 숫자가 아니므로 그 자리는 버려지고, 진짜 줄 번호 앞 콜론까지 늘어난다.
|
|
177
|
+
*
|
|
178
|
+
* 전에는 여기에 "첫 조각이 한 글자면 다시 본다" 는 갈래가 하나 더 있었다.
|
|
179
|
+
* 지우고 검사를 돌려도 아무것도 안 빨개졌다 — 닿지 않는 길이었다는 뜻이다.
|
|
180
|
+
* 막고 있는 척만 하는 코드는 없느니만 못해서 지웠다. 대신 윈도우 경로를
|
|
181
|
+
* 재는 검사를 test/fastgrep.test.js 에 남겨 둔다.
|
|
182
|
+
*/
|
|
183
|
+
export function 줄가르기(줄) {
|
|
184
|
+
const m = /^(.*?):(\d+):([\s\S]*)$/.exec(줄);
|
|
185
|
+
if (!m) return null;
|
|
186
|
+
return { 파일: m[1], 줄: Number(m[2]), 내용: m[3] };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 빠른 엔진으로 찾아 본다. 못 쓰면 null — 부르는 쪽이 예전 길로 간다.
|
|
191
|
+
*
|
|
192
|
+
* @returns {{엔진:string, 줄들:Array<{파일,줄,내용}>, 잘림:boolean} | null}
|
|
193
|
+
*/
|
|
194
|
+
export function 빠르게찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000 }) {
|
|
195
|
+
const 것 = 엔진찾기();
|
|
196
|
+
if (것.rg) {
|
|
197
|
+
const r = rg로찾기({ 무늬, 자리, glob, 대소문자무시, 무시파일, 최대 });
|
|
198
|
+
if (r.ok) return { 엔진: 'rg', 줄들: r.줄들.map(줄가르기).filter(Boolean), 잘림: r.잘림 };
|
|
199
|
+
// rg 가 무늬를 못 읽은 것일 수 있다(Rust regex 에는 되돌아보기가 없다).
|
|
200
|
+
// 그건 '없다' 가 아니라 '못 물어봤다' 이므로 예전 길로 내려간다.
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
// git grep 은 .deelignore 를 시킬 방법이 없다. 그 파일이 있으면 예전 길로 간다 —
|
|
204
|
+
// 사람이 "deel 은 여기 보지 마라" 고 적어 둔 것을 못 지키면 빠른 것이 뜻이 없다.
|
|
205
|
+
if (것.gitgrep && !무시파일 && 저장소인가(자리)) {
|
|
206
|
+
const r = git로찾기({ 무늬, 자리, 대소문자무시, 최대 });
|
|
207
|
+
if (r.ok) return { 엔진: 'git grep', 줄들: r.줄들.map(줄가르기).filter(Boolean), 잘림: r.잘림 };
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 화면·결과 꼬리에 적을 한마디. 무엇으로 찾았는지 사람이 알아야 한다. */
|
|
214
|
+
export function 엔진말(이름) {
|
|
215
|
+
if (!이름) return '';
|
|
216
|
+
return `(${이름} 으로 찾았습니다 — 이 PC 에 이미 있어서 빌려 썼습니다)`;
|
|
217
|
+
}
|
package/src/tools/fsutil.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// 파일 훑기와 glob 매칭. 외부 패키지 없이 직접 구현한다.
|
|
2
2
|
import { readdirSync, statSync, readFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
3
|
-
import { join
|
|
3
|
+
import { join } from 'node:path';
|
|
4
4
|
import { decode, looksBinary } from './encoding.js';
|
|
5
|
+
import { 뿌리규칙읽기, 파일규칙읽기, 걸리나 } from './ignore.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 폴더를 통째로 옮겨 담는다.
|
|
@@ -142,32 +143,74 @@ function escapeLiteral(s) {
|
|
|
142
143
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
/**
|
|
147
|
+
* 폴더를 훑어 파일 목록을 낸다. { path(절대), rel(/구분), mtime, size }
|
|
148
|
+
*
|
|
149
|
+
* `ignore` 가 켜져 있으면(기본) .gitignore 가 건너뛰라는 것은 건너뛴다 (tools/ignore.js).
|
|
150
|
+
* skipDirs 는 그 아래의 바닥이다 — .gitignore 가 없어도 node_modules 는 늘 건너뛴다.
|
|
151
|
+
* 돌려주는 배열에는 `건너뜀 = { 폴더, 파일 }` 이 (열거되지 않게) 붙어 있다. 부르는 쪽이 그 수를
|
|
152
|
+
* 화면에 적는다 — 조용히 빼면 "그 파일이 없다" 로 읽힌다.
|
|
153
|
+
* 폴더를 통째로 옮기거나 복사할 때는 `ignore: false` 로 — 그때는 다 있어야 한다.
|
|
154
|
+
*
|
|
155
|
+
* `잘림` 도 같이 붙는다 — **상한에서 멈췄다는 뜻이다.**
|
|
156
|
+
*
|
|
157
|
+
* 전에는 이 상한이 조용했다. 파일 5만 개짜리 저장소에서 앞의 2만 개만 훑고
|
|
158
|
+
* 멈추는데, 아무 데도 그 말이 안 나갔다. 그래서 `Grep` 이 "일치 없음" 이라고
|
|
159
|
+
* 답했다 — 실제로는 뒤쪽 3만 개에 100군데가 있는데도. 못 찾은 것과 안 본 것은
|
|
160
|
+
* 다르고, 그걸 안 가르면 사람은 없는 줄 알고 그냥 간다. 조용한 상한은
|
|
161
|
+
* 틀린 답보다 나쁘다.
|
|
162
|
+
*/
|
|
163
|
+
export const 기본훑기상한 = 20000;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 몇 개까지 훑을까.
|
|
167
|
+
*
|
|
168
|
+
* 2만 개는 대부분의 저장소에 넉넉하지만 큰 단일 저장소에는 모자란다. 그래서
|
|
169
|
+
* 올릴 수 있게 열어 둔다 — 못 보는 것을 알려 주기만 하고 손쓸 방법이 없으면
|
|
170
|
+
* 알려 준 뜻이 없다. 터무니없는 값은 안 받는다(100 미만·200만 초과).
|
|
171
|
+
*/
|
|
172
|
+
export function 훑기상한(env = process.env) {
|
|
173
|
+
const n = Number(env.DEEL_WALK_LIMIT);
|
|
174
|
+
if (!Number.isFinite(n) || n < 100 || n > 2000000) return 기본훑기상한;
|
|
175
|
+
return Math.floor(n);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignore = true } = {}) {
|
|
147
179
|
const out = [];
|
|
148
|
-
const
|
|
180
|
+
const 건너뜀 = { 폴더: 0, 파일: 0 };
|
|
181
|
+
const stack = [{ dir: root, rel: '', 규칙: ignore ? 뿌리규칙읽기(root) : [] }];
|
|
149
182
|
while (stack.length && out.length < limit) {
|
|
150
|
-
const dir = stack.pop();
|
|
183
|
+
const { dir, rel, 규칙 } = stack.pop();
|
|
151
184
|
let entries;
|
|
152
185
|
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
186
|
+
// 이 폴더에 .gitignore 가 있으면 그 아래에만 더한다 (뿌리 것은 위에서 읽었다).
|
|
187
|
+
let 여기규칙 = 규칙;
|
|
188
|
+
if (ignore && rel && entries.some((e) => e.name === '.gitignore' && e.isFile())) {
|
|
189
|
+
const 추가 = 파일규칙읽기(join(dir, '.gitignore'), rel);
|
|
190
|
+
if (추가.length) 여기규칙 = [...규칙, ...추가];
|
|
191
|
+
}
|
|
153
192
|
for (const e of entries) {
|
|
154
193
|
const full = join(dir, e.name);
|
|
194
|
+
const erel = rel ? `${rel}/${e.name}` : e.name;
|
|
155
195
|
if (e.isDirectory()) {
|
|
156
196
|
if (skipDirs.has(e.name)) continue;
|
|
157
|
-
|
|
197
|
+
if (여기규칙.length && 걸리나(erel, true, 여기규칙)) { 건너뜀.폴더 += 1; continue; }
|
|
198
|
+
stack.push({ dir: full, rel: erel, 규칙: 여기규칙 });
|
|
158
199
|
} else if (e.isFile()) {
|
|
200
|
+
if (여기규칙.length && 걸리나(erel, false, 여기규칙)) { 건너뜀.파일 += 1; continue; }
|
|
159
201
|
let st;
|
|
160
202
|
try { st = statSync(full); } catch { continue; }
|
|
161
|
-
out.push({
|
|
162
|
-
path: full,
|
|
163
|
-
rel: relative(root, full).split(sep).join('/'),
|
|
164
|
-
mtime: st.mtimeMs,
|
|
165
|
-
size: st.size,
|
|
166
|
-
});
|
|
203
|
+
out.push({ path: full, rel: erel, mtime: st.mtimeMs, size: st.size });
|
|
167
204
|
if (out.length >= limit) break;
|
|
168
205
|
}
|
|
169
206
|
}
|
|
170
207
|
}
|
|
208
|
+
Object.defineProperty(out, '건너뜀', { value: 건너뜀, enumerable: false });
|
|
209
|
+
// 상한까지 찼으면 "여기서 멈췄다" 고 표시한다. 딱 맞아떨어져 끝난 경우까지
|
|
210
|
+
// 잘렸다고 하게 되지만, 그쪽으로 틀리는 편이 낫다 — 덜 봤다고 말하는 것은
|
|
211
|
+
// 사람을 한 번 더 보게 만들 뿐이고, 다 봤다고 말하는 것은 못 보게 만든다.
|
|
212
|
+
Object.defineProperty(out, '잘림', { value: out.length >= limit, enumerable: false });
|
|
213
|
+
Object.defineProperty(out, '상한', { value: limit, enumerable: false });
|
|
171
214
|
return out;
|
|
172
215
|
}
|
|
173
216
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// .gitignore 를 읽어, git 이 건너뛰는 것을 도구도 건너뛴다 — Glob · Grep · Outline · Verify · @폴더.
|
|
2
|
+
//
|
|
3
|
+
// 왜: walk() 는 정해진 폴더 몇 개(node_modules · dist …)만 건너뛰었다. 실제 저장소는 out/ ·
|
|
4
|
+
// .gradle/ · coverage/ · 만들어진 코드 · 자료 덤프를 .gitignore 에 적어 두는데, 그걸 다 훑으면
|
|
5
|
+
// 32k 창에서 Grep 한 번이 빌드 산출물로 예산을 다 쓴다. 사람은 git 이 안 보는 것을 도구도
|
|
6
|
+
// 안 볼 거라고 생각한다 — 그 기대를 맞춘다.
|
|
7
|
+
//
|
|
8
|
+
// 지원하는 것 — git 규칙의 부분집합:
|
|
9
|
+
// 빈 줄 · # 주석 · !부정 · 끝의 / (폴더만) · 앞의 / (그 자리에 고정) · * · ** · ? · [abc] · \ 이스케이프
|
|
10
|
+
// 슬래시가 없는 패턴은 어느 깊이든 이름으로 맞고, 슬래시가 있으면 그 파일이 있는 폴더 기준이다.
|
|
11
|
+
// 아래 폴더의 .gitignore 는 그 아래에만 적용되고, 뒤에 오는 규칙이 이긴다 (git 과 같다).
|
|
12
|
+
// 건너뛴 폴더 안의 것은 !로도 되살릴 수 없다 (git 도 그렇다 — 폴더째 안 들어간다).
|
|
13
|
+
// 뿌리의 .deelignore 도 같은 문법으로 .gitignore 뒤에 읽는다 — git 에는 안 적고 deel 만 건너뛸 것.
|
|
14
|
+
// 안 하는 것: 작업 폴더 **위**의 .gitignore, .git/info/exclude, core.excludesFile.
|
|
15
|
+
// 윈도우에서는 대소문자를 안 가린다 (globToRegex 와 같다).
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 규칙 파일 하나를 규칙 목록으로.
|
|
21
|
+
* @param {string} text 파일 내용
|
|
22
|
+
* @param {string} 기준 그 파일이 있는 폴더 (작업 폴더 기준 상대, / 구분, 뿌리면 '')
|
|
23
|
+
*/
|
|
24
|
+
export function 규칙읽기(text, 기준 = '') {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const 줄 of String(text ?? '').replace(/\r/g, '').split('\n')) {
|
|
27
|
+
const r = 패턴규칙(줄, 기준);
|
|
28
|
+
if (r) out.push(r);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 줄 하나 → 규칙. 빈 줄·주석이면 null. */
|
|
34
|
+
export function 패턴규칙(줄, 기준 = '') {
|
|
35
|
+
let p = String(줄 ?? '');
|
|
36
|
+
// 끝의 빈칸은 뗀다 — 역슬래시로 살린 것만 남긴다.
|
|
37
|
+
p = p.replace(/(?<!\\)\s+$/, '');
|
|
38
|
+
if (!p || p.startsWith('#')) return null;
|
|
39
|
+
let 부정 = false;
|
|
40
|
+
if (p.startsWith('!')) { 부정 = true; p = p.slice(1); }
|
|
41
|
+
else if (p.startsWith('\\!') || p.startsWith('\\#')) p = p.slice(1);
|
|
42
|
+
if (!p) return null;
|
|
43
|
+
let 폴더만 = false;
|
|
44
|
+
if (p.endsWith('/') && !p.endsWith('\\/')) { 폴더만 = true; p = p.replace(/\/+$/, ''); }
|
|
45
|
+
if (!p) return null;
|
|
46
|
+
let 고정 = false;
|
|
47
|
+
if (p.startsWith('/')) { 고정 = true; p = p.replace(/^\/+/, ''); }
|
|
48
|
+
// 슬래시가 가운데 있어도 그 자리에 고정된다. 없으면 어느 깊이든 이름으로.
|
|
49
|
+
if (p.includes('/')) 고정 = true;
|
|
50
|
+
const 몸 = 글롭정규식(p);
|
|
51
|
+
const src = 고정 ? `^${몸}$` : `(?:^|/)${몸}$`;
|
|
52
|
+
return { re: new RegExp(src, process.platform === 'win32' ? 'i' : ''), 부정, 폴더만, 기준: String(기준 ?? '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''), 원문: 줄 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// gitignore 글롭 → 정규식 몸통 (앞뒤 고정은 부르는 쪽이 붙인다).
|
|
56
|
+
function 글롭정규식(p) {
|
|
57
|
+
let re = '';
|
|
58
|
+
let i = 0;
|
|
59
|
+
while (i < p.length) {
|
|
60
|
+
const ch = p[i];
|
|
61
|
+
if (ch === '\\' && i + 1 < p.length) { re += 글자(p[i + 1]); i += 2; continue; }
|
|
62
|
+
if (ch === '*') {
|
|
63
|
+
if (p[i + 1] === '*') {
|
|
64
|
+
/*
|
|
65
|
+
* `**` 는 **제 자리에 있을 때만** 슬래시를 넘는다.
|
|
66
|
+
*
|
|
67
|
+
* 맨 앞의 별둘 + 슬래시 → 어느 깊이든
|
|
68
|
+
* 맨 뒤의 슬래시 + 별둘 → 그 안의 전부
|
|
69
|
+
* 슬래시 사이의 별둘 → 폴더 0개 이상
|
|
70
|
+
*
|
|
71
|
+
* 그 밖의 자리(`a**b`)는 git 이 그냥 별 하나로 읽는다 — 슬래시를 안
|
|
72
|
+
* 넘는다. 여기서 `.*` 로 읽으면 `a**b` 가 `a/dir/b` 를 걸어 버려서,
|
|
73
|
+
* git 은 그대로 두는 파일을 deel 만 감춘다. 규칙을 적은 사람은
|
|
74
|
+
* 없어진 파일을 찾을 길이 없다.
|
|
75
|
+
*/
|
|
76
|
+
const 앞이경계 = i === 0 || p[i - 1] === '/';
|
|
77
|
+
if (앞이경계 && p[i + 2] === '/') { re += '(?:.*/)?'; i += 3; continue; }
|
|
78
|
+
if (앞이경계 && i + 2 === p.length) { re += '.*'; i += 2; continue; }
|
|
79
|
+
// 붙어 있는 별은 몇 개든 하나로 친다.
|
|
80
|
+
while (p[i] === '*') i += 1;
|
|
81
|
+
re += '[^/]*';
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
re += '[^/]*'; i += 1; continue;
|
|
85
|
+
}
|
|
86
|
+
if (ch === '?') { re += '[^/]'; i += 1; continue; }
|
|
87
|
+
if (ch === '[') {
|
|
88
|
+
const end = p.indexOf(']', i + 1);
|
|
89
|
+
if (end > i + 1) {
|
|
90
|
+
let 안 = p.slice(i + 1, end);
|
|
91
|
+
if (안.startsWith('!')) 안 = '^' + 안.slice(1);
|
|
92
|
+
re += `[${안.replace(/\\/g, '\\\\')}]`;
|
|
93
|
+
i = end + 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
re += '\\['; i += 1; continue;
|
|
97
|
+
}
|
|
98
|
+
re += 글자(ch); i += 1;
|
|
99
|
+
}
|
|
100
|
+
return re;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function 글자(ch) { return ch.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); }
|
|
104
|
+
|
|
105
|
+
// 규칙의 기준 폴더 안에 있는 경로면 그 기준부터의 경로를, 아니면 null.
|
|
106
|
+
function 기준안(rel, 기준) {
|
|
107
|
+
if (!기준) return rel;
|
|
108
|
+
if (rel === 기준) return null; // 기준 폴더 자신은 그 안의 규칙 대상이 아니다
|
|
109
|
+
if (!rel.startsWith(기준 + '/')) return null;
|
|
110
|
+
return rel.slice(기준.length + 1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 경로 하나가 규칙에 걸리는가 — 부모 폴더는 이미 통과했다고 보고 이 경로만 본다. walk 가 쓴다. */
|
|
114
|
+
export function 걸리나(rel, 폴더인가, 규칙들) {
|
|
115
|
+
let 답 = false;
|
|
116
|
+
for (const r of 규칙들) {
|
|
117
|
+
if (r.폴더만 && !폴더인가) continue;
|
|
118
|
+
const sub = 기준안(rel, r.기준);
|
|
119
|
+
if (sub === null) continue;
|
|
120
|
+
if (r.re.test(sub)) 답 = !r.부정;
|
|
121
|
+
}
|
|
122
|
+
return 답;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 경로 하나가 무시되는가 — 부모 폴더까지 본다. 건너뛴 폴더 안의 것은 !로도 못 살린다.
|
|
127
|
+
* @param {string} rel 작업 폴더 기준 상대 경로 (/ 구분)
|
|
128
|
+
*/
|
|
129
|
+
export function 무시하나(rel, 폴더인가, 규칙들) {
|
|
130
|
+
if (!규칙들?.length) return false;
|
|
131
|
+
const 칸 = String(rel).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '').split('/');
|
|
132
|
+
for (let i = 1; i < 칸.length; i++) {
|
|
133
|
+
if (걸리나(칸.slice(0, i).join('/'), true, 규칙들)) return true;
|
|
134
|
+
}
|
|
135
|
+
return 걸리나(칸.join('/'), 폴더인가, 규칙들);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 파일을 읽어 규칙으로. 없거나 못 읽으면 빈 목록. */
|
|
139
|
+
export function 파일규칙읽기(path, 기준 = '') {
|
|
140
|
+
try { return 규칙읽기(readFileSync(path, 'utf8'), 기준); } catch { return []; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 뿌리의 규칙 — .gitignore 다음에 .deelignore. */
|
|
144
|
+
export function 뿌리규칙읽기(root) {
|
|
145
|
+
return [...파일규칙읽기(join(root, '.gitignore'), ''), ...파일규칙읽기(join(root, '.deelignore'), '')];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 뿌리부터 이 폴더까지 오면서 만나는 규칙을 다 모은다.
|
|
150
|
+
*
|
|
151
|
+
* walk() 는 내려가면서 폴더마다 더하면 되지만, @폴더 처럼 중간을 바로 들여다보는
|
|
152
|
+
* 쪽은 자기 위쪽을 스스로 되짚어야 한다. 안 그러면 sub/.gitignore 가 있는데도
|
|
153
|
+
* @sub 로 지목했을 때만 규칙이 없는 것처럼 보인다 — 같은 폴더가 도구마다 다르게
|
|
154
|
+
* 보이는 것이 제일 나쁘다.
|
|
155
|
+
*/
|
|
156
|
+
export function 계보규칙읽기(root, rel = '') {
|
|
157
|
+
const 규칙 = 뿌리규칙읽기(root);
|
|
158
|
+
const 조각 = String(rel ?? '').replace(/\\/g, '/').split('/').filter((x) => x && x !== '.');
|
|
159
|
+
let 여기 = '';
|
|
160
|
+
for (const 한칸 of 조각) {
|
|
161
|
+
여기 = 여기 ? `${여기}/${한칸}` : 한칸;
|
|
162
|
+
규칙.push(...파일규칙읽기(join(root, ...여기.split('/'), '.gitignore'), 여기));
|
|
163
|
+
}
|
|
164
|
+
return 규칙;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 화면 한 줄. 건너뛴 것이 없으면 빈 문자열 — 없는데 줄을 만들면 그게 소음이다. */
|
|
168
|
+
/**
|
|
169
|
+
* @param 건너뜀 `{ 폴더, 파일 }` — .gitignore 로 뺀 수
|
|
170
|
+
* @param 잘림 상한에서 멈췄나 (walk 가 붙여 준다)
|
|
171
|
+
* @param 상한 몇 개까지 봤나. 0 이면 수 없이 말한다.
|
|
172
|
+
* (숫자를 인자로 받는다 — fsutil 이 ignore 를 부르므로 거꾸로 가져오면 고리가 된다)
|
|
173
|
+
*/
|
|
174
|
+
export function 건너뜀말(건너뜀, 잘림 = false, 상한 = 0) {
|
|
175
|
+
const 폴더 = 건너뜀?.폴더 ?? 0;
|
|
176
|
+
const 파일 = 건너뜀?.파일 ?? 0;
|
|
177
|
+
const 줄 = [];
|
|
178
|
+
if (폴더 || 파일) {
|
|
179
|
+
const 몫 = [폴더 ? `폴더 ${폴더}개` : null, 파일 ? `파일 ${파일}개` : null].filter(Boolean).join(' · ');
|
|
180
|
+
줄.push(`(.gitignore 로 ${몫} 건너뜀 — 경로를 직접 주면 Read 된다)`);
|
|
181
|
+
}
|
|
182
|
+
/*
|
|
183
|
+
* 상한에서 멈췄으면 반드시 말한다.
|
|
184
|
+
*
|
|
185
|
+
* 이 한 줄이 없으면 "일치 없음" 이 "없다" 로 읽힌다. 실제로는 안 본 것이다.
|
|
186
|
+
*/
|
|
187
|
+
if (잘림) {
|
|
188
|
+
const 몇 = 상한 ? `${상한.toLocaleString('en-US')}개까지만` : '앞부분만';
|
|
189
|
+
줄.push(`(파일이 너무 많아 ${몇} 봤습니다 — 못 본 자리에 있는 것은 여기 안 나옵니다. 폴더를 좁혀서 다시 시켜 보세요)`);
|
|
190
|
+
}
|
|
191
|
+
return 줄.length ? `\n\n${줄.join('\n')}` : '';
|
|
192
|
+
}
|