deel-local-cli 1.20.12 → 2.0.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.ko.md +96 -77
- package/README.md +119 -80
- package/bin/deel.js +398 -32
- package/package.json +2 -1
- package/src/acp/jsonrpc.js +87 -7
- package/src/acp/map.js +62 -13
- package/src/acp/serve.js +338 -30
- package/src/agent/agents.js +110 -12
- package/src/agent/askcheck.js +50 -6
- package/src/agent/asks.js +59 -5
- package/src/agent/budget.js +9 -2
- package/src/agent/card.js +8 -2
- package/src/agent/commit.js +168 -16
- package/src/agent/compact.js +93 -21
- package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
- package/src/agent/effort.js +85 -15
- package/src/agent/evidence.js +63 -7
- package/src/agent/evolve.js +132 -24
- package/src/agent/grade.js +12 -3
- package/src/agent/loop.js +408 -81
- package/src/agent/memory.js +176 -29
- package/src/agent/mention.js +22 -5
- package/src/agent/models.js +31 -7
- package/src/agent/modes.js +25 -10
- package/src/agent/outschema.js +381 -56
- package/src/agent/pins.js +33 -2
- package/src/agent/project.js +73 -8
- package/src/agent/recall.js +40 -9
- package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
- package/src/agent/review.js +143 -13
- package/src/agent/route.js +506 -32
- package/src/agent/salvage.js +2 -1
- package/src/agent/session.js +457 -31
- package/src/agent/store.js +232 -22
- package/src/agent/threads.js +52 -3
- package/src/backend/adapter.js +508 -85
- package/src/backend/azure.js +12 -1
- package/src/backend/cachemark.js +31 -5
- package/src/backend/clientcert.js +23 -1
- package/src/backend/ctxsize.js +56 -9
- package/src/backend/detect.js +252 -19
- package/src/backend/http.js +383 -42
- package/src/backend/learn.js +101 -11
- package/src/backend/mcp.js +333 -24
- package/src/backend/price.js +61 -13
- package/src/backend/probe.js +188 -36
- package/src/backend/proxy.js +108 -14
- package/src/backend/quota.js +226 -30
- package/src/backend/retry.js +46 -3
- package/src/backend/scan.js +47 -10
- package/src/backend/scanui.js +57 -7
- package/src/backend/toolfit.js +204 -22
- package/src/backend/vision.js +14 -1
- package/src/backend/wire.js +58 -10
- package/src/cmdnames.js +16 -0
- package/src/commands/common.js +70 -0
- package/src/commands/extend.js +300 -0
- package/src/commands/model.js +852 -0
- package/src/commands/view.js +328 -0
- package/src/commands/work.js +823 -0
- package/src/commands.js +193 -2082
- package/src/completion.js +45 -3
- package/src/config.js +452 -39
- package/src/configexplain.js +125 -14
- package/src/doctor.js +164 -26
- package/src/i18n/en.js +22 -0
- package/src/i18n/index.js +31 -1
- package/src/i18n/ja.js +25 -1
- package/src/i18n/ko.js +25 -2
- package/src/i18n/zh.js +25 -1
- package/src/lsp/client.js +111 -9
- package/src/lsp/servers.js +86 -22
- package/src/oneshot.js +325 -65
- package/src/pack/sbom.js +28 -5
- package/src/pack/selfpack.js +54 -5
- package/src/pack/sheet.en.js +24 -3
- package/src/pack/tar.js +45 -8
- package/src/pack/zip.js +103 -8
- package/src/plugins/manage.js +165 -14
- package/src/preview/serve.js +239 -23
- package/src/providers/bedrock.js +30 -5
- package/src/providers/gemini.js +8 -0
- package/src/providers/index.js +17 -1
- package/src/repl.js +254 -61
- package/src/report.js +7 -2
- package/src/reset.js +151 -25
- package/src/safety/audit.js +108 -8
- package/src/safety/authcmd.js +75 -5
- package/src/safety/guard.js +507 -15
- package/src/safety/hooks.js +172 -20
- package/src/safety/keystore.js +178 -18
- package/src/safety/network.js +209 -6
- package/src/safety/policy.js +377 -36
- package/src/safety/runmode.js +9 -2
- package/src/safety/secrets.js +243 -17
- package/src/safety/shellenv.js +64 -3
- package/src/safety/trust.js +298 -32
- package/src/safety/undo.js +409 -17
- package/src/setup.js +175 -17
- package/src/skills/discover.js +122 -19
- package/src/stats.js +57 -16
- package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
- package/src/tools/clipboard.js +53 -20
- package/src/tools/convert.js +118 -37
- package/src/tools/desc.en.js +48 -5
- package/src/tools/doc2md.js +11 -1
- package/src/tools/docs.js +94 -18
- package/src/tools/edit-match.js +212 -13
- package/src/tools/encoding.js +525 -28
- package/src/tools/excel-com.js +63 -15
- package/src/tools/excel.js +12 -7
- package/src/tools/fastgrep.js +444 -36
- package/src/tools/fig.js +40 -9
- package/src/tools/fsutil.js +235 -21
- package/src/tools/hwpxwrite.js +80 -19
- package/src/tools/ignore.js +55 -7
- package/src/tools/index.js +1029 -145
- package/src/tools/jobs.js +325 -50
- package/src/tools/kiwi.js +50 -5
- package/src/tools/lsp.js +164 -32
- package/src/tools/outline.js +52 -12
- package/src/tools/pdf.js +366 -56
- package/src/tools/shell.js +12 -4
- package/src/tools/spawn.js +203 -18
- package/src/tools/todo.js +73 -4
- package/src/tools/verify.js +312 -43
- package/src/tools/webfetch.js +184 -25
- package/src/tools/xlsx.js +232 -31
- package/src/ui/ansi.js +209 -15
- package/src/ui/banner.js +2 -1
- package/src/ui/complete.js +46 -4
- package/src/ui/diff.js +4 -2
- package/src/ui/export.js +91 -17
- package/src/ui/inputbox.js +58 -18
- package/src/ui/intro.js +9 -2
- package/src/ui/level.js +32 -7
- package/src/ui/md.js +101 -15
- package/src/ui/motion.js +5 -2
- package/src/ui/notify.js +7 -1
- package/src/ui/office.js +37 -5
- package/src/ui/pastechip.js +3 -2
- package/src/ui/pick.js +85 -12
- package/src/ui/prompt.js +176 -13
- package/src/ui/screen.js +2 -1
- package/src/ui/spinner.js +6 -1
- package/src/ui/status.js +80 -12
- package/src/ui/wrap.js +23 -37
- /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
- /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
- /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
- /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/tools/docs.js
CHANGED
|
@@ -110,6 +110,19 @@ export function 겉속다름말(갈래, 속) {
|
|
|
110
110
|
+ ' 파일이 깨졌을 수 있습니다. 이 파일로는 더 해 볼 것이 없으니 사용자에게 알리세요.';
|
|
111
111
|
}
|
|
112
112
|
if (속.startsWith('옛 Office')) {
|
|
113
|
+
/*
|
|
114
|
+
* hwpx 만 길이 다르다. OLE 복합문서 서명은 옛 hwp 와 옛 Office 가 **똑같아서**,
|
|
115
|
+
* 이름만 hwpx 인 것은 십중팔구 옛 hwp 다. 그런데 soffice 에는 hwpx 로 바꾸는
|
|
116
|
+
* 길이 없다 — 없는 길을 알려 주면 모델은 그 명령을 실제로 불러 보고 실패하고,
|
|
117
|
+
* 사람은 그 왕복을 고스란히 문다. 진짜 길은 바로 아래 옛hwp안내 가 적어 둔
|
|
118
|
+
* 그것이다 (한글에서 hwpx 로 저장).
|
|
119
|
+
*/
|
|
120
|
+
if (갈래 === 'hwpx') {
|
|
121
|
+
return `${머리} — 실제로는 옛 형식(OLE 복합문서)입니다. 옛 hwp 이거나 옛 Office 파일입니다.\n`
|
|
122
|
+
+ ' 한글(한컴오피스)에서 열어 **hwpx 로 저장**하면 그대로 읽을 수 있습니다\n'
|
|
123
|
+
+ ' (다른 이름으로 저장 → 파일 형식에서 hwpx 선택).\n'
|
|
124
|
+
+ ' soffice 로는 hwpx 로 못 바꿉니다 — 그 길은 없습니다.';
|
|
125
|
+
}
|
|
113
126
|
return `${머리} — 실제로는 ${속} 입니다.\n`
|
|
114
127
|
+ ` 이름만 ${갈래} 로 바뀐 옛 형식이라 이 도구로는 못 읽습니다.\n`
|
|
115
128
|
+ ` PowerPoint·Word 에서 열어 ${갈래} 로 다시 저장하거나,\n`
|
|
@@ -175,8 +188,33 @@ export function 문단뽑기(xml, 갈래, { 표를따로 = false } = {}) {
|
|
|
175
188
|
|
|
176
189
|
let 글모음 = []; // 지금 문단의 글 조각들
|
|
177
190
|
let 표깊이 = 0;
|
|
178
|
-
|
|
179
|
-
|
|
191
|
+
/*
|
|
192
|
+
* ── 표 안의 표가 바깥 행을 통째로 지웠다 ────────────────────────────
|
|
193
|
+
*
|
|
194
|
+
* 사람이 본 것 —
|
|
195
|
+
*
|
|
196
|
+
* ◧ Read(사업계획서.docx)
|
|
197
|
+
* └ 담당 | 과장 | 부장
|
|
198
|
+
*
|
|
199
|
+
* 결재란만 나오고 「사업명」도 「예산」도 없다. 그런데 오류도, 잘림 알림도
|
|
200
|
+
* 없다. 나온 글은 멀쩡한 문장이라 **뭔가 빠졌다는 표가 아무 데도 안 난다.**
|
|
201
|
+
* 모델은 그게 문서 전부인 줄 알고 "그런 항목은 없습니다" 라고 답했다.
|
|
202
|
+
*
|
|
203
|
+
* 속에서 벌어진 일 — 표깊이는 세면서 그릇(행칸들·칸글)은 하나뿐이었다.
|
|
204
|
+
* 바깥 칸이 '사업명' 을 모으는 중에 안쪽 <tr> 이 행칸들을 새 [] 로, 안쪽
|
|
205
|
+
* <tc> 가 칸글을 새 [] 로 갈아 치웠다. 그 순간 '사업명' 이 사라졌고, 안쪽
|
|
206
|
+
* </tr> 이 행칸들을 null 로 되돌려 놓아서 바깥 </tc>·</tr> 은 담을 그릇이
|
|
207
|
+
* 없다며 아무것도 안 밀어 넣었다. 한국 관공서 양식은 표 안에 결재란을 두는
|
|
208
|
+
* 것이 표준이라, 하필 **값이 든 바깥 표**만 골라서 없어진 셈이다.
|
|
209
|
+
*
|
|
210
|
+
* 이제 지키는 규칙: 그릇은 표 깊이마다 따로 둔다. 안쪽 표가 무엇을 하든
|
|
211
|
+
* 바깥 층의 칸은 건드리지 않는다. 안쪽 행이 먼저 나오고 그 뒤에 바깥 행이
|
|
212
|
+
* 나오지만, 사라지는 것은 없다.
|
|
213
|
+
*/
|
|
214
|
+
const 행칸들쌓임 = []; // 깊이마다: 그 층 행에 담긴 칸 글들
|
|
215
|
+
const 칸글쌓임 = []; // 깊이마다: 그 층 칸의 글 조각들
|
|
216
|
+
const 지금행칸들 = () => 행칸들쌓임[표깊이] ?? null;
|
|
217
|
+
const 지금칸글 = () => 칸글쌓임[표깊이] ?? null;
|
|
180
218
|
let 글안 = false; // <t> 안인가 — 글 태그 밖의 지시문·수식 글을 안 줍기 위해
|
|
181
219
|
|
|
182
220
|
const 문단닫기 = () => {
|
|
@@ -189,40 +227,44 @@ export function 문단뽑기(xml, 갈래, { 표를따로 = false } = {}) {
|
|
|
189
227
|
if (t.text !== undefined) {
|
|
190
228
|
if (!글안 || t.blank) continue;
|
|
191
229
|
const 글 = unescapeXml(t.text);
|
|
192
|
-
|
|
230
|
+
const 칸 = 지금칸글();
|
|
231
|
+
if (칸) 칸.push(글);
|
|
193
232
|
else 글모음.push(글);
|
|
194
233
|
continue;
|
|
195
234
|
}
|
|
196
235
|
const 이름 = 끝이름(t.name);
|
|
197
236
|
|
|
198
237
|
if (이름 === 표기.글) { 글안 = !t.closing && !t.selfClosing; continue; }
|
|
199
|
-
if (표기.탭 && 이름 === 표기.탭 && !t.closing) { (
|
|
200
|
-
if (표기.줄바꿈 && 이름 === 표기.줄바꿈 && !t.closing) { (
|
|
238
|
+
if (표기.탭 && 이름 === 표기.탭 && !t.closing) { (지금칸글() ?? 글모음).push('\t'); continue; }
|
|
239
|
+
if (표기.줄바꿈 && 이름 === 표기.줄바꿈 && !t.closing) { (지금칸글() ?? 글모음).push('\n'); continue; }
|
|
201
240
|
|
|
202
241
|
if (이름 === 표기.표) { 표깊이 += t.closing ? -1 : (t.selfClosing ? 0 : 1); continue; }
|
|
203
242
|
if (표깊이 > 0 && 이름 === 표기.행) {
|
|
204
243
|
if (t.closing) {
|
|
205
|
-
|
|
206
|
-
|
|
244
|
+
const 담긴것 = 지금행칸들();
|
|
245
|
+
if (담긴것) {
|
|
246
|
+
const 칸 = 담긴것.map((x) => x.trim());
|
|
207
247
|
문단들.push(표를따로 ? { 행: 칸 } : 칸.join(' | '));
|
|
208
248
|
}
|
|
209
|
-
|
|
249
|
+
행칸들쌓임[표깊이] = null;
|
|
210
250
|
} else {
|
|
211
|
-
|
|
251
|
+
행칸들쌓임[표깊이] = [];
|
|
212
252
|
}
|
|
213
253
|
continue;
|
|
214
254
|
}
|
|
215
255
|
if (표깊이 > 0 && 이름 === 표기.칸) {
|
|
216
256
|
if (t.closing) {
|
|
217
|
-
|
|
218
|
-
|
|
257
|
+
const 담긴것 = 지금행칸들();
|
|
258
|
+
const 모은글 = 지금칸글();
|
|
259
|
+
if (담긴것 && 모은글) 담긴것.push(모은글.join(''));
|
|
260
|
+
칸글쌓임[표깊이] = null;
|
|
219
261
|
} else {
|
|
220
|
-
|
|
262
|
+
칸글쌓임[표깊이] = [];
|
|
221
263
|
}
|
|
222
264
|
continue;
|
|
223
265
|
}
|
|
224
266
|
|
|
225
|
-
if (이름 === 표기.문단 && t.closing &&
|
|
267
|
+
if (이름 === 표기.문단 && t.closing && !지금칸글()) 문단닫기();
|
|
226
268
|
}
|
|
227
269
|
문단닫기(); // 안 닫힌 채 끝나는 문서도 있다. 마지막 글을 버리지 않는다.
|
|
228
270
|
return 문단들;
|
|
@@ -240,6 +282,16 @@ const 알맹이 = {
|
|
|
240
282
|
pptx: { 골라 : /^ppt\/slides\/slide(\d+)\.xml$/i, 구획이름: (n, 번호) => `${번호}장` },
|
|
241
283
|
};
|
|
242
284
|
|
|
285
|
+
/** 꾸러미 속 이름표로 갈래를 짚는다. 경로 없이 버퍼만 받았을 때 쓴다. 모르면 null. */
|
|
286
|
+
function 꾸러미갈래(꾸러미) {
|
|
287
|
+
for (const [갈, { 골라 }] of Object.entries(알맹이)) {
|
|
288
|
+
for (const 이름 of 꾸러미.files.keys()) {
|
|
289
|
+
if (골라.test(이름.replace(/\\/g, '/'))) return 갈;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
243
295
|
/**
|
|
244
296
|
* 문서 하나를 읽는다.
|
|
245
297
|
*
|
|
@@ -248,16 +300,19 @@ const 알맹이 = {
|
|
|
248
300
|
* 예외가 나면 "문서가 깨졌다" 가 "도구가 터졌다" 로 보고된다.
|
|
249
301
|
*/
|
|
250
302
|
export function readDoc(경로또는버퍼, { 표를따로 = false } = {}) {
|
|
251
|
-
const
|
|
303
|
+
const 버퍼로왔나 = Buffer.isBuffer(경로또는버퍼);
|
|
304
|
+
let 갈래 = 버퍼로왔나 ? null : 종류(경로또는버퍼);
|
|
252
305
|
let buf;
|
|
253
306
|
try {
|
|
254
|
-
buf =
|
|
307
|
+
buf = 버퍼로왔나 ? 경로또는버퍼 : readFileSync(경로또는버퍼);
|
|
255
308
|
} catch (err) {
|
|
256
309
|
return { ok: false, error: `못 읽었습니다: ${err.message}` };
|
|
257
310
|
}
|
|
258
|
-
if (!갈래) return { ok: false, error: '어떤 문서인지 모르는 경로입니다' };
|
|
311
|
+
if (!갈래 && !버퍼로왔나) return { ok: false, error: '어떤 문서인지 모르는 경로입니다' };
|
|
259
312
|
if (!looksZip(buf)) {
|
|
260
|
-
return
|
|
313
|
+
return 갈래
|
|
314
|
+
? { ok: false, error: 겉속다름말(갈래, 속내용(buf)), 끝났다: true }
|
|
315
|
+
: { ok: false, error: `문서 꾸러미가 아닙니다 — ${속내용(buf) ?? '무엇인지 알아보지 못했습니다'}`, 끝났다: true };
|
|
261
316
|
}
|
|
262
317
|
|
|
263
318
|
let 꾸러미;
|
|
@@ -266,6 +321,18 @@ export function readDoc(경로또는버퍼, { 표를따로 = false } = {}) {
|
|
|
266
321
|
} catch (err) {
|
|
267
322
|
return { ok: false, error: `꾸러미를 풀지 못했습니다 — ${err.message}` };
|
|
268
323
|
}
|
|
324
|
+
/*
|
|
325
|
+
* 버퍼로 온 것은 **꾸러미 속을 보고** 갈래를 짚는다.
|
|
326
|
+
*
|
|
327
|
+
* 인자 이름이 `경로또는버퍼` 고 버퍼를 읽는 갈래까지 있는데 갈래는 경로의
|
|
328
|
+
* 확장자로만 정해서, 버퍼로 부르면 언제나 「어떤 문서인지 모르는 경로입니다」
|
|
329
|
+
* 였다 — 있는 척만 하는 죽은 길이다 (8회차). 확장자가 없으면 알맹이 이름표가
|
|
330
|
+
* 유일한 단서고, 셋은 서로 겹치지 않는다.
|
|
331
|
+
*/
|
|
332
|
+
if (!갈래) 갈래 = 꾸러미갈래(꾸러미);
|
|
333
|
+
if (!갈래) {
|
|
334
|
+
return { ok: false, error: '어떤 문서인지 모르는 꾸러미입니다 — hwpx·docx·pptx 의 본문이 없습니다' };
|
|
335
|
+
}
|
|
269
336
|
|
|
270
337
|
const { 골라, 구획이름 } = 알맹이[갈래];
|
|
271
338
|
const 찾은 = [];
|
|
@@ -273,6 +340,15 @@ export function readDoc(경로또는버퍼, { 표를따로 = false } = {}) {
|
|
|
273
340
|
const m = 골라.exec(이름.replace(/\\/g, '/'));
|
|
274
341
|
if (m) 찾은.push({ 번호: m[1] ? Number(m[1]) : 0, 몸 });
|
|
275
342
|
}
|
|
343
|
+
/*
|
|
344
|
+
* 목록에는 있는데 **못 푼** 구획 (2.0.0 3회차 사냥). readZip 이 건너뛴 것을 안 봐서, 석 장 중 한 장이 깨진
|
|
345
|
+
* 발표가 두 장짜리 온전한 글처럼 나갔다 — 모델은 그대로 「두 장입니다」 라고 요약한다. 그 자리에 못 읽었다는
|
|
346
|
+
* 문단을 세운다. 몇째 구획인지가 남아야 사람이 원본의 어디를 열어 볼지 안다.
|
|
347
|
+
*/
|
|
348
|
+
for (const s of 꾸러미.skipped ?? []) {
|
|
349
|
+
const m = 골라.exec(String(s.name).replace(/\\/g, '/'));
|
|
350
|
+
if (m) 찾은.push({ 번호: m[1] ? Number(m[1]) : 0, 몸: null, 못푼까닭: s.why });
|
|
351
|
+
}
|
|
276
352
|
if (!찾은.length) {
|
|
277
353
|
return { ok: false, error: `${갈래} 꾸러미인데 본문을 찾지 못했습니다. 깨졌거나 비정상 파일입니다.` };
|
|
278
354
|
}
|
|
@@ -280,7 +356,7 @@ export function readDoc(경로또는버퍼, { 표를따로 = false } = {}) {
|
|
|
280
356
|
|
|
281
357
|
const 덩이들 = 찾은.map((s, i) => ({
|
|
282
358
|
이름: 구획이름(i, s.번호),
|
|
283
|
-
문단들: 문단뽑기(s.몸.toString('utf8'), 갈래, { 표를따로 }),
|
|
359
|
+
문단들: s.몸 ? 문단뽑기(s.몸.toString('utf8'), 갈래, { 표를따로 }) : [`(이 구획은 못 읽었습니다 — ${s.못푼까닭})`],
|
|
284
360
|
}));
|
|
285
361
|
return { ok: true, 갈래, 덩이들 };
|
|
286
362
|
}
|
package/src/tools/edit-match.js
CHANGED
|
@@ -9,6 +9,7 @@ const TIERS = [
|
|
|
9
9
|
{ id: 'trail', label: '줄 끝 공백·줄바꿈 차이 무시' },
|
|
10
10
|
{ id: 'indent', label: '들여쓰기 차이 무시' },
|
|
11
11
|
{ id: 'space', label: '모든 공백 차이 무시' },
|
|
12
|
+
{ id: 'nfc', label: '유니코드 정규화(NFC·NFD) 차이 무시' },
|
|
12
13
|
];
|
|
13
14
|
|
|
14
15
|
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -20,27 +21,41 @@ function pattern(needle, tier) {
|
|
|
20
21
|
// 줄 끝 공백과 CRLF/LF 차이를 흡수한다.
|
|
21
22
|
return lines.map((l) => esc(l.replace(/[ \t]+$/, ''))).join('[ \\t]*\\r?\\n');
|
|
22
23
|
case 'indent':
|
|
23
|
-
// 줄 앞 들여쓰기까지 흡수한다.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// 줄 앞 들여쓰기까지 흡수한다. 다만 끝이 줄바꿈이면(마지막 조각이 빈 줄) 그 뒤는 **다음 줄**이라
|
|
25
|
+
// 들여쓰기를 안 먹는다 — 먹으면 갈아 끼울 때 다음 줄이 들여쓰기를 잃었다 (6회차 Gemini 고침맞추기6).
|
|
26
|
+
return lines
|
|
27
|
+
.map((l, i) => (i > 0 && i === lines.length - 1 && !l.trim() ? '' : '[ \\t]*') + esc(l.trim()))
|
|
28
|
+
.join('[ \\t]*\\r?\\n');
|
|
29
|
+
case 'space': {
|
|
30
|
+
// 공백이라면 종류·개수를 가리지 않는다. 다만 **끝** 공백은 그 안의 줄바꿈 수만큼만 본다 — `\s+` 가
|
|
31
|
+
// 다음 줄 들여쓰기·빈 줄·줄바꿈까지 먹어 갈아 끼우면 사라졌다 (6회차 Gemini 고침맞추기6).
|
|
32
|
+
const 꼬리 = /\s*$/.exec(needle)[0];
|
|
33
|
+
const 몸 = esc(needle.slice(0, needle.length - 꼬리.length)).replace(/\s+/g, '\\s+');
|
|
34
|
+
const 줄 = (꼬리.match(/\n/g) ?? []).length;
|
|
35
|
+
if (줄) return 몸 + '[ \\t]*\\r?\\n'.repeat(줄);
|
|
36
|
+
return 꼬리 ? `${몸}[ \\t]*(?=\\s|$)` : 몸;
|
|
37
|
+
}
|
|
28
38
|
default:
|
|
29
39
|
return null;
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
// 찾은 자리들을 모두 돌려준다.
|
|
34
|
-
|
|
44
|
+
//
|
|
45
|
+
// `전부` 는 replace_all 일 때다. 그때는 **정말 전부** 모은다 — 아래 상한 머리말.
|
|
46
|
+
function findAll(text, needle, tier, { 전부 = false } = {}) {
|
|
35
47
|
if (tier === 'exact') {
|
|
36
48
|
const out = [];
|
|
37
49
|
let i = 0;
|
|
38
50
|
while ((i = text.indexOf(needle, i)) >= 0) {
|
|
39
51
|
out.push({ start: i, end: i + needle.length });
|
|
40
|
-
|
|
52
|
+
// 모호한지 볼 때는 **겹친 자리도** 센다. 겹치지 않게만 세면 `}\n}\n}` 안의 `}\n}` 두 자리가 한 곳으로
|
|
53
|
+
// 잡혀 첫 자리를 조용히 고쳤다 (6회차 Gemini 고침맞추기6). replace_all 은 여태처럼 안 겹치게 바꾼다.
|
|
54
|
+
i += 전부 ? needle.length || 1 : 1;
|
|
41
55
|
}
|
|
42
56
|
return out;
|
|
43
57
|
}
|
|
58
|
+
if (tier === 'nfc') return 정규화로찾기(text, needle, 전부);
|
|
44
59
|
const p = pattern(needle, tier);
|
|
45
60
|
if (!p) return [];
|
|
46
61
|
let re;
|
|
@@ -56,15 +71,140 @@ function findAll(text, needle, tier) {
|
|
|
56
71
|
// 여기서는 못 찾은 것으로 친다. 정확히 일치하는 경우는 위에서 indexOf 로 이미
|
|
57
72
|
// 처리했으니, 느슨하게 맞춰 보는 이 길만 포기하는 것이다.
|
|
58
73
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
if (전부) {
|
|
75
|
+
// replace_all 은 여태처럼 **안 겹치게** 걷는다 — 바꿀 자리를 고르는 길이다.
|
|
76
|
+
for (const m of text.matchAll(re)) {
|
|
77
|
+
if (m[0].length === 0) continue;
|
|
78
|
+
out.push({ start: m.index, end: m.index + m[0].length });
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
/*
|
|
82
|
+
* ── 모호한지 볼 때는 **겹친 자리도** 센다 (6회차 고침맞추기6dq G2) ──
|
|
83
|
+
*
|
|
84
|
+
* 위 「정확히」 단계는 `i += 1` 로 겹친 자리까지 세어 `}\n}\n}` 안의
|
|
85
|
+
* `}\n}` 두 자리를 모호로 막는다. 그런데 여기는 `matchAll` 이라 한 번
|
|
86
|
+
* 맞으면 그 끝까지 건너뛴다 — 같은 글에 들여쓰기만 붙으면(` }` 세 줄,
|
|
87
|
+
* 파이썬·중괄호 닫는 줄에서 흔한 꼴) 두 자리 중 앞엣것 하나만 잡히고
|
|
88
|
+
* **조용히 첫 자리를 고쳤다.** 못 찾는 것보다 엉뚱한 곳을 고치는 것이
|
|
89
|
+
* 나쁘다는 이 파일의 원칙이 단계마다 달랐던 자리다.
|
|
90
|
+
*
|
|
91
|
+
* `exec` 로 한 칸씩 밀며 걷는다. 빈 맞춤은 제자리걸음이 되므로 건너뛴다.
|
|
92
|
+
*
|
|
93
|
+
* 다만 **끝자리가 같은 것은 한 자리다.** 이 단계의 무늬는 `[ \t]*` 로 시작해서
|
|
94
|
+
* ` foo` 의 0·1·2 어디서 시작해도 같은 자리에 닿는다 — 그걸 따로 세면 들여쓴
|
|
95
|
+
* 줄은 죄다 「2군데」 가 되어, 조이려던 것이 **멀쩡한 고치기를 다 막는다.**
|
|
96
|
+
* 먼저 나온 것(가장 이른 시작)을 남긴다 — matchAll 이 주던 자리와 같다.
|
|
97
|
+
*/
|
|
98
|
+
const 끝본것 = new Set();
|
|
99
|
+
for (let i = 0; i <= text.length;) {
|
|
100
|
+
re.lastIndex = i;
|
|
101
|
+
const m = re.exec(text);
|
|
102
|
+
if (!m) break;
|
|
103
|
+
const 끝 = m.index + m[0].length;
|
|
104
|
+
if (m[0].length > 0 && !끝본것.has(끝)) { 끝본것.add(끝); out.push({ start: m.index, end: 끝 }); }
|
|
105
|
+
/*
|
|
106
|
+
* ── 이 상한은 **모호한지 볼 때만** 쓴다 ──────────────────────────
|
|
107
|
+
*
|
|
108
|
+
* 「너무 많으면 어차피 모호하다」 는 replace_all 이 아닐 때의 말이다.
|
|
109
|
+
* 그런데 replace_all 에도 똑같이 걸려서, 공백만 조금 달라 느슨하게 맞은
|
|
110
|
+
* 100군데 중 **51군데만** 바꾸고 「고침: (51군데)」 라고 답했다.
|
|
111
|
+
* 나머지 49군데는 그대로인데 모델은 다 바꾼 줄 알고 넘어간다. 정확히
|
|
112
|
+
* 맞은 경우(indexOf 길)는 상한이 없어서 느슨한 길에서만 새던 자리다.
|
|
113
|
+
*/
|
|
114
|
+
if (out.length > 50) break;
|
|
115
|
+
i = m.index + 1;
|
|
116
|
+
}
|
|
63
117
|
}
|
|
64
118
|
} catch { return []; }
|
|
65
119
|
return out;
|
|
66
120
|
}
|
|
67
121
|
|
|
122
|
+
/*
|
|
123
|
+
* ── 눈에는 같은 글자인데 **코드가 다른** 글 (NFC · NFD) ──────────────────
|
|
124
|
+
*
|
|
125
|
+
* 맥에서 만든 파일, 맥 파일 이름을 옮겨 적은 글, 일부 PDF 에서 긁은 글은 한글이
|
|
126
|
+
* 자모로 풀린 꼴(NFD — ㅎ+ㅏ+ㄴ)로 들어 있다. 모델은 늘 모아 쓴 꼴(NFC)로
|
|
127
|
+
* 적는다. 화면에는 둘 다 「한글」 인데 indexOf 도 위 느슨한 단계도 못 찾아서,
|
|
128
|
+
* 모델은 「찾지 못했습니다」 를 받고 Read 로 다시 읽고 **같은 글자로** 또 보내며
|
|
129
|
+
* 헛돌았다. `é` 를 e + ◌́ 로 적은 글도 같다. 공백은 흡수하면서 이건 못 했다.
|
|
130
|
+
*
|
|
131
|
+
* 둘 다 NFD 로 풀어 견준다. 풀기는 글자마다 따로 되고 결합 표시의 순서만
|
|
132
|
+
* 제자리에서 바뀌므로, **결합하는 글자가 아닌 자리**에서 시작하고 끝나는 곳은
|
|
133
|
+
* 원래 글의 자리로 되짚을 수 있다. 글자 한가운데(모음 자모 하나 · 결합 표시
|
|
134
|
+
* 하나)에서 시작하거나 끝나는 곳은 버린다 — 그걸 고치면 글자가 쪼개진다.
|
|
135
|
+
* 되짚은 자리는 한 번 더 정규화해 견주어 확인한다. 모호하면 거부하는 것은
|
|
136
|
+
* 다른 단계와 같다.
|
|
137
|
+
*/
|
|
138
|
+
const 결합하는글자 = /^[\p{M}ᅠ-ᇿힰ-]/u;
|
|
139
|
+
|
|
140
|
+
function 정규화로찾기(text, needle, 전부) {
|
|
141
|
+
const 풀린글 = text.normalize('NFD');
|
|
142
|
+
const 풀린바늘 = needle.normalize('NFD');
|
|
143
|
+
// 둘 다 이미 풀린 꼴이면 위 「정확히」 단계가 이미 본 것이다.
|
|
144
|
+
if (!풀린바늘 || (풀린글 === text && 풀린바늘 === needle)) return [];
|
|
145
|
+
// 바늘이 결합하는 글자로 시작하면 어느 자리든 글자 한가운데서 시작한다 (「아각」 의 ㅏ+각).
|
|
146
|
+
if (결합하는글자.test(풀린바늘)) return [];
|
|
147
|
+
|
|
148
|
+
const 자리들 = [];
|
|
149
|
+
for (let i = 풀린글.indexOf(풀린바늘); i >= 0; i = 풀린글.indexOf(풀린바늘, i + 1)) {
|
|
150
|
+
const 끝 = i + 풀린바늘.length;
|
|
151
|
+
// 바로 뒤가 결합하는 글자면 글자 한가운데서 끝난다 — 「각」 안의 「가」 를 고치면 받침이 딴 글자에 붙는다.
|
|
152
|
+
if (끝 < 풀린글.length && 결합하는글자.test(풀린글.slice(끝, 끝 + 1))) continue;
|
|
153
|
+
자리들.push([i, 끝]);
|
|
154
|
+
// 정확히 단계와 같다 — replace_all 이 아니면 겹친 자리도 세어 모호함을 가린다.
|
|
155
|
+
if (전부) i = 끝 - 1;
|
|
156
|
+
if (!전부 && 자리들.length > 50) break;
|
|
157
|
+
}
|
|
158
|
+
if (!자리들.length) return [];
|
|
159
|
+
|
|
160
|
+
// 풀린 글의 자리 → 원래 글의 자리. 글자(코드포인트)마다 풀린 길이를 더해 간다.
|
|
161
|
+
const 필요 = new Set(자리들.flat());
|
|
162
|
+
const 맨끝 = 자리들[자리들.length - 1][1];
|
|
163
|
+
const 되짚기 = new Map();
|
|
164
|
+
const 풀린길이 = new Map();
|
|
165
|
+
let t = 0;
|
|
166
|
+
let k = 0;
|
|
167
|
+
for (;;) {
|
|
168
|
+
if (필요.has(t) && !되짚기.has(t)) 되짚기.set(t, k);
|
|
169
|
+
if (k >= text.length || t >= 맨끝) break;
|
|
170
|
+
const cp = text.codePointAt(k);
|
|
171
|
+
const 폭 = cp > 0xFFFF ? 2 : 1;
|
|
172
|
+
let d = 폭;
|
|
173
|
+
if (cp >= 0xC0) {
|
|
174
|
+
d = 풀린길이.get(cp);
|
|
175
|
+
if (d === undefined) { d = String.fromCodePoint(cp).normalize('NFD').length; 풀린길이.set(cp, d); }
|
|
176
|
+
}
|
|
177
|
+
k += 폭;
|
|
178
|
+
t += d;
|
|
179
|
+
}
|
|
180
|
+
const 모은꼴 = needle.normalize('NFC');
|
|
181
|
+
const out = [];
|
|
182
|
+
for (const [i, 끝] of 자리들) {
|
|
183
|
+
const start = 되짚기.get(i);
|
|
184
|
+
const end = 되짚기.get(끝);
|
|
185
|
+
if (start === undefined || end === undefined || end <= start) continue;
|
|
186
|
+
if (text.slice(start, end).normalize('NFC') !== 모은꼴) continue;
|
|
187
|
+
out.push({ start, end });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 정규화 단계로 찾았으면 넣는 글도 **파일이 쓰는 꼴**로 맞춘다.
|
|
194
|
+
*
|
|
195
|
+
* NFD 파일 한가운데에 NFC 글을 넣으면 같은 파일 안에 두 꼴이 섞인다 — 다음
|
|
196
|
+
* 고치기에서 또 못 찾고, 맥의 파일 비교 도구는 멀쩡한 줄을 바뀌었다고 한다.
|
|
197
|
+
* 찾은 자리가 한 꼴로만 되어 있을 때만 맞춘다. 섞였으면 그대로 둔다.
|
|
198
|
+
*/
|
|
199
|
+
export function 꼴맞추기(newString, matched) {
|
|
200
|
+
const 풀린 = matched.normalize('NFD');
|
|
201
|
+
const 모은 = matched.normalize('NFC');
|
|
202
|
+
if (풀린 === 모은) return newString;
|
|
203
|
+
if (matched === 풀린) return newString.normalize('NFD');
|
|
204
|
+
if (matched === 모은) return newString.normalize('NFC');
|
|
205
|
+
return newString;
|
|
206
|
+
}
|
|
207
|
+
|
|
68
208
|
/**
|
|
69
209
|
* @returns {{ok:true, spans, tier, tierLabel}} 또는
|
|
70
210
|
* {{ok:false, reason, tier?, count?, near?}}
|
|
@@ -73,7 +213,7 @@ export function findMatch(text, needle, { replaceAll = false } = {}) {
|
|
|
73
213
|
if (!needle) return { ok: false, reason: 'empty' };
|
|
74
214
|
|
|
75
215
|
for (const t of TIERS) {
|
|
76
|
-
const spans = findAll(text, needle, t.id);
|
|
216
|
+
const spans = findAll(text, needle, t.id, { 전부: replaceAll });
|
|
77
217
|
if (!spans.length) continue;
|
|
78
218
|
if (spans.length > 1 && !replaceAll) {
|
|
79
219
|
return { ok: false, reason: 'ambiguous', tier: t.id, count: spans.length };
|
|
@@ -135,15 +275,74 @@ export function reindent(newString, matchedText, needle) {
|
|
|
135
275
|
const shiftable = lines.every((l, i) => i === 0 || !l.trim() || l.startsWith(needleIndent));
|
|
136
276
|
if (!shiftable) return newString;
|
|
137
277
|
|
|
278
|
+
const 더들여쓰기 = 단위바꾸개(fileIndent, needleIndent);
|
|
138
279
|
return lines
|
|
139
280
|
.map((l, i) => {
|
|
140
281
|
if (i === 0) return fileIndent + l.replace(/^[ \t]*/, '');
|
|
141
282
|
if (!l.trim()) return l;
|
|
142
|
-
|
|
283
|
+
const 나머지 = l.slice(needleIndent.length);
|
|
284
|
+
const 더 = /^[ \t]*/.exec(나머지)[0];
|
|
285
|
+
return fileIndent + 더들여쓰기(더) + 나머지.slice(더.length);
|
|
143
286
|
})
|
|
144
287
|
.join('\n');
|
|
145
288
|
}
|
|
146
289
|
|
|
290
|
+
/*
|
|
291
|
+
* ── 바깥 들여쓰기만 갈아 끼우면 **안쪽이 섞인다** ─────────────────────────
|
|
292
|
+
*
|
|
293
|
+
* 위 reindent 는 줄 앞의 「모델 들여쓰기」 를 「파일 들여쓰기」 로 바꾸기만
|
|
294
|
+
* 했다. 탭으로 쓴 파일에 모델이 네 칸 공백으로 적어 보내면 이렇게 됐다:
|
|
295
|
+
*
|
|
296
|
+
* 파일 \tif (x) {\n\t\treturn 1;
|
|
297
|
+
* 모델 ····if (y) {\n········return 2;
|
|
298
|
+
* 넣은 것 \tif (y) {\n\t····return 2; ← 탭 뒤에 공백 넷
|
|
299
|
+
*
|
|
300
|
+
* 바깥 한 단만 탭이고 그 안쪽 한 단은 모델의 공백이 그대로 남는다. 파이썬이면
|
|
301
|
+
* 그 자리에서 TabError 고, 아니어도 편집기마다 들여쓰기가 달리 보인다.
|
|
302
|
+
*
|
|
303
|
+
* 모델 들여쓰기와 파일 들여쓰기가 **한 종류씩**이고(탭만 · 공백만) 나누어
|
|
304
|
+
* 떨어질 때만 한 단의 폭을 알 수 있다 — 탭 하나를 모델이 몇 칸으로 적었나.
|
|
305
|
+
* 그 폭으로 안쪽 들여쓰기도 옮긴다. 폭을 모르면(바깥이 비었거나 섞였으면)
|
|
306
|
+
* 여태처럼 그대로 둔다. 어설피 고치면 더 나쁘다는 위 머리말과 같은 자세다.
|
|
307
|
+
*/
|
|
308
|
+
function 단위바꾸개(fileIndent, needleIndent) {
|
|
309
|
+
const 탭만 = (s) => /^\t+$/.test(s);
|
|
310
|
+
const 칸만 = (s) => /^ +$/.test(s);
|
|
311
|
+
if (탭만(fileIndent) && 칸만(needleIndent) && needleIndent.length % fileIndent.length === 0) {
|
|
312
|
+
const 폭 = needleIndent.length / fileIndent.length;
|
|
313
|
+
return (더) => (칸만(더) ? '\t'.repeat(Math.floor(더.length / 폭)) + ' '.repeat(더.length % 폭) : 더);
|
|
314
|
+
}
|
|
315
|
+
if (칸만(fileIndent) && 탭만(needleIndent) && fileIndent.length % needleIndent.length === 0) {
|
|
316
|
+
const 폭 = fileIndent.length / needleIndent.length;
|
|
317
|
+
return (더) => (탭만(더) ? ' '.repeat(더.length * 폭) : 더);
|
|
318
|
+
}
|
|
319
|
+
return (더) => 더;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/*
|
|
323
|
+
* ── 넣는 글의 줄끝은 **파일의 줄끝**을 따른다 ────────────────────────────
|
|
324
|
+
*
|
|
325
|
+
* 모델은 줄바꿈을 늘 `\n` 으로 적는다. 느슨하게 찾는 길(trail)은 그 차이를
|
|
326
|
+
* 흡수해서 CRLF 파일에서도 자리를 찾아 주는데, 넣는 쪽은 모델이 준 `\n` 을
|
|
327
|
+
* 그대로 넣었다. 그래서 `.bat` 두 줄을 세 줄로 고치면 —
|
|
328
|
+
*
|
|
329
|
+
* @echo off\r\n set A=10\n set B=20\n set C=30\r\n goto :end\r\n
|
|
330
|
+
*
|
|
331
|
+
* 고친 자리만 LF 인 파일이 된다. cmd.exe 는 LF 만 있는 줄에서 레이블(goto)을
|
|
332
|
+
* 헛짚고, git 은 줄마다 바뀌었다고 하고, /diff 에는 글자가 같아 안 보인다.
|
|
333
|
+
*
|
|
334
|
+
* CRLF **만** 쓰는 파일일 때만 맞춘다. 이미 섞인 파일은 어느 쪽이 맞는지
|
|
335
|
+
* 모르니 손대지 않는다.
|
|
336
|
+
*/
|
|
337
|
+
export function CRLF뿐인가(text) {
|
|
338
|
+
const t = String(text ?? '');
|
|
339
|
+
return t.includes('\r\n') && !/(^|[^\r])\n/.test(t);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function CRLF로(s) {
|
|
343
|
+
return String(s).replace(/\r?\n/g, '\r\n');
|
|
344
|
+
}
|
|
345
|
+
|
|
147
346
|
// 찾은 자리들을 실제로 바꾼다. 뒤에서부터 바꿔야 앞쪽 위치가 안 밀린다.
|
|
148
347
|
export function applySpans(text, spans, makeReplacement) {
|
|
149
348
|
let out = text;
|