deel-local-cli 2.0.0 → 2.0.3
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.ko.md +14 -5
- package/README.md +14 -5
- package/package.json +1 -1
- package/src/acp/serve.js +49 -12
- package/src/agent/commit.js +2 -2
- package/src/agent/compact.js +4 -4
- package/src/agent/grade.js +1 -1
- package/src/agent/loop.js +2 -2
- package/src/agent/memory.js +1 -1
- package/src/agent/models.js +1 -1
- package/src/agent/outschema.js +1 -1
- package/src/agent/pins.js +7 -2
- package/src/agent/project.js +1 -1
- package/src/agent/recall.js +1 -1
- package/src/agent/store.js +13 -26
- package/src/backend/ctxsize.js +2 -2
- package/src/backend/http.js +1 -1
- package/src/backend/learn.js +27 -1
- package/src/backend/mcp.js +97 -16
- package/src/backend/proxy.js +1 -1
- package/src/backend/quota.js +2 -2
- package/src/commands/common.js +2 -2
- package/src/commands/extend.js +0 -1
- package/src/commands/model.js +1 -2
- package/src/commands.js +4 -2
- package/src/completion.js +13 -3
- package/src/config.js +35 -13
- package/src/configexplain.js +3 -2
- package/src/doctor.js +1 -2
- package/src/jsonl.js +42 -0
- package/src/lsp/client.js +6 -46
- package/src/oneshot.js +22 -0
- package/src/pack/selfpack.js +2 -2
- package/src/pack/zip.js +18 -1
- package/src/plugins/manage.js +81 -14
- package/src/reap.js +70 -0
- package/src/report.js +1 -1
- package/src/safety/audit.js +4 -19
- package/src/safety/authcmd.js +1 -1
- package/src/safety/guard.js +81 -7
- package/src/safety/hooks.js +1 -1
- package/src/safety/keystore.js +18 -3
- package/src/safety/shellenv.js +1 -1
- package/src/safety/undo.js +44 -20
- package/src/skills/discover.js +1 -1
- package/src/tools/doc2md.js +25 -3
- package/src/tools/docs.js +3 -3
- package/src/tools/edit-match.js +1 -1
- package/src/tools/encoding.js +1 -1
- package/src/tools/excel-com.js +56 -30
- package/src/tools/fastgrep.js +65 -10
- package/src/tools/fsutil.js +8 -4
- package/src/tools/ignore.js +21 -1
- package/src/tools/index.js +77 -7
- package/src/tools/jobs.js +27 -5
- package/src/tools/pdf.js +1 -3
- package/src/tools/shell.js +27 -0
- package/src/tools/todo.js +1 -1
- package/src/tools/verify.js +17 -3
- package/src/tools/webfetch.js +12 -2
- package/src/tools/xlsx.js +4 -4
- package/src/ui/banner.js +1 -1
- package/src/ui/inputbox.js +1 -1
- package/src/ui/md.js +1 -1
- package/src/ui/motion.js +1 -1
package/src/skills/discover.js
CHANGED
|
@@ -38,7 +38,7 @@ export const 내장자리 = join(dirname(fileURLToPath(import.meta.url)), 'built
|
|
|
38
38
|
* 앞머리가 몇 백 줄일 까닭은 없어서 찾는 줄 수에도 상한을 둔다. 같은 열쇠가 두 번 오면
|
|
39
39
|
* 첫 것을 둔다 — 뒤엣것은 대개 본문이 새어 들어온 것이다.
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
const 앞머리최대줄 = 200;
|
|
42
42
|
const 금인가 = (줄) => /^---[ \t]*\r?$/.test(줄);
|
|
43
43
|
// YAML 앞머리에 설 수 있는 줄 꼴 — 빈 줄 · 들여 쓴 줄 · 주석 · 목록 · `열쇠:` 줄.
|
|
44
44
|
const 앞머리줄인가 = (줄) => {
|
package/src/tools/doc2md.js
CHANGED
|
@@ -50,12 +50,34 @@ const 최대글자 = 60000;
|
|
|
50
50
|
// 이보다 칸이 많으면 마크다운 표로 안 그린다 — 가로로 흐르는 벽이 된다.
|
|
51
51
|
const 최대칸 = 12;
|
|
52
52
|
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* 마크다운 표 칸 하나. `|` 와 줄바꿈은 표를 부수므로 바꿔 준다.
|
|
55
|
+
*
|
|
56
|
+
* 백슬래시는 **끝엣것만** 짝을 맞춘다. 칸이 백슬래시로 끝나면 그것이 뒤따르는
|
|
57
|
+
* 칸 가름표(`|`)를 먹어 버려 표가 그 줄부터 통째로 어긋난다 — 윈도우 경로가
|
|
58
|
+
* 든 표에서 바로 나온다.
|
|
59
|
+
*
|
|
60
|
+
* 모든 백슬래시를 벗기려다 한 번 되돌렸다(2차 눈). 그러면 칸 안의
|
|
61
|
+
* `` `\d+` `` 같은 글이 `` `\\d+` `` 로 **불어난다.** 가운데 백슬래시는
|
|
62
|
+
* 표를 안 부수니 건드릴 까닭이 없다. 부수는 것은 끝엣것뿐이다.
|
|
63
|
+
*
|
|
64
|
+
* ── `|` 바로 앞의 백슬래시도 짝을 맞춘다 (20회차 CodeQL #140) ────────────
|
|
65
|
+
*
|
|
66
|
+
* `.replace(/\|/g, '\\|')` 하나만으로는 모자랐다. 칸에 `a\|b` 가 들어오면
|
|
67
|
+
* 우리가 백슬래시를 하나 더해 `a\\|b` 가 되는데, 마크다운은 그것을
|
|
68
|
+
* **「백슬래시 하나」 + 살아 있는 칸 가름표**로 읽는다. 막으려던 바로 그
|
|
69
|
+
* 일이 우리 손으로 일어난다 — 가름표 앞에 백슬래시가 홀수 개면 언제나 그렇다.
|
|
70
|
+
*
|
|
71
|
+
* 그래서 `|` 앞에 붙은 백슬래시 묶음을 먼저 두 배로 늘리고 나서 `|` 를
|
|
72
|
+
* 벗긴다. 그러면 가름표 앞 백슬래시 수가 늘 짝수라 벗김표가 살아남는다.
|
|
73
|
+
* `|` 가 없는 자리의 백슬래시는 그대로 두므로 위의 `` `\d+` `` 도 안 는다.
|
|
74
|
+
*/
|
|
54
75
|
function 칸(v) {
|
|
55
76
|
return String(v ?? '')
|
|
56
|
-
.replace(
|
|
77
|
+
.replace(/(\\*)\|/g, (m, 앞) => 앞 + 앞 + '\\|')
|
|
57
78
|
.replace(/\r?\n/g, '<br>')
|
|
58
|
-
.trim()
|
|
79
|
+
.trim()
|
|
80
|
+
.replace(/\\+$/, (m) => m + m);
|
|
59
81
|
}
|
|
60
82
|
|
|
61
83
|
/** 이 행들이 다 비었나. 빈 표를 그리면 머리글만 있는 유령 표가 남는다. */
|
package/src/tools/docs.js
CHANGED
|
@@ -84,7 +84,7 @@ const 서명들 = [
|
|
|
84
84
|
];
|
|
85
85
|
|
|
86
86
|
/** 앞 몇 바이트로 정체를 짚는다. 모르면 null. */
|
|
87
|
-
|
|
87
|
+
function 속내용(buf) {
|
|
88
88
|
if (!Buffer.isBuffer(buf) || buf.length < 2) return null;
|
|
89
89
|
for (const s of 서명들) {
|
|
90
90
|
if (s.바이트.every((b, i) => buf[i] === b)) return s.이름;
|
|
@@ -103,7 +103,7 @@ export function 속내용(buf) {
|
|
|
103
103
|
* @param {string} 갈래 겉으로 본 갈래 (pptx · docx · hwpx)
|
|
104
104
|
* @param {string|null} 속 속내용() 이 짚은 것
|
|
105
105
|
*/
|
|
106
|
-
|
|
106
|
+
function 겉속다름말(갈래, 속) {
|
|
107
107
|
const 머리 = `겉은 ${갈래} 인데 속이 다릅니다`;
|
|
108
108
|
if (!속) {
|
|
109
109
|
return `${머리} — 무엇인지 알아보지 못했습니다 (zip 꾸러미가 아닙니다).\n`
|
|
@@ -182,7 +182,7 @@ const 끝이름 = (name) => {
|
|
|
182
182
|
* (tools/doc2md.js) — 마크다운 표는 칸이 몇 개인지 알아야 그릴 수 있고,
|
|
183
183
|
* 이어 붙인 한 줄에서 그걸 되짚으면 글에 든 | 하나에 표가 어긋난다.
|
|
184
184
|
*/
|
|
185
|
-
|
|
185
|
+
function 문단뽑기(xml, 갈래, { 표를따로 = false } = {}) {
|
|
186
186
|
const 표기 = 이름표[갈래];
|
|
187
187
|
const 문단들 = [];
|
|
188
188
|
|
package/src/tools/edit-match.js
CHANGED
|
@@ -224,7 +224,7 @@ export function findMatch(text, needle, { replaceAll = false } = {}) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// 못 찾았을 때, 모델이 스스로 고칠 수 있게 "가장 비슷한 줄"을 알려준다.
|
|
227
|
-
|
|
227
|
+
function nearest(text, needle) {
|
|
228
228
|
const key = String(needle).split(/\r?\n/).find((l) => l.trim()) ?? '';
|
|
229
229
|
const probe = key.trim().replace(/\s+/g, ' ');
|
|
230
230
|
if (probe.length < 4) return null;
|
package/src/tools/encoding.js
CHANGED
|
@@ -581,7 +581,7 @@ export function detect(buf, { fallback = null, system = null, 잘림 = false } =
|
|
|
581
581
|
|
|
582
582
|
// 이 컴퓨터가 쓰는 옛 인코딩. 콘솔 코드페이지를 물어봐서 정한다.
|
|
583
583
|
let _sys = null;
|
|
584
|
-
|
|
584
|
+
function systemLegacy() {
|
|
585
585
|
if (_sys) return _sys;
|
|
586
586
|
const cp = consoleCodepage();
|
|
587
587
|
_sys = LEGACY.find((x) => x.cp === cp)?.id ?? 'windows-1252';
|
package/src/tools/excel-com.js
CHANGED
|
@@ -53,6 +53,10 @@ $dst = $env:DEEL_XL_OUT
|
|
|
53
53
|
# 그러면 맞는 암호를 넣은 사람이 「암호가 맞지 않습니다」 를 세 번 받고 끝난다. 나가는 쪽은
|
|
54
54
|
# 이미 decode() 로 콘솔 인코딩을 풀고 있는데(아래 받은글), 들어오는 쪽만 빠져 있었다.
|
|
55
55
|
try { [Console]::InputEncoding = New-Object System.Text.UTF8Encoding $false } catch { }
|
|
56
|
+
# 나가는 쪽도 UTF-8 로 (2.0.2 · XL3). 콘솔 코드페이지로 두면 그 판에 없는 글자(일본어 엑셀의 「パスワード」,
|
|
57
|
+
# 중국어 엑셀의 「密码」, 한글 판에 없는 시트 이름)가 「?」 가 되고, 받는 쪽 decode() 가 인코딩을 잘못 짚어
|
|
58
|
+
# 한 줄이 통째로 딴 글자가 됐다 — 그러면 암호를 틀렸다는 말도 못 알아본다.
|
|
59
|
+
try { [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false } catch { }
|
|
56
60
|
$pw = [Console]::In.ReadLine()
|
|
57
61
|
if ($null -eq $pw) { $pw = '' }
|
|
58
62
|
|
|
@@ -89,8 +93,8 @@ try {
|
|
|
89
93
|
catch {
|
|
90
94
|
$m = $_.Exception.Message
|
|
91
95
|
# 작은따옴표는 PowerShell 에서 이스케이프를 안 푼다. 탭을 넣으려면 큰따옴표여야 한다.
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
# 「암호가 틀렸다」 는 말은 이 PC 언어로 온다 — 가르는 일은 JS 쪽 암호탓인가 가 한다 (2.0.2 · XL3).
|
|
97
|
+
if ($m -eq 'BUSY') { Write-Output "ERR\`tBUSY" }
|
|
94
98
|
elseif ($m -eq 'OPENFAIL') { Write-Output "ERR\`tPASSWORD" }
|
|
95
99
|
else { Write-Output ("ERR\`tOTHER\`t" + $m) }
|
|
96
100
|
}
|
|
@@ -191,6 +195,53 @@ export async function excelToTables(경로, { password = '', timeout = 90000 } =
|
|
|
191
195
|
}
|
|
192
196
|
}
|
|
193
197
|
|
|
198
|
+
/**
|
|
199
|
+
* 엑셀이 「암호가 틀렸다」 고 한 말인가 (2.0.2 · XL3).
|
|
200
|
+
*
|
|
201
|
+
* 그 말은 **이 컴퓨터 언어로** 온다. 여기가 파워셸 안의 `'password|암호|protected'` 였는데,
|
|
202
|
+
* 일본어 엑셀은 「パスワード」, 중국어는 「密码」, 독일어는 「Kennwort」 라고 말한다 — 그러면 암호를
|
|
203
|
+
* 틀린 사람이 「암호가 맞지 않습니다」 대신 「엑셀이 열지 못했습니다: …」 를 받고, 모델은 암호를
|
|
204
|
+
* 다시 물을 줄 모른다. 판정은 이쪽에서 한 번만 한다 — 엑셀 없는 판에서도 잴 수 있게.
|
|
205
|
+
*/
|
|
206
|
+
const 암호말 = /password|protected|암호|パスワード|密码|密碼|kennwort|mot de passe|contraseña|senha|palavra-passe|пароль|wachtwoord|hasło|parola|lösenord|adgangskode|passord|salasana|jelszó|heslo|mật khẩu/i;
|
|
207
|
+
export function 암호탓인가(말) {
|
|
208
|
+
return 암호말.test(String(말 ?? ''));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 파워셸이 뱉은 글(이미 푼 것)을 답으로 (2.0.2 · XL3).
|
|
213
|
+
*
|
|
214
|
+
* 돌리기 의 close 안에 있던 것을 떼어 냈다. 그 자리는 엑셀이 깔린 PC 에서만 닿아서,
|
|
215
|
+
* 여기 갈래가 틀려도 잴 길이 없었다.
|
|
216
|
+
*/
|
|
217
|
+
export function 엑셀답읽기(out, err = '') {
|
|
218
|
+
const 줄들 = String(out ?? '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
219
|
+
const 오류 = 줄들.find((l) => l.startsWith('ERR\t'));
|
|
220
|
+
if (오류) {
|
|
221
|
+
const [, 갈래, 말] = 오류.split('\t');
|
|
222
|
+
if (갈래 === 'PASSWORD' || 암호탓인가(말)) return { ok: false, reason: 'password', message: '암호가 맞지 않거나, 이 파일에는 암호가 필요합니다' };
|
|
223
|
+
if (갈래 === 'BUSY') return { ok: false, reason: 'busy', message: '엑셀이 계속 바쁘다고 합니다 — 열려 있는 엑셀 창을 닫고 다시 해보세요' };
|
|
224
|
+
// COM 클래스를 못 찾는다 = 이 컴퓨터에 엑셀이 없다.
|
|
225
|
+
// 이걸 그대로 내보내면 사용자는 알아볼 수 없는 오류 코드만 받는다.
|
|
226
|
+
if (없는엑셀(말)) {
|
|
227
|
+
return { ok: false, reason: 'no-excel', message: '이 컴퓨터에 엑셀이 설치되어 있지 않습니다 — 암호가 걸린 파일과 옛 .xls 는 엑셀이 있어야 읽을 수 있습니다' };
|
|
228
|
+
}
|
|
229
|
+
return { ok: false, reason: 'other', message: `엑셀이 열지 못했습니다: ${말 ?? ''}`.trim() };
|
|
230
|
+
}
|
|
231
|
+
if (!줄들.includes('OK')) {
|
|
232
|
+
const 첫줄 = String(err ?? '').split('\n')[0] ?? '';
|
|
233
|
+
if (없는엑셀(err)) {
|
|
234
|
+
return { ok: false, reason: 'no-excel', message: '이 컴퓨터에 엑셀이 설치되어 있지 않습니다' };
|
|
235
|
+
}
|
|
236
|
+
return { ok: false, reason: 'other', message: `엑셀이 끝내지 못했습니다${첫줄 ? ` — ${첫줄}` : ''}` };
|
|
237
|
+
}
|
|
238
|
+
const 시트들 = 줄들.filter((l) => l.startsWith('SHEET\t')).map((l) => {
|
|
239
|
+
const [, i, ...name] = l.split('\t');
|
|
240
|
+
return { i: Number(i), name: name.join('\t') || `시트${i}` };
|
|
241
|
+
});
|
|
242
|
+
return { ok: true, 시트들 };
|
|
243
|
+
}
|
|
244
|
+
|
|
194
245
|
function 돌리기(경로, 밖, password, timeout) {
|
|
195
246
|
return new Promise((done) => {
|
|
196
247
|
// 스크립트는 -EncodedCommand 로 넘긴다. 임시 .ps1 파일을 안 만들려는 것이다 —
|
|
@@ -202,7 +253,8 @@ function 돌리기(경로, 밖, password, timeout) {
|
|
|
202
253
|
env: { ...process.env, DEEL_XL_IN: 경로, DEEL_XL_OUT: 밖 },
|
|
203
254
|
});
|
|
204
255
|
|
|
205
|
-
// PowerShell 은 UTF-8 이 아니라 이 컴퓨터 콘솔 인코딩으로
|
|
256
|
+
// PowerShell 은 UTF-8 이 아니라 이 컴퓨터 콘솔 인코딩으로 뱉는다(스크립트가 UTF-8 로 돌려
|
|
257
|
+
// 놓지만, 그게 안 먹는 판도 있다).
|
|
206
258
|
// utf8 이라고 하고 받으면 오류 메시지가 통째로 깨져서, 무엇이 잘못됐는지
|
|
207
259
|
// 알아볼 수 없는 글자가 사용자에게 그대로 간다. 바이트로 모아 뒤에 푼다.
|
|
208
260
|
const 밖조각 = [];
|
|
@@ -232,33 +284,7 @@ function 돌리기(경로, 밖, password, timeout) {
|
|
|
232
284
|
if (끝남) return;
|
|
233
285
|
끝남 = true;
|
|
234
286
|
clearTimeout(시계);
|
|
235
|
-
|
|
236
|
-
const err = 풀기(오류조각);
|
|
237
|
-
const 줄들 = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
238
|
-
const 오류 = 줄들.find((l) => l.startsWith('ERR\t'));
|
|
239
|
-
if (오류) {
|
|
240
|
-
const [, 갈래, 말] = 오류.split('\t');
|
|
241
|
-
if (갈래 === 'PASSWORD') return done({ ok: false, reason: 'password', message: '암호가 맞지 않거나, 이 파일에는 암호가 필요합니다' });
|
|
242
|
-
if (갈래 === 'BUSY') return done({ ok: false, reason: 'busy', message: '엑셀이 계속 바쁘다고 합니다 — 열려 있는 엑셀 창을 닫고 다시 해보세요' });
|
|
243
|
-
// COM 클래스를 못 찾는다 = 이 컴퓨터에 엑셀이 없다.
|
|
244
|
-
// 이걸 그대로 내보내면 사용자는 알아볼 수 없는 오류 코드만 받는다.
|
|
245
|
-
if (없는엑셀(말)) {
|
|
246
|
-
return done({ ok: false, reason: 'no-excel', message: '이 컴퓨터에 엑셀이 설치되어 있지 않습니다 — 암호가 걸린 파일과 옛 .xls 는 엑셀이 있어야 읽을 수 있습니다' });
|
|
247
|
-
}
|
|
248
|
-
return done({ ok: false, reason: 'other', message: `엑셀이 열지 못했습니다: ${말 ?? ''}`.trim() });
|
|
249
|
-
}
|
|
250
|
-
if (!줄들.includes('OK')) {
|
|
251
|
-
const 첫줄 = err.split('\n')[0] ?? '';
|
|
252
|
-
if (없는엑셀(err)) {
|
|
253
|
-
return done({ ok: false, reason: 'no-excel', message: '이 컴퓨터에 엑셀이 설치되어 있지 않습니다' });
|
|
254
|
-
}
|
|
255
|
-
return done({ ok: false, reason: 'other', message: `엑셀이 끝내지 못했습니다${첫줄 ? ` — ${첫줄}` : ''}` });
|
|
256
|
-
}
|
|
257
|
-
const 시트들 = 줄들.filter((l) => l.startsWith('SHEET\t')).map((l) => {
|
|
258
|
-
const [, i, ...name] = l.split('\t');
|
|
259
|
-
return { i: Number(i), name: name.join('\t') || `시트${i}` };
|
|
260
|
-
});
|
|
261
|
-
done({ ok: true, 시트들 });
|
|
287
|
+
done(엑셀답읽기(풀기(밖조각), 풀기(오류조각)));
|
|
262
288
|
});
|
|
263
289
|
|
|
264
290
|
// 암호는 여기로만 나간다. 줄바꿈까지 보내고 바로 닫는다.
|
package/src/tools/fastgrep.js
CHANGED
|
@@ -52,8 +52,15 @@ export const 안볼확장자 = [
|
|
|
52
52
|
'map', 'min.js', 'min.css', 'lock',
|
|
53
53
|
];
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 위 목록으로 만든 정규식. 자바스크립트 길이 쓴다.
|
|
57
|
+
*
|
|
58
|
+
* 점만 벗기고 있었다. 지금 목록에는 점 말고 특수한 글자가 없어서 탈이 안 났지만,
|
|
59
|
+
* `c++` 같은 갈래 이름을 한 줄 더하는 날 그 줄이 조용히 **다른 무늬**가 된다.
|
|
60
|
+
* 목록은 사람이 늘리는 자리니 벗기는 쪽을 온전하게 둔다.
|
|
61
|
+
*/
|
|
62
|
+
const 정규식글자벗기기 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
63
|
+
export const 안볼정규식 = new RegExp(`\\.(${안볼확장자.map(정규식글자벗기기).join('|')})$`, 'i');
|
|
57
64
|
|
|
58
65
|
/** 위 목록으로 만든 rg 옵션. 대소문자를 안 가린다(정규식의 `i` 와 맞춘다). */
|
|
59
66
|
export function 안볼글로브() {
|
|
@@ -135,7 +142,7 @@ export function 저장소인가(폴더) {
|
|
|
135
142
|
*
|
|
136
143
|
* @returns {{ok:true, 줄들:string[], 잘림:boolean} | {ok:false, 왜:string}}
|
|
137
144
|
*/
|
|
138
|
-
export async function rg로찾기({ 무늬, 자리, 뿌리 = null, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, timeout = 20000, signal = null }) {
|
|
145
|
+
export async function rg로찾기({ 무늬, 자리, 뿌리 = null, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, timeout = 20000, signal = null, 부르기 = 돌려보기 }) {
|
|
139
146
|
const 인자 = [
|
|
140
147
|
'--line-number',
|
|
141
148
|
'--no-heading',
|
|
@@ -155,12 +162,18 @@ export async function rg로찾기({ 무늬, 자리, 뿌리 = null, glob = null,
|
|
|
155
162
|
];
|
|
156
163
|
if (대소문자무시) 인자.push('--ignore-case');
|
|
157
164
|
// `--` 뒤로 넘겨서 무늬가 옵션으로 안 읽히게 한다. `-foo` 같은 무늬가 실제로 있다.
|
|
158
|
-
|
|
165
|
+
const 덤 = rg작업폴더(뿌리, { glob, 무시파일 });
|
|
166
|
+
const { 찾을, 되붙이기 } = rg찾을자리(뿌리, 자리, 덤);
|
|
167
|
+
인자.push('--regexp', 무늬, '--', 찾을);
|
|
159
168
|
|
|
160
|
-
const r = await
|
|
169
|
+
const r = await 부르기('rg', 인자, { timeout, signal, 덤 });
|
|
161
170
|
const 못준까닭 = rg가못준까닭(r);
|
|
162
171
|
if (못준까닭) return { ok: false, 왜: 못준까닭 };
|
|
163
|
-
|
|
172
|
+
// 경로는 첫 NUL 앞이다(--null). 그 앞만 원래 뿌리에 되붙인다.
|
|
173
|
+
const 줄들 = String(r.stdout ?? '').split('\n').filter(Boolean).map((줄) => {
|
|
174
|
+
const 경로끝 = 줄.indexOf('\0');
|
|
175
|
+
return 경로끝 < 0 ? 줄 : 되붙이기(줄.slice(0, 경로끝)) + 줄.slice(경로끝);
|
|
176
|
+
});
|
|
164
177
|
return { ok: true, 줄들: 줄들.slice(0, 최대), 잘림: 줄들.length > 최대 };
|
|
165
178
|
}
|
|
166
179
|
|
|
@@ -222,7 +235,7 @@ export function rg가못준까닭(r) {
|
|
|
222
235
|
*/
|
|
223
236
|
const 이음 = '[\\x80-\\xBF]';
|
|
224
237
|
const 이음아님 = '(?:[^\\x80-\\xBF]|$)';
|
|
225
|
-
|
|
238
|
+
const 못푸는바이트무늬 = '(?-u:'
|
|
226
239
|
+ [
|
|
227
240
|
'\\x00',
|
|
228
241
|
'[\\xC0\\xC1\\xF5-\\xFF]',
|
|
@@ -251,9 +264,11 @@ export async function rg못푸는파일({ 자리, 뿌리 = null, glob = null,
|
|
|
251
264
|
// NUL 이 든 파일도 끝까지 본다. 안 그러면 rg 가 그 파일을 버려서 이름도 안 나온다.
|
|
252
265
|
'--text',
|
|
253
266
|
...rg거르는인자({ glob, 무시파일, 자리, 뿌리 }),
|
|
254
|
-
'--regexp', 못푸는바이트무늬, '--', 자리,
|
|
255
267
|
];
|
|
256
|
-
const
|
|
268
|
+
const 덤 = rg작업폴더(뿌리, { glob, 무시파일 });
|
|
269
|
+
const { 찾을, 되붙이기 } = rg찾을자리(뿌리, 자리, 덤);
|
|
270
|
+
인자.push('--regexp', 못푸는바이트무늬, '--', 찾을);
|
|
271
|
+
const r = await 부르기('rg', 인자, { timeout, signal, 덤 });
|
|
257
272
|
/*
|
|
258
273
|
* 실패는 **rg가못준까닭 한 자로** 가른다. 여기는 `error` 와 `status === 2` 두 줄뿐이라,
|
|
259
274
|
* 신호로 죽거나(`status === null`) 2 가 아닌 값으로 끝나면 **빈 목록이 `ok:true`** 로
|
|
@@ -264,7 +279,7 @@ export async function rg못푸는파일({ 자리, 뿌리 = null, glob = null,
|
|
|
264
279
|
*/
|
|
265
280
|
const 못준까닭 = rg가못준까닭(r);
|
|
266
281
|
if (못준까닭) return { ok: false, 왜: 못준까닭 };
|
|
267
|
-
const 파일들 = String(r.stdout ?? '').split('\n').map((l) => l.replace(/\r$/, '')).filter(Boolean);
|
|
282
|
+
const 파일들 = String(r.stdout ?? '').split('\n').map((l) => l.replace(/\r$/, '')).filter(Boolean).map(되붙이기);
|
|
268
283
|
return { ok: true, 파일들 };
|
|
269
284
|
}
|
|
270
285
|
|
|
@@ -293,6 +308,46 @@ function rg작업폴더(뿌리, { glob = null, 무시파일 = null } = {}) {
|
|
|
293
308
|
return 뿌리 && (glob || 무시파일) ? { cwd: 뿌리 } : {};
|
|
294
309
|
}
|
|
295
310
|
|
|
311
|
+
/*
|
|
312
|
+
* ── 작업 폴더를 정했으면 찾을 자리도 **그 기준 상대경로로** 넘긴다 (2.0.1) ──
|
|
313
|
+
*
|
|
314
|
+
* rg 는 glob 을 작업 폴더 기준으로 맞춘다. 그러려면 찾을 자리가 작업 폴더
|
|
315
|
+
* **밑으로 보여야** 한다 — 그 판단은 글자로 한다. 그런데 윈도우에서는 같은
|
|
316
|
+
* 폴더를 두 가지로 적을 수 있다:
|
|
317
|
+
*
|
|
318
|
+
* C:\Users\RUNNER~1\AppData\Local\Temp (8.3 짧은 이름 — os.tmpdir() 가 준다)
|
|
319
|
+
* C:\Users\runneradmin\AppData\Local\Temp (긴 이름 — 같은 폴더)
|
|
320
|
+
*
|
|
321
|
+
* Chocolatey·Scoop 은 rg 를 **심(shim)** 으로 깐다. 재 보니 심을 거치면 작업
|
|
322
|
+
* 폴더가 긴 이름으로 바뀌어 진짜 rg 가 뜬다. 우리는 찾을 자리를 짧은 이름으로
|
|
323
|
+
* 넘겼으니 rg 눈에는 그 자리가 작업 폴더 밑이 아니고, 빗금 든 glob 이 한 파일에도
|
|
324
|
+
* 안 맞았다 — 오류 없이 「일치 없음」, `!` 빼기는 아무것도 안 뺐다.
|
|
325
|
+
*
|
|
326
|
+
* 둘 중 하나만 있어서는 안 깨진다(윈도 러너에서 갈라 봤다):
|
|
327
|
+
*
|
|
328
|
+
* 심 + 짧은 이름 → 5개 빨강
|
|
329
|
+
* 진짜 rg.exe + 짧은 이름 → 121 통과
|
|
330
|
+
* 심 + 긴 이름 → 121 통과
|
|
331
|
+
*
|
|
332
|
+
* 윈도우에서 rg 는 대개 choco·scoop 으로 깔고, 짧은 이름은 사용자 이름이 여덟
|
|
333
|
+
* 글자를 넘거나 빈칸이 들면 %TEMP% 에 그대로 온다. 흔한 짝이다.
|
|
334
|
+
*
|
|
335
|
+
* 그래서 이름 적는 방식에 기대지 않는다. 찾을 자리를 **뿌리 기준 상대경로**로
|
|
336
|
+
* 넘기면 rg 는 제 작업 폴더에서부터 걸어 들어가고, 작업 폴더를 어떻게 적었든
|
|
337
|
+
* 상관이 없다. 나온 경로는 **원래 뿌리에 되붙여** 돌려준다 — 아래로 흐르는
|
|
338
|
+
* 경로 꼴은 예전과 똑같다.
|
|
339
|
+
*
|
|
340
|
+
* 작업 폴더를 안 정하는 판(glob·무시파일 없음)과, 찾을 자리가 뿌리 밖인 판은
|
|
341
|
+
* 예전대로 절대경로를 넘긴다.
|
|
342
|
+
*/
|
|
343
|
+
function rg찾을자리(뿌리, 자리, 덤) {
|
|
344
|
+
const 그대로 = { 찾을: 자리, 되붙이기: (f) => f };
|
|
345
|
+
if (!덤?.cwd || !뿌리 || !자리) return 그대로;
|
|
346
|
+
const 상대 = relative(뿌리, 자리);
|
|
347
|
+
if (isAbsolute(상대) || 상대 === '..' || 상대.startsWith(`..${sep}`)) return 그대로;
|
|
348
|
+
return { 찾을: 상대 || '.', 되붙이기: (f) => (isAbsolute(f) ? f : join(뿌리, f)) };
|
|
349
|
+
}
|
|
350
|
+
|
|
296
351
|
/*
|
|
297
352
|
* glob 하나를 rg 인자로. 규칙은 fsutil.js 의 glob거르개 와 **같다**.
|
|
298
353
|
*
|
package/src/tools/fsutil.js
CHANGED
|
@@ -109,10 +109,14 @@ export function 내부살림(abs) {
|
|
|
109
109
|
* Write .deel/config.json::$DATA **설정 본체를 덮어썼다**
|
|
110
110
|
* cat .deel/audit.jsonl::$DATA checkPaths 통과
|
|
111
111
|
*
|
|
112
|
-
* 드라이브 조각(`C:`)만 빼고 첫 `:` 뒤를 버린다.
|
|
113
|
-
*
|
|
112
|
+
* 드라이브 조각(`C:`)만 빼고 첫 `:` 뒤를 버린다.
|
|
113
|
+
*
|
|
114
|
+
* **윈도에서만** 그렇게 한다 (2.0.2 · 480). 유닉스·맥에서 `:` 는 그냥 글자이고 스트림이 없다 —
|
|
115
|
+
* `.deel/config.json:v2` 는 설정 파일이 아니라 딴 파일이다. 모든 판에서 벗겼더니 리눅스에서 그런
|
|
116
|
+
* 이름을 살림으로 잘못 알고 막았다(안전 쪽이지만 까닭 없는 막힘이다).
|
|
114
117
|
*/
|
|
115
|
-
const
|
|
118
|
+
const 스트림꼴 = process.platform === 'win32';
|
|
119
|
+
const 조각 = 편.split('/').map((x, 몇째) => (!스트림꼴 || (몇째 === 0 && /^[A-Za-z]:$/.test(x)) ? x : x.replace(/:.*$/s, '')));
|
|
116
120
|
const 이름 = (조각[조각.length - 1] ?? '').toLowerCase();
|
|
117
121
|
/*
|
|
118
122
|
* 살림 자리를 `.deel` 이라는 **글자**로 찾고 있었다.
|
|
@@ -405,7 +409,7 @@ export function 훑기상한(env = process.env) {
|
|
|
405
409
|
* 자주 쉬면 훑기 자체가 느려지고, 드물게 쉬면 그만큼 귀를 닫는다. 200개면
|
|
406
410
|
* 사내망 드라이브에서도 한 숨 사이가 10ms 언저리다.
|
|
407
411
|
*/
|
|
408
|
-
|
|
412
|
+
const 숨쉴간격 = 200;
|
|
409
413
|
const 한숨 = () => new Promise((풀기) => setImmediate(풀기));
|
|
410
414
|
|
|
411
415
|
/**
|
package/src/tools/ignore.js
CHANGED
|
@@ -30,11 +30,31 @@ export function 규칙읽기(text, 기준 = '') {
|
|
|
30
30
|
return out;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/*
|
|
34
|
+
* ── 끝 빈칸 떼기는 git 과 **똑같이** (2.0.2 · G4) ─────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* 「역슬래시 바로 뒤 빈칸은 살린다」 로 쟀더니 `foo\\ ` 가 틀렸다. 앞의 `\\` 는 역슬래시 **글자**
|
|
37
|
+
* 하나이고 빈칸은 안 살린 것이라 git 은 `foo\` 로 읽는데, 우리는 빈칸까지 남겨 `foo\ ` 를 찾았다.
|
|
38
|
+
* git(dir.c 의 trim_trailing_spaces)을 그대로 옮긴다 — 역슬래시는 다음 글자 하나를 먹고, 떼는 것은
|
|
39
|
+
* 끝에 이어진 **빈칸**(' ')뿐이다. 탭은 git 도 안 뗀다.
|
|
40
|
+
*/
|
|
41
|
+
export function 끝빈칸떼기(줄) {
|
|
42
|
+
const p = String(줄 ?? '');
|
|
43
|
+
let 빈칸자리 = -1;
|
|
44
|
+
for (let i = 0; i < p.length; i++) {
|
|
45
|
+
const ch = p[i];
|
|
46
|
+
if (ch === ' ') { if (빈칸자리 < 0) 빈칸자리 = i; continue; }
|
|
47
|
+
if (ch === '\\') { i++; if (i >= p.length) return p; }
|
|
48
|
+
빈칸자리 = -1;
|
|
49
|
+
}
|
|
50
|
+
return 빈칸자리 >= 0 ? p.slice(0, 빈칸자리) : p;
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
/** 줄 하나 → 규칙. 빈 줄·주석이면 null. */
|
|
34
54
|
export function 패턴규칙(줄, 기준 = '') {
|
|
35
55
|
let p = String(줄 ?? '');
|
|
36
56
|
// 끝의 빈칸은 뗀다 — 역슬래시로 살린 것만 남긴다.
|
|
37
|
-
p = p
|
|
57
|
+
p = 끝빈칸떼기(p);
|
|
38
58
|
if (!p || p.startsWith('#')) return null;
|
|
39
59
|
let 부정 = false;
|
|
40
60
|
if (p.startsWith('!')) { 부정 = true; p = p.slice(1); }
|
package/src/tools/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// 도구. 이름과 인자를 Claude Code 와 같게 맞춘다 —
|
|
2
2
|
// 그래야 그 관례로 쓰인 스킬·명령이 그대로 먹는다.
|
|
3
|
-
import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, statSync, lstatSync, renameSync,
|
|
3
|
+
import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, statSync, lstatSync, renameSync, copyFileSync, rmSync,
|
|
4
4
|
openSync, readSync, closeSync } from 'node:fs';
|
|
5
5
|
import { dirname, extname, isAbsolute, join, relative, sep } from 'node:path';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { 무리로돌리기 } from './spawn.js';
|
|
8
|
-
import { walk, readText, readTextFull, 내부살림, glob
|
|
8
|
+
import { walk, readText, readTextFull, 내부살림, glob거르개, copyDir } from './fsutil.js';
|
|
9
9
|
import { 건너뜀말 } from './ignore.js';
|
|
10
10
|
import { encode, label as encLabel, decode as decodeBytes, consoleCodepage, looksBinary, 바꾼데만쓰기 } from './encoding.js';
|
|
11
11
|
import { checkCommand, checkPaths, isMutating, 셸이파일에쓰나 } from '../safety/guard.js';
|
|
12
12
|
import { 띄우기, 나무끊기, 무리끊기, 남은무리끊기, JOBS_TOOL } from './jobs.js';
|
|
13
|
-
import {
|
|
13
|
+
import { 셸명령, 셸한계넘나 } from './shell.js';
|
|
14
14
|
import { findMatch, applySpans, reindent, TIER_LABELS, CRLF뿐인가, CRLF로, 꼴맞추기 } from './edit-match.js';
|
|
15
15
|
import { loadSkill } from '../skills/discover.js';
|
|
16
16
|
import { WEB_FETCH_TOOL } from './webfetch.js';
|
|
@@ -187,7 +187,7 @@ export function 붙박이그림줄이기(줄들) {
|
|
|
187
187
|
* 줄을 통째로 지워 버리는 경우가 더 흔하고, 그때는 표시도 같이 사라진다.
|
|
188
188
|
* 그래서 **개수를 견준다** — 있던 것보다 줄었으면 잃은 것이다.
|
|
189
189
|
*/
|
|
190
|
-
|
|
190
|
+
function 박힌그림수(글) {
|
|
191
191
|
return (String(글 ?? '').match(그림자리) ?? []).length;
|
|
192
192
|
}
|
|
193
193
|
|
|
@@ -274,6 +274,19 @@ function 파일크기(abs) {
|
|
|
274
274
|
try { return statSync(abs).size; } catch { return -1; }
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/*
|
|
278
|
+
* 쓴 자리에 **파일이 생겼나** (2.0.2 · 379). 안 생겼으면 그 말을 돌려준다.
|
|
279
|
+
*
|
|
280
|
+
* 윈도 10 까지는 `NUL`·`CON`·`aux.txt` 같은 이름이 파일이 아니라 장치다 — 쓰기는 성공하고
|
|
281
|
+
* 아무것도 안 남는다. 그런데 화면은 `새로 만듦: NUL (1줄)` 이었다. 윈도 11 은 진짜 파일을
|
|
282
|
+
* 만들어서 판마다 다르다 — 이름을 외우지 않고 **쓴 결과**를 본다. Write·Append·한글 문서가
|
|
283
|
+
* 같이 쓴다.
|
|
284
|
+
*/
|
|
285
|
+
export function 파일안생김(ctx, abs) {
|
|
286
|
+
try { if (statSync(abs).isFile()) return null; } catch { /* 없다 — 아래 말 */ }
|
|
287
|
+
return { error: `${ctx.scope.show(abs)} 에 썼는데 파일이 안 생겼습니다 — 윈도가 이 이름(NUL·CON·AUX·COM1 따위)을 장치로 읽습니다. 다른 이름을 쓰세요.` };
|
|
288
|
+
}
|
|
289
|
+
|
|
277
290
|
/*
|
|
278
291
|
* ── 크기만으로는 「그 파일 그대로」 를 모른다 ─────────────────────────────
|
|
279
292
|
*
|
|
@@ -1034,15 +1047,59 @@ function pdf읽기(abs, ctx) {
|
|
|
1034
1047
|
* { 복사깨짐: 까닭 } 닿은 자리가 반쯤 됐다 — 실패다
|
|
1035
1048
|
* { 원본남음: 까닭 } 닿은 자리는 다 됐고 원본이 남았다 — 실패가 아니다
|
|
1036
1049
|
*
|
|
1050
|
+
* 안 베낀 것이 있으면 `건너뜀` 이 같이 온다. 그때는 언제나 `원본남음` 과 함께다
|
|
1051
|
+
* (바로 아래 머리말).
|
|
1052
|
+
*
|
|
1037
1053
|
* fs 를 밖에서 넣을 수 있게 해 둔 것은 검사 때문이다. 진짜 EXDEV 와 진짜
|
|
1038
1054
|
* '지우기만 깨짐' 은 어느 PC 에서나 똑같이 만들어 낼 수 없다.
|
|
1039
1055
|
*/
|
|
1040
|
-
|
|
1056
|
+
/*
|
|
1057
|
+
* ── cpSync 는 윈도우에서 프로세스를 **통째로** 죽인다 ─────────────────────
|
|
1058
|
+
*
|
|
1059
|
+
* Node 22 + 윈도우에서 경로에 한글이 들어 있으면 cpSync 가 0xC0000409 로
|
|
1060
|
+
* 프로세스를 끝낸다. try 로 못 잡는다 — 예외가 아니라 네이티브 크래시다.
|
|
1061
|
+
* 아래 `복사깨짐` 갈래는 아예 돌지 못하고, 옮기던 사람은 아무 말도 못 듣는다.
|
|
1062
|
+
* 한국어로 쓰는 도구라 폴더 이름에 한글은 늘 있다.
|
|
1063
|
+
*
|
|
1064
|
+
* fsutil.js 의 copyDir 이 **이미 같은 까닭으로** 있었다 — 거기 머리말에
|
|
1065
|
+
* 0xC0000409 까지 적혀 있다. 그런데 옮기기만 cpSync 로 남아 있었다.
|
|
1066
|
+
* 한 자리를 고칠 때 같은 병이 다른 자리에도 있나를 안 물었던 것이다.
|
|
1067
|
+
*
|
|
1068
|
+
* copyDir 은 링크를 안 따라가고 건너뛴 것을 돌려준다. 그 편이 맞다 —
|
|
1069
|
+
* 따라가면 링크 너머의 진짜 파일이 사본이 되고, 원본을 지울 때 링크만
|
|
1070
|
+
* 사라진다. 건너뛴 것은 아래에서 사람에게 그대로 말한다.
|
|
1071
|
+
*/
|
|
1072
|
+
function 안전복사(앞, 뒤) {
|
|
1073
|
+
if (statSync(앞).isDirectory()) return copyDir(앞, 뒤);
|
|
1074
|
+
mkdirSync(dirname(뒤), { recursive: true });
|
|
1075
|
+
copyFileSync(앞, 뒤);
|
|
1076
|
+
return { skipped: [] };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export function 복사해옮기기(앞, 뒤, { 복사 = 안전복사, 지우기 = rmSync } = {}) {
|
|
1080
|
+
let 건너뜀 = [];
|
|
1041
1081
|
try {
|
|
1042
|
-
복사(앞, 뒤, { recursive: true });
|
|
1082
|
+
건너뜀 = 복사(앞, 뒤, { recursive: true })?.skipped ?? [];
|
|
1043
1083
|
} catch (err) {
|
|
1044
1084
|
return { 복사깨짐: err.message };
|
|
1045
1085
|
}
|
|
1086
|
+
/*
|
|
1087
|
+
* ── 안 베낀 것이 있으면 원본을 **안 지운다** (2차 눈) ────────────────
|
|
1088
|
+
*
|
|
1089
|
+
* copyDir 은 링크를 안 따라가고 건너뛴다. 그런데 그 뒤에서 원본 폴더를
|
|
1090
|
+
* 통째로 지우면 **베끼지도 않은 링크가 영영 없어진다.** 옮긴 자리에도
|
|
1091
|
+
* 없고 헌 자리에도 없다 — 재어 봤더니 실제로 그렇게 사라졌다.
|
|
1092
|
+
*
|
|
1093
|
+
* 게다가 화면은 「링크는 안 옮겼습니다」 라고 말할 참이었다. 안 옮긴 것이
|
|
1094
|
+
* 아니라 **지운** 것인데. 옮기기가 파일을 없애는 꼴이라 여기서 멈춘다.
|
|
1095
|
+
*
|
|
1096
|
+
* 못 옮긴 것이 하나라도 있으면 원본을 그대로 둔다. 무엇이 남았는지 말하고
|
|
1097
|
+
* 사람이 고르게 한다 — 안전망(/undo)도 못 되살리는 것을 조용히 지우는
|
|
1098
|
+
* 것보다 낫다.
|
|
1099
|
+
*/
|
|
1100
|
+
if (건너뜀.length) {
|
|
1101
|
+
return { 건너뜀, 원본남음: `링크 ${건너뜀.length}개를 안 베껴, 지우면 없어지므로 원본을 그대로 뒀습니다` };
|
|
1102
|
+
}
|
|
1046
1103
|
try {
|
|
1047
1104
|
지우기(앞, { recursive: true, force: true });
|
|
1048
1105
|
} catch (err) {
|
|
@@ -1240,6 +1297,8 @@ async function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
1240
1297
|
}
|
|
1241
1298
|
|
|
1242
1299
|
let 원본남음 = null;
|
|
1300
|
+
/** 드라이브를 넘는 복사에서 안 따라간 링크. 옮겼다는 말과 같이 적어 준다. */
|
|
1301
|
+
let 건너뛴링크 = [];
|
|
1243
1302
|
/*
|
|
1244
1303
|
* 옮기는 함수는 갈아 끼울 수 있게 둔다. rename 이 EBUSY 로 깨지는 판(다른
|
|
1245
1304
|
* 프로그램이 잡고 있는 파일)은 검사에서 파일 시스템으로 못 만든다
|
|
@@ -1269,6 +1328,7 @@ async function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
1269
1328
|
* 복사 도중에 끊기면 양쪽에 반씩 남기 때문이다.
|
|
1270
1329
|
*/
|
|
1271
1330
|
const 벌어진일 = 복사해옮기기(앞, 뒤);
|
|
1331
|
+
건너뛴링크 = 벌어진일.건너뜀 ?? [];
|
|
1272
1332
|
if (벌어진일.복사깨짐) {
|
|
1273
1333
|
// 닿은 자리를 여기서 지우지 않는다 — 이미 있던 폴더로 옮기는 중이었으면
|
|
1274
1334
|
// 남의 파일까지 지운다. 어디에 반쯤 남았는지만 정확히 말한다.
|
|
@@ -1288,7 +1348,11 @@ async function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
1288
1348
|
+ (되돌리기반쪽
|
|
1289
1349
|
? `\n(파일이 ${훑은것.상한.toLocaleString('en-US')}개를 넘어 되돌리기에는 앞부분만 떴습니다 — 옮기기는 전부 됐지만 /undo 는 다 못 되돌립니다)`
|
|
1290
1350
|
: '')
|
|
1291
|
-
+ (
|
|
1351
|
+
+ (건너뛴링크.length
|
|
1352
|
+
? `\n(링크 ${건너뛴링크.length}개는 안 옮겼습니다 — 드라이브를 넘는 복사라 링크는 따라가지 않습니다:`
|
|
1353
|
+
+ ` ${건너뛴링크.slice(0, 3).map((p) => ctx.scope.show(p)).join(' · ')}${건너뛴링크.length > 3 ? ' 외' : ''}).`
|
|
1354
|
+
+ `\n 그래서 원본도 안 지웠습니다 — ${ctx.scope.show(앞)} 에 그대로 있습니다.`
|
|
1355
|
+
: 원본남음
|
|
1292
1356
|
? `\n(닿은 자리에는 다 옮겼는데 원본을 못 지웠습니다 — 지금 ${ctx.scope.show(앞)} 에도 그대로 있습니다: ${원본남음})`
|
|
1293
1357
|
: '')
|
|
1294
1358
|
+ (이름못되돌림
|
|
@@ -1388,6 +1452,7 @@ function hwpx새로만들기(args, ctx, abs) {
|
|
|
1388
1452
|
writeFileSync(abs, 만듦.buf);
|
|
1389
1453
|
}, () => !existsSync(abs));
|
|
1390
1454
|
if (못씀) return 못씀;
|
|
1455
|
+
{ const 탈 = 파일안생김(ctx, abs); if (탈) return 탈; }
|
|
1391
1456
|
/*
|
|
1392
1457
|
* `seen` 에 안 올린다.
|
|
1393
1458
|
*
|
|
@@ -1557,6 +1622,7 @@ function 한파일쓰기(args, ctx) {
|
|
|
1557
1622
|
writeFileSync(abs, 만든것.buf);
|
|
1558
1623
|
}, () => (existed ? !!읽음 && readFileSync(abs).equals(읽음.buf) : !existsSync(abs)));
|
|
1559
1624
|
if (못씀) return 못씀;
|
|
1625
|
+
{ const 탈 = 파일안생김(ctx, abs); if (탈) return 탈; }
|
|
1560
1626
|
ctx.seen.add(abs);
|
|
1561
1627
|
const n = args.content.split('\n').length;
|
|
1562
1628
|
const 표기 = 원래 !== 'utf-8' ? ` · ${encLabel(원래)}${짐작이다 ? ' (짐작)' : ''}` : '';
|
|
@@ -2401,6 +2467,7 @@ export const TOOLS = {
|
|
|
2401
2467
|
else writeFileSync(abs, 만든것.buf);
|
|
2402
2468
|
}, () => (existed ? 파일크기(abs) === 지금크기 : !existsSync(abs)));
|
|
2403
2469
|
if (못씀) return 못씀;
|
|
2470
|
+
{ const 탈 = 파일안생김(ctx, abs); if (탈) return 탈; }
|
|
2404
2471
|
// 붙인 **뒤** 앞머리로 적어 둔다. 64KB 보다 작던 파일은 붙인 조각이 앞머리에 든다.
|
|
2405
2472
|
잰것캐시.set(abs, {
|
|
2406
2473
|
지문: 지금크기 >= 인코딩볼바이트 ? 앞지문 : 표본지문(앞머리(abs, 인코딩볼바이트)),
|
|
@@ -3004,6 +3071,9 @@ export const TOOLS = {
|
|
|
3004
3071
|
// 어느 셸로 넘기나 — tools/shell.js 가 정한다. Jobs 와 같은 답이어야 하므로 한 군데다.
|
|
3005
3072
|
// (윈도우 cmd 의 따옴표 문제와 그 해법도 거기 적혀 있다.)
|
|
3006
3073
|
const shell = 셸명령(cmd);
|
|
3074
|
+
// cmd 의 한 줄 한계를 넘으면 돌리기 전에 말한다 (tools/shell.js 셸한계넘나 · 2.0.2 S6).
|
|
3075
|
+
const 한계 = 셸한계넘나(cmd, shell);
|
|
3076
|
+
if (한계) return { error: 한계 };
|
|
3007
3077
|
|
|
3008
3078
|
const 제한 = bash제한시간(args.timeout);
|
|
3009
3079
|
// 무엇을 빼고 넘길지 여기서 한 번 정한다. 뺀 이름은 아래에서 명령이
|