deel-local-cli 1.5.8 → 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 +23 -13
- package/README.md +22 -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 +78 -6
- package/src/report.js +26 -2
- package/src/safety/guard.js +13 -2
- 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 +226 -36
- 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
package/src/setup.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// 첫 실행 마법사 + 진단 실행.
|
|
2
2
|
import { c, say, rule, mark } from './ui/ansi.js';
|
|
3
|
+
import { 주소가리기 } from './safety/secrets.js';
|
|
3
4
|
import { ask, pick } from './ui/prompt.js';
|
|
4
5
|
import { spin } from './ui/spinner.js';
|
|
5
6
|
import { detect } from './backend/detect.js';
|
|
6
7
|
import { probe } from './backend/probe.js';
|
|
7
8
|
import { renderHeader, renderLine, verdict, renderVerdict, plainReport } from './report.js';
|
|
8
9
|
import { load, save, upsert, slug, resolveKey, activeProfile, configPath } from './config.js';
|
|
10
|
+
import { 보관방식 } from './safety/keystore.js';
|
|
11
|
+
import { 애저풀기, 애저base } from './backend/azure.js';
|
|
9
12
|
import { allowEndpoint } from './safety/network.js';
|
|
10
13
|
import { writeFileSync } from 'node:fs';
|
|
11
14
|
|
|
@@ -18,14 +21,22 @@ export function banner() {
|
|
|
18
21
|
// 주소와 키를 받아 연결을 찾아낸다. 실패하면 null.
|
|
19
22
|
async function connect(url, key) {
|
|
20
23
|
const s = spin(`${url} 확인 중...`);
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
/*
|
|
25
|
+
* 사용자가 방금 적어 넣은 주소다. 확인하는 동안만 문을 연다.
|
|
26
|
+
*
|
|
27
|
+
* 스킴을 안 적었으면 **두 가지 다 열어 둔다.** 자리마다 기본으로 붙이는
|
|
28
|
+
* 스킴이 다르기 때문이다 — 로컬 서버를 찾는 쪽은 `http://` 를, Azure 쪽은
|
|
29
|
+
* `https://` 를 붙인다. 한쪽만 열어 두면 우리가 만든 주소를 우리 자물쇠가
|
|
30
|
+
* 막고, 화면에는 "허용되지 않은 주소" 만 뜬다. 여는 것은 사람이 적어 넣은
|
|
31
|
+
* 그 호스트 하나뿐이라 넓어지는 것이 아니다.
|
|
32
|
+
*/
|
|
33
|
+
allowEndpoint(/^https?:\/\//i.test(url) ? url : [`http://${url}`, `https://${url}`]);
|
|
23
34
|
const found = await detect(url, key);
|
|
24
35
|
if (!found.kind) {
|
|
25
36
|
s.stop(` ${mark.no} ${c.red('연결 실패')}`);
|
|
26
37
|
say('');
|
|
27
38
|
say(` 시도한 주소:`);
|
|
28
|
-
for (const t of found.tried) say(` ${c.gray(t
|
|
39
|
+
for (const t of found.tried) say(` ${c.gray(주소가리기(t.includes('?') ? t : `${t}/models`))}`);
|
|
29
40
|
say('');
|
|
30
41
|
say(` ${c.gray('확인할 것 — 주소·포트가 맞는지, 프록시가 필요한지,')}`);
|
|
31
42
|
say(` ${c.gray('사내 인증서라면 NODE_EXTRA_CA_CERTS 환경변수가 필요합니다.')}`);
|
|
@@ -34,7 +45,8 @@ async function connect(url, key) {
|
|
|
34
45
|
}
|
|
35
46
|
const kindName = found.kind === 'ollama' ? `Ollama ${found.version ?? ''}`.trim() : 'OpenAI 호환';
|
|
36
47
|
s.stop(` ${mark.ok} ${c.green('연결됨')} ${c.gray(`${kindName} · ${found.ms}ms`)}`);
|
|
37
|
-
|
|
48
|
+
// 물음표 뒤에 열쇠를 싣는 앞단이 있다 (safety/secrets.js 의 주소가리기).
|
|
49
|
+
say(` ${c.gray('주소')} ${주소가리기(found.base)}`);
|
|
38
50
|
say(` ${c.gray('인증')} ${found.auth === 'none' ? '없음' : found.auth}`);
|
|
39
51
|
say(` ${c.gray('모델')} ${found.models.length}개 발견`);
|
|
40
52
|
if (found.warn) say(` ${mark.warn} ${c.yellow(found.warn)}`);
|
|
@@ -70,6 +82,10 @@ export async function runProbe(conn, { out = null } = {}) {
|
|
|
70
82
|
|
|
71
83
|
export async function runSetup() {
|
|
72
84
|
banner();
|
|
85
|
+
// 설정에 적어 둔 api-version 을 **붙기 전에** 읽는다. load() 가 애저정하기()를
|
|
86
|
+
// 부른다. 이걸 뒤에서 하면 사내에서 판을 고정해 둔 곳이 확인만 GA판으로 하고,
|
|
87
|
+
// 문서에 적어 둔 대로 안 도는 셈이 된다.
|
|
88
|
+
load();
|
|
73
89
|
say(` ${c.gray('모델 연결을 설정합니다. 주소와 키만 있으면 됩니다.')}`);
|
|
74
90
|
say('');
|
|
75
91
|
|
|
@@ -85,6 +101,18 @@ export async function runSetup() {
|
|
|
85
101
|
const model = await chooseModel(found);
|
|
86
102
|
if (!model) { say(` ${mark.no} 모델이 비었습니다.`); return 1; }
|
|
87
103
|
|
|
104
|
+
/*
|
|
105
|
+
* Azure 는 모델 이름이 **주소 안에** 있다.
|
|
106
|
+
*
|
|
107
|
+
* 목록에서 다른 배포를 골랐으면 주소도 그 배포로 바꿔야 한다. 안 바꾸면
|
|
108
|
+
* 화면에는 고른 이름이 보이는데 요청은 처음 주소의 배포로 나간다 —
|
|
109
|
+
* 사람이 알아챌 방법이 없는 어긋남이다.
|
|
110
|
+
*/
|
|
111
|
+
if (found.azure) {
|
|
112
|
+
const 푼것 = 애저풀기(found.base);
|
|
113
|
+
if (푼것) found.base = 애저base(푼것.origin, model, 푼것.판);
|
|
114
|
+
}
|
|
115
|
+
|
|
88
116
|
const conn = { kind: found.kind, base: found.base, auth: found.auth, key, model };
|
|
89
117
|
const { facts } = await runProbe(conn);
|
|
90
118
|
|
|
@@ -102,14 +130,18 @@ export async function runSetup() {
|
|
|
102
130
|
tools: facts.tools ?? false,
|
|
103
131
|
json: facts.json ?? false,
|
|
104
132
|
think: facts.think ?? false,
|
|
133
|
+
vision: facts.vision ?? false,
|
|
105
134
|
});
|
|
106
135
|
cfg.active = id;
|
|
107
136
|
const p = save(cfg);
|
|
108
137
|
|
|
109
138
|
say(` ${mark.ok} 저장됨 ${c.cyan(p)}`);
|
|
110
139
|
if (key) {
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
// 열쇠가 파일에 '어떤 꼴로' 들어갔는지를 그대로 적는다. 잠겼는지 아닌지를
|
|
141
|
+
// 사람이 짐작하게 두면, 안 잠긴 파일을 잠긴 줄 알고 아무 데나 둔다.
|
|
142
|
+
const 저장한것 = activeProfile(load())?.apiKey ?? null;
|
|
143
|
+
say(` ${c.gray(`열쇠 보관 — ${보관방식(저장한것)}`)}`);
|
|
144
|
+
say(` ${c.gray('파일에 아예 안 남기려면 환경변수 DEEL_API_KEY 로 넣으세요 — 그쪽이 우선합니다.')}`);
|
|
113
145
|
}
|
|
114
146
|
say('');
|
|
115
147
|
return 0;
|
|
@@ -121,6 +153,7 @@ export async function runDiagnose(flags) {
|
|
|
121
153
|
let conn;
|
|
122
154
|
|
|
123
155
|
if (flags.url) {
|
|
156
|
+
load(); // 설정의 api-version 을 먼저 읽는다 (runSetup 과 같은 이유)
|
|
124
157
|
const key = flags.key ?? process.env.DEEL_API_KEY ?? '';
|
|
125
158
|
const found = await connect(flags.url, key);
|
|
126
159
|
if (!found) return 1;
|
|
@@ -167,7 +200,7 @@ export async function showStatus() {
|
|
|
167
200
|
p.think ? c.green('추론') : c.gray('추론'),
|
|
168
201
|
].join(c.gray('·'));
|
|
169
202
|
say(` ${c.bold(p.name)} ${c.gray(p.model)} ${caps}${here}`);
|
|
170
|
-
say(` ${c.gray(p.baseUrl)}`);
|
|
203
|
+
say(` ${c.gray(주소가리기(p.baseUrl))}`);
|
|
171
204
|
}
|
|
172
205
|
say('');
|
|
173
206
|
say(` ${c.gray('설정 파일')} ${configPath()}`);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 클립보드에 든 그림 꺼내기 — 화면 캡처 → 바로 붙이기.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 눈이 달린 모델에게 화면을 보여 줄 수 있게 됐는데(backend/vision.js),
|
|
7
|
+
* 보여 주려면 먼저 **파일로 저장해야** 했다. 오류 화면 하나를 물어보려고
|
|
8
|
+
* 캡처 → 그림판 → 저장 → 경로 찾기를 거친다. 그 네 걸음이 「그냥 말로
|
|
9
|
+
* 설명하자」로 사람을 되돌린다.
|
|
10
|
+
*
|
|
11
|
+
* 캡처는 이미 클립보드에 있다. 꺼내 오기만 하면 된다.
|
|
12
|
+
*
|
|
13
|
+
* ── 왜 의존성 없이 되나 ─────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* 세 운영체제 모두 **이미 깔려 있는 것**으로 꺼낸다. 새로 깔라고 하지 않는다.
|
|
16
|
+
*
|
|
17
|
+
* 윈도우 파워셸의 .NET Clipboard (윈도우에 늘 있다)
|
|
18
|
+
* 맥 osascript (맥에 늘 있다)
|
|
19
|
+
* 리눅스 wl-paste 나 xclip — 배포판에 따라 없을 수 있다
|
|
20
|
+
*
|
|
21
|
+
* ── 없으면 없다고 한다 ──────────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* 리눅스에서 둘 다 없으면 **어떻게 하면 되는지**를 같이 준다. 「안 됩니다」
|
|
24
|
+
* 로 끝내면 사람은 이 기능이 고장 난 줄 알지, 도구 하나 깔면 되는 줄 모른다.
|
|
25
|
+
* 클립보드에 글만 있을 때도 「그림이 없다」와 「못 꺼냈다」를 갈라 말한다 —
|
|
26
|
+
* 앞의 것은 사람이 캡처를 다시 하면 되고, 뒤의 것은 우리 탓이다.
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, statSync } from 'node:fs';
|
|
30
|
+
import { tmpdir } from 'node:os';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
/** 이보다 큰 그림은 안 받는다. 화면 캡처 한 장은 보통 1MB 안쪽이다. */
|
|
34
|
+
export const 그림한도 = 12 * 1024 * 1024;
|
|
35
|
+
|
|
36
|
+
const 있나 = (이름, 인자) => {
|
|
37
|
+
const r = spawnSync(이름, 인자, { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
38
|
+
return !r.error && r.status === 0;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* 윈도우.
|
|
43
|
+
*
|
|
44
|
+
* 파워셸을 **파일로 적어 두고** 부른다. `-Command` 뒤에 길게 붙이면 따옴표가
|
|
45
|
+
* 셸을 두 번 거치면서 깨지고, 그 깨짐이 PC 마다 다르게 나타난다. 스크립트는
|
|
46
|
+
* ASCII 로만 쓴다 — Windows PowerShell 5.1 은 BOM 없는 .ps1 을 그 PC 의 옛
|
|
47
|
+
* 코드페이지로 읽어서, 한글이 섞이면 문법이 무너진다(src/completion.js 참고).
|
|
48
|
+
*/
|
|
49
|
+
function 윈도우에서(방) {
|
|
50
|
+
const 낼곳 = join(방, 'clip.png');
|
|
51
|
+
const 스크립트 = join(방, 'grab.ps1');
|
|
52
|
+
const 글 = [
|
|
53
|
+
'$ErrorActionPreference = "Stop"',
|
|
54
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
55
|
+
'Add-Type -AssemblyName System.Drawing',
|
|
56
|
+
'$img = [System.Windows.Forms.Clipboard]::GetImage()',
|
|
57
|
+
'if ($img -eq $null) { Write-Output "NOIMAGE"; exit 0 }',
|
|
58
|
+
`$img.Save(${JSON.stringify(낼곳)}, [System.Drawing.Imaging.ImageFormat]::Png)`,
|
|
59
|
+
'Write-Output "OK"',
|
|
60
|
+
].join('\n');
|
|
61
|
+
// Buffer 로 적어서 인코딩이 끼어들 자리를 없앤다.
|
|
62
|
+
writeFileSync(스크립트, Buffer.from(글, 'latin1'));
|
|
63
|
+
|
|
64
|
+
const r = spawnSync('powershell', [
|
|
65
|
+
'-NoProfile', '-NonInteractive', '-STA', '-ExecutionPolicy', 'Bypass', '-File', 스크립트,
|
|
66
|
+
], { encoding: 'utf8', timeout: 20000, windowsHide: true });
|
|
67
|
+
|
|
68
|
+
if (r.error) return { ok: false, 왜: `파워셸을 못 불렀습니다: ${r.error.message}` };
|
|
69
|
+
const 낸말 = String(r.stdout ?? '').trim();
|
|
70
|
+
if (/NOIMAGE/.test(낸말)) return { ok: false, 없음: true };
|
|
71
|
+
if (!existsSync(낼곳)) {
|
|
72
|
+
const 탈 = String(r.stderr ?? '').split('\n').find((l) => l.trim()) ?? '';
|
|
73
|
+
return { ok: false, 왜: 탈 ? `클립보드를 못 읽었습니다: ${탈.trim()}` : '클립보드를 못 읽었습니다' };
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, buf: readFileSync(낼곳), mime: 'image/png' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* 맥.
|
|
80
|
+
*
|
|
81
|
+
* osascript 로 PNG 를 꺼내면 «data PNGf89504e47…» 꼴의 16진수 글이 나온다.
|
|
82
|
+
* 그림이 없으면 오류를 내므로, 그것으로 「없음」을 가른다.
|
|
83
|
+
*/
|
|
84
|
+
function 맥에서() {
|
|
85
|
+
const r = spawnSync('osascript', ['-e', 'the clipboard as «class PNGf»'], {
|
|
86
|
+
encoding: 'utf8', timeout: 20000,
|
|
87
|
+
});
|
|
88
|
+
if (r.error) return { ok: false, 왜: `osascript 를 못 불렀습니다: ${r.error.message}` };
|
|
89
|
+
const 낸말 = String(r.stdout ?? '');
|
|
90
|
+
const m = /«data PNGf([0-9A-Fa-f]+)»/.exec(낸말);
|
|
91
|
+
if (!m) {
|
|
92
|
+
// 그림이 아니면 osascript 가 오류를 낸다. 그건 「없음」이지 고장이 아니다.
|
|
93
|
+
if (r.status !== 0) return { ok: false, 없음: true };
|
|
94
|
+
return { ok: false, 없음: true };
|
|
95
|
+
}
|
|
96
|
+
return { ok: true, buf: Buffer.from(m[1], 'hex'), mime: 'image/png' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* 리눅스.
|
|
101
|
+
*
|
|
102
|
+
* 웨이랜드면 wl-paste, X11 이면 xclip. 배포판이 기본으로 안 깔아 주는 것이라
|
|
103
|
+
* 없을 수 있고, 그때는 **무엇을 깔면 되는지** 알려 준다.
|
|
104
|
+
*/
|
|
105
|
+
function 리눅스에서() {
|
|
106
|
+
const 후보 = [
|
|
107
|
+
{ 이름: 'wl-paste', 볼것: ['--list-types'], 꺼내기: ['--type', 'image/png'] },
|
|
108
|
+
{ 이름: 'xclip', 볼것: ['-selection', 'clipboard', '-t', 'TARGETS', '-o'], 꺼내기: ['-selection', 'clipboard', '-t', 'image/png', '-o'] },
|
|
109
|
+
];
|
|
110
|
+
const 있는것 = 후보.filter((x) => 있나(x.이름, ['--version']) || 있나(x.이름, ['-version']));
|
|
111
|
+
if (!있는것.length) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
왜: '클립보드에서 그림을 꺼낼 도구가 없습니다.\n'
|
|
115
|
+
+ ' 웨이랜드면 sudo apt install wl-clipboard\n'
|
|
116
|
+
+ ' X11 이면 sudo apt install xclip\n'
|
|
117
|
+
+ ' 둘 다 안 되면 캡처를 파일로 저장한 뒤 @경로 로 붙이세요.',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
for (const x of 있는것) {
|
|
121
|
+
const 종류 = spawnSync(x.이름, x.볼것, { encoding: 'utf8', timeout: 10000 });
|
|
122
|
+
if (!/image\/png/i.test(String(종류.stdout ?? ''))) continue;
|
|
123
|
+
const r = spawnSync(x.이름, x.꺼내기, { timeout: 20000, maxBuffer: 64 * 1024 * 1024 });
|
|
124
|
+
if (r.error || !r.stdout?.length) continue;
|
|
125
|
+
return { ok: true, buf: Buffer.from(r.stdout), mime: 'image/png' };
|
|
126
|
+
}
|
|
127
|
+
return { ok: false, 없음: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 클립보드에 그림이 있으면 꺼낸다.
|
|
132
|
+
*
|
|
133
|
+
* @returns {{ok:true, buf:Buffer, mime:string}
|
|
134
|
+
* | {ok:false, 없음:true} 그림이 없다 (사람이 다시 캡처하면 된다)
|
|
135
|
+
* | {ok:false, 왜:string}} 못 꺼냈다 (까닭과 길을 같이 준다)
|
|
136
|
+
*/
|
|
137
|
+
export function 클립보드그림({ platform = process.platform } = {}) {
|
|
138
|
+
let 방 = null;
|
|
139
|
+
try {
|
|
140
|
+
if (platform === 'win32') {
|
|
141
|
+
방 = mkdtempSync(join(tmpdir(), 'deel-clip-'));
|
|
142
|
+
const r = 윈도우에서(방);
|
|
143
|
+
return 잰다(r);
|
|
144
|
+
}
|
|
145
|
+
if (platform === 'darwin') return 잰다(맥에서());
|
|
146
|
+
if (platform === 'linux') return 잰다(리눅스에서());
|
|
147
|
+
return { ok: false, 왜: `${platform} 에서는 아직 클립보드 그림을 못 꺼냅니다 — 파일로 저장한 뒤 @경로 로 붙이세요.` };
|
|
148
|
+
} catch (err) {
|
|
149
|
+
return { ok: false, 왜: `클립보드를 읽다 막혔습니다: ${err.message}` };
|
|
150
|
+
} finally {
|
|
151
|
+
if (방) { try { rmSync(방, { recursive: true, force: true }); } catch { /* 치우다 실패는 넘어간다 */ } }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 크기를 여기서 한 번만 잰다 — 운영체제마다 따로 재면 한 군데를 빠뜨린다. */
|
|
156
|
+
function 잰다(r) {
|
|
157
|
+
if (!r?.ok) return r;
|
|
158
|
+
if (!r.buf?.length) return { ok: false, 없음: true };
|
|
159
|
+
if (r.buf.length > 그림한도) {
|
|
160
|
+
return { ok: false, 왜: `그림이 너무 큽니다 (${(r.buf.length / 1048576).toFixed(1)}MB · 한도 ${그림한도 / 1048576}MB)` };
|
|
161
|
+
}
|
|
162
|
+
return r;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 꺼낸 그림을 파일로 앉힌다.
|
|
167
|
+
*
|
|
168
|
+
* 살림 폴더(.deel/붙인그림/) 안에 둔다. 작업 폴더에 흩뿌리면 사람이 지우기
|
|
169
|
+
* 전에 커밋에 딸려 들어가고, 임시 폴더에 두면 대화를 다시 열었을 때 사라진다.
|
|
170
|
+
*/
|
|
171
|
+
export function 그림앉히기(buf, 살림, { 이제 = new Date() } = {}) {
|
|
172
|
+
const 방 = join(살림, '붙인그림');
|
|
173
|
+
mkdirSync(방, { recursive: true });
|
|
174
|
+
const 이름 = `${이제.toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19)}.png`;
|
|
175
|
+
const 자리 = join(방, 이름);
|
|
176
|
+
writeFileSync(자리, buf);
|
|
177
|
+
return { 자리, 이름, 바이트: statSync(자리).size };
|
|
178
|
+
}
|
|
@@ -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
|
|