deel-local-cli 2.0.1 → 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 +13 -5
- package/README.md +13 -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.js +4 -2
- package/src/completion.js +13 -3
- package/src/config.js +33 -11
- package/src/configexplain.js +1 -1
- 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 +56 -6
- 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/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 +1 -1
- package/src/tools/fsutil.js +8 -4
- package/src/tools/ignore.js +21 -1
- package/src/tools/index.js +21 -2
- package/src/tools/jobs.js +23 -4
- package/src/tools/shell.js +27 -0
- package/src/tools/todo.js +1 -1
- package/src/tools/webfetch.js +1 -1
- 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/plugins/manage.js
CHANGED
|
@@ -6,7 +6,7 @@ import { execFile } from 'node:child_process';
|
|
|
6
6
|
import { homeDir } from '../config.js';
|
|
7
7
|
import { join, dirname, basename, resolve, relative, sep } from 'node:path';
|
|
8
8
|
import {
|
|
9
|
-
existsSync, mkdirSync, writeFileSync, readFileSync, rmSync,
|
|
9
|
+
existsSync, mkdirSync, writeFileSync, readFileSync, rmSync, renameSync,
|
|
10
10
|
readdirSync, statSync,
|
|
11
11
|
} from 'node:fs';
|
|
12
12
|
import { untargz, stripTop, 밖을가리키는것 } from '../pack/tar.js';
|
|
@@ -107,13 +107,26 @@ export function 묶음풀기(dest, files) {
|
|
|
107
107
|
* 고 하고 하나만 남겼다. 쓰기 전에 전부 재 보고 사람 말로 거절한다.
|
|
108
108
|
*/
|
|
109
109
|
const 뿌리 = resolve(dest);
|
|
110
|
-
|
|
110
|
+
/*
|
|
111
|
+
* ── 쓸 이름도 **잰 이름과 같게** (2.0.2 · P3 · P1) ──────────────────────────
|
|
112
|
+
*
|
|
113
|
+
* 아래 재기는 역슬래시를 빗금으로 보고 쟀는데, 쓸 때는 적힌 이름 그대로 `join` 했다.
|
|
114
|
+
* 윈도에서는 둘이 같지만 리눅스·맥에서는 역슬래시가 **이름 글자**라, 윈도에서 만든
|
|
115
|
+
* 묶음의 `sub\file.txt` 가 `sub` 폴더 안이 아니라 뿌리에 한 파일로 풀렸다 — 스킬이
|
|
116
|
+
* 제자리를 못 찾는다. 이름을 한 번 빗금으로 편 것을 재기와 쓰기가 같이 쓴다.
|
|
117
|
+
*
|
|
118
|
+
* 같은 자리 견주기에는 NFC 도 맞춘다. 맥은 `한글`(NFC)과 `한글`(NFD)을 한 파일로
|
|
119
|
+
* 보므로, 두 꼴이 같이 든 묶음은 맥에서 하나가 말없이 사라진다. 대소문자만 다른
|
|
120
|
+
* 이름과 같은 대접으로 묶음째 거절한다.
|
|
121
|
+
*/
|
|
122
|
+
const 편이름 = (f) => String(f.name ?? '').replace(/\\/g, '/');
|
|
123
|
+
const 자리들 = new Map(); // 소문자·NFC 상대 자리 → 적힌 이름
|
|
111
124
|
for (const f of files) {
|
|
112
|
-
const 상대 = relative(뿌리, resolve(뿌리,
|
|
125
|
+
const 상대 = relative(뿌리, resolve(뿌리, 편이름(f))).replace(/\\/g, '/');
|
|
113
126
|
if (!상대) return { error: `묶음 안에 풀 폴더 그 자체를 가리키는 파일이 있습니다 — 풀지 않았습니다: ${f.name}` };
|
|
114
|
-
const 열쇠 = 상대.toLowerCase();
|
|
127
|
+
const 열쇠 = 상대.normalize('NFC').toLowerCase();
|
|
115
128
|
if (자리들.has(열쇠)) {
|
|
116
|
-
return { error: `묶음 안에 같은 자리를 가리키는 이름이 둘 있습니다(
|
|
129
|
+
return { error: `묶음 안에 같은 자리를 가리키는 이름이 둘 있습니다(대소문자나 한글 자모 꼴만 다를 수 있음) — 풀지 않았습니다: ${자리들.get(열쇠)} · ${f.name}` };
|
|
117
130
|
}
|
|
118
131
|
자리들.set(열쇠, f.name);
|
|
119
132
|
}
|
|
@@ -126,13 +139,32 @@ export function 묶음풀기(dest, files) {
|
|
|
126
139
|
}
|
|
127
140
|
rmSync(dest, { recursive: true, force: true });
|
|
128
141
|
for (const f of files) {
|
|
129
|
-
const p = join(dest, f
|
|
142
|
+
const p = join(dest, 편이름(f));
|
|
130
143
|
mkdirSync(dirname(p), { recursive: true });
|
|
131
144
|
writeFileSync(p, f.data);
|
|
132
145
|
}
|
|
133
146
|
return { 푼것: files.length };
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
/*
|
|
150
|
+
* ── git 없이 받을 주소들 (2.0.2 · P5) ─────────────────────────────────────────
|
|
151
|
+
*
|
|
152
|
+
* `#v1.0.0` 처럼 **태그**를 적어도 `refs/heads/` 만 물어서, git 이 없는 PC 에서는 늘
|
|
153
|
+
* 「받지 못했습니다 — 가지 이름을 확인하세요」 였다. git 이 있으면 `clone --branch` 가
|
|
154
|
+
* 태그도 받으니, 같은 명령이 git 유무에 따라 되고 안 됐다. 가지를 먼저 묻고 없으면
|
|
155
|
+
* 태그를 묻는다(같은 이름이면 git 도 가지를 먼저 고른다).
|
|
156
|
+
*/
|
|
157
|
+
export function 받을주소들(spec) {
|
|
158
|
+
const 뿌리 = `https://codeload.github.com/${spec.owner}/${spec.repo}/tar.gz`;
|
|
159
|
+
if (spec.ref) {
|
|
160
|
+
return [
|
|
161
|
+
{ branch: spec.ref, url: `${뿌리}/refs/heads/${spec.ref}` },
|
|
162
|
+
{ branch: spec.ref, url: `${뿌리}/refs/tags/${spec.ref}` },
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
return ['main', 'master'].map((b) => ({ branch: b, url: `${뿌리}/refs/heads/${b}` }));
|
|
166
|
+
}
|
|
167
|
+
|
|
136
168
|
// 받는 방법 두 가지. git 이 있으면 clone, 없으면 tarball 을 내려받아 푼다.
|
|
137
169
|
async function fetchInto(spec, dest, onStep) {
|
|
138
170
|
if (await has('git')) {
|
|
@@ -150,8 +182,7 @@ async function fetchInto(spec, dest, onStep) {
|
|
|
150
182
|
|
|
151
183
|
onStep?.('tarball 내려받기');
|
|
152
184
|
if (isOffline()) return { error: '오프라인 모드입니다 — 받아 올 수 없습니다. 풀어 놓은 폴더 경로를 주세요.' };
|
|
153
|
-
for (const branch
|
|
154
|
-
const url = `https://codeload.github.com/${spec.owner}/${spec.repo}/tar.gz/refs/heads/${branch}`;
|
|
185
|
+
for (const { branch, url } of 받을주소들(spec)) {
|
|
155
186
|
// 사용자가 이 명령을 친 동안만 github 를 연다. 끝나면 바로 닫는다.
|
|
156
187
|
const close = allowTemporarily(url);
|
|
157
188
|
let gz;
|
|
@@ -183,7 +214,7 @@ async function fetchInto(spec, dest, onStep) {
|
|
|
183
214
|
if (푼것.error) return { error: 푼것.error };
|
|
184
215
|
return { how: 'tarball', branch };
|
|
185
216
|
}
|
|
186
|
-
return { error: '받지 못했습니다 — 저장소 주소나
|
|
217
|
+
return { error: '받지 못했습니다 — 저장소 주소나 가지·태그 이름을 확인하세요' };
|
|
187
218
|
}
|
|
188
219
|
|
|
189
220
|
/**
|
|
@@ -246,7 +277,7 @@ export function 이름한칸(이름) {
|
|
|
246
277
|
* 자르는 자가 언젠가 틀릴 수 있다. 그때 마지막으로 서는 그물이다. 지우고
|
|
247
278
|
* 쓰는 자리라 두 겹으로 막는다.
|
|
248
279
|
*/
|
|
249
|
-
|
|
280
|
+
function 플러그인자리인가(base, dest) {
|
|
250
281
|
const 뿌리 = resolve(base);
|
|
251
282
|
const 갈곳 = resolve(dest);
|
|
252
283
|
return 갈곳 !== 뿌리 && 갈곳.startsWith(뿌리 + sep);
|
|
@@ -312,7 +343,7 @@ export async function install(spec, 옵션 = {}) {
|
|
|
312
343
|
}
|
|
313
344
|
}
|
|
314
345
|
|
|
315
|
-
async function 설치하기(spec, { home = homeDir(), onStep } = {}, 치울것 = []) {
|
|
346
|
+
async function 설치하기(spec, { home = homeDir(), onStep, 복사 = copyDir } = {}, 치울것 = []) {
|
|
316
347
|
const base = pluginsDir(home);
|
|
317
348
|
|
|
318
349
|
// 이미 풀어 놓은 폴더를 그대로 넣는 길. 오프라인 기기에서 이쪽을 쓴다.
|
|
@@ -383,9 +414,43 @@ async function 설치하기(spec, { home = homeDir(), onStep } = {}, 치울것 =
|
|
|
383
414
|
return { error: `스킬도 명령도 없습니다. 플러그인이 맞는지 확인하세요 (${isLocal ? asPath : parsed.owner + '/' + parsed.repo})` };
|
|
384
415
|
}
|
|
385
416
|
|
|
386
|
-
|
|
417
|
+
/*
|
|
418
|
+
* ── 옛것을 **먼저 지우지 않는다** (2.0.2 · P7) ──────────────────────────────
|
|
419
|
+
*
|
|
420
|
+
* 다시 설치(업데이트)는 옛 폴더를 지우고 새것을 베꼈다. 베끼다 넘어지면(디스크가
|
|
421
|
+
* 꽉 참 · 잠긴 파일 · 긴 경로) **둘 다 잃었다** — 옛것은 지웠고 새것은 반쪽이다.
|
|
422
|
+
* 업데이트 한 번에 쓰던 플러그인이 사라지는 것이다.
|
|
423
|
+
*
|
|
424
|
+
* 옛것을 점 이름(`.old-…` — list() 가 안 보는 이름)으로 비켜 두고 베낀다. 다 베끼면
|
|
425
|
+
* 비켜 둔 것을 버리고, 넘어지면 반쪽을 치우고 비켜 둔 것을 제자리로 되돌린다.
|
|
426
|
+
* 비키기(rename)부터 안 되면(윈도에서 그 폴더의 파일을 연 프로그램이 있으면 흔하다)
|
|
427
|
+
* 아무것도 안 건드리고 그렇다고 말한다.
|
|
428
|
+
*/
|
|
387
429
|
mkdirSync(dirname(dest), { recursive: true });
|
|
388
|
-
|
|
430
|
+
const 옛것있나 = existsSync(dest);
|
|
431
|
+
const 비켜둘곳 = join(base, `.old-${name}-${process.pid}-${Date.now()}`);
|
|
432
|
+
if (옛것있나) {
|
|
433
|
+
try { renameSync(dest, 비켜둘곳); }
|
|
434
|
+
catch (err) {
|
|
435
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
436
|
+
return { error: `깔려 있던 ${name} 을 비키지 못해 다시 설치하지 않았습니다 — 쓰던 것은 그대로입니다 (${err?.code ?? err?.message ?? err}). 그 폴더의 파일을 연 프로그램을 닫고 다시 하세요.` };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
복사(tmp, dest, { skipped: 건너뛴것 });
|
|
441
|
+
} catch (err) {
|
|
442
|
+
try { rmSync(dest, { recursive: true, force: true }); } catch { /* 반쪽을 못 치우면 아래 되돌리기가 넘어진다 — 그 말도 한다 */ }
|
|
443
|
+
let 되돌림 = '쓰던 것은 그대로입니다';
|
|
444
|
+
if (옛것있나) {
|
|
445
|
+
try { renameSync(비켜둘곳, dest); }
|
|
446
|
+
catch { 되돌림 = `쓰던 것은 ${비켜둘곳} 에 남아 있습니다 — 폴더 이름을 ${name} 으로 되돌리면 됩니다`; }
|
|
447
|
+
} else {
|
|
448
|
+
되돌림 = '반쪽은 치웠습니다';
|
|
449
|
+
}
|
|
450
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
451
|
+
return { error: `${name} 을 베끼지 못해 설치하지 않았습니다 — ${되돌림} (${err?.code ?? err?.message ?? err})` };
|
|
452
|
+
}
|
|
453
|
+
if (옛것있나) { try { rmSync(비켜둘곳, { recursive: true, force: true }); } catch { /* 못 버려도 list() 는 점 이름을 안 본다 */ } }
|
|
389
454
|
rmSync(tmp, { recursive: true, force: true });
|
|
390
455
|
// 개수는 **깔린 자리**에서 다시 센다. 복사하다 빠진 것이 여기서 빠진다.
|
|
391
456
|
const counts = countIn(dest);
|
|
@@ -569,7 +634,9 @@ export function pack(outFile, { home = homeDir(), only = null } = {}) {
|
|
|
569
634
|
].join('\n');
|
|
570
635
|
entries.unshift({ name: '사용안내.txt', data: Buffer.from(manifest, 'utf8') });
|
|
571
636
|
|
|
572
|
-
|
|
637
|
+
// 한도(파일 65,535개 · 4GB)를 넘으면 makeZip 이 사람 말로 던진다 — 명령째 죽지 않게 받아 올린다 (2.0.2 · Z5).
|
|
638
|
+
let zip;
|
|
639
|
+
try { zip = makeZip(entries); } catch (err) { return { error: String(err?.message ?? err) }; }
|
|
573
640
|
mkdirSync(dirname(outFile), { recursive: true });
|
|
574
641
|
writeFileSync(outFile, zip);
|
|
575
642
|
return { out: outFile, plugins: included, files: entries.length, skipped, bytes: zip.length, manifest };
|
package/src/reap.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 신호로 끝날 때 아이들을 거두는 **하나뿐인** 손 (2.0.2 · M3).
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 한 자리로 모았나 ──────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 'exit' 그물(MCP 서버 · 일감 · 언어 서버가 저마다 건 것)은 **신호로 끝나면 안 돈다.**
|
|
7
|
+
* Node 는 SIGINT·SIGTERM 에 손이 없으면 그 자리에서 죽고, 그 길에는 'exit' 이 없다.
|
|
8
|
+
*
|
|
9
|
+
* 신호 손은 언어 서버(lsp/client.js)에만 있었고, 그 손은 **제 아이만** 거둔 뒤 같은 신호를
|
|
10
|
+
* 다시 보내 죽었다. 그래서 `kill <pid>` 한 번에 언어 서버는 거둬지고 MCP 서버와 일감
|
|
11
|
+
* (`npm run dev` 같은 것)은 남았다 — 입력이 닫히면 스스로 끝나는 서버만 운 좋게 사라졌다.
|
|
12
|
+
* 저마다 손을 따로 달면 더 나쁘다. 손이 둘이면 서로 「남이 맡고 있다」 고 보고 아무도 안
|
|
13
|
+
* 죽어서, deel 이 SIGTERM 을 **삼키는** 프로그램이 된다(아래 다시보낼까).
|
|
14
|
+
*
|
|
15
|
+
* 그래서 손은 여기 하나만 달고, 거둘 것은 각자 `신호에거두기()` 로 적는다.
|
|
16
|
+
*
|
|
17
|
+
* ── 치우고 나면 **원래대로 죽어야 한다** ────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* Node 는 SIGINT·SIGTERM 에 손이 하나라도 달려 있으면 **기본 동작(끝내기)을 안 한다.**
|
|
20
|
+
* 치우기만 하고 말면 `kill <pid>` 에 아이들은 죽고 deel 은 그대로 산다 — 잡 관리자·컨테이너
|
|
21
|
+
* 종료·systemd 가 전부 이 신호로 정리하는데, 그것들이 하나같이 시간을 다 기다린 뒤 SIGKILL 로
|
|
22
|
+
* 때려잡게 된다. 그래서 치운 다음 우리 손을 떼고 같은 신호를 자신에게 다시 보낸다. 그러면
|
|
23
|
+
* 기본 동작이 돌아 128+n 로 끝난다 — 셸이 기대하는 그 값이다.
|
|
24
|
+
*
|
|
25
|
+
* 다만 **남이 그 신호를 맡고 있으면 안 건드린다.** 한 방 실행은 SIGINT 로 턴만 끊고 살아
|
|
26
|
+
* 있어야 하고(oneshot.js), 대화 화면의 Ctrl+C 는 readline 이 따로 받는다. 우리 것 말고 다른
|
|
27
|
+
* 손이 달려 있으면 그쪽 뜻이 먼저다 — 남이 맡은 신호로 결국 끝나면 그때는 'exit' 그물이 거둔다.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** 신호로 끝날 때 부를 것들. 저마다 제 'exit' 그물과 같은 함수를 적는다. */
|
|
31
|
+
const 거둘것 = new Set();
|
|
32
|
+
|
|
33
|
+
/** 신호로 끝날 때 이것도 부른다. 같은 함수를 두 번 적어도 한 번만 돈다. */
|
|
34
|
+
export function 신호에거두기(손) { 거둘것.add(손); return 손; }
|
|
35
|
+
|
|
36
|
+
/** 검사가 본다 — 이 손이 적혀 있나. */
|
|
37
|
+
export function 거둘것에있나(손) { return 거둘것.has(손); }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 치운 다음 그 신호로 진짜 죽어야 하나.
|
|
41
|
+
*
|
|
42
|
+
* 갈림을 함수로 빼 둔 까닭: 윈도우에서는 이 길을 통째로 밟아 볼 수가 없다
|
|
43
|
+
* (SIGTERM 을 보내면 손이 돌기도 전에 Node 가 죽인다). 그런데 갈림 자체가
|
|
44
|
+
* 틀리면 유닉스에서 한 방 실행의 Ctrl+C 가 턴만 끊는 대신 프로그램을 죽인다 —
|
|
45
|
+
* 어느 판에서든 재어 볼 수 있어야 하는 자리다.
|
|
46
|
+
*
|
|
47
|
+
* @param 셈 이 신호에 달린 손의 수. 우리 것 하나만이면 우리가 마지막이다.
|
|
48
|
+
*/
|
|
49
|
+
export function 다시보낼까(신호, 셈 = process.listenerCount(신호)) {
|
|
50
|
+
return Number(셈) <= 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 적힌 것을 다 부른다. 하나가 던져도 나머지는 부른다 — 끝나는 길이다. */
|
|
54
|
+
function 모두거두기() {
|
|
55
|
+
for (const 손 of 거둘것) { try { 손(); } catch { /* 끝나는 중이라 할 수 있는 게 없다 */ } }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const 신호 of ['SIGINT', 'SIGTERM']) {
|
|
59
|
+
const 손 = () => {
|
|
60
|
+
/*
|
|
61
|
+
* 갈림을 **거두기보다 먼저** 본다. 거두고 나서 갈랐었다 — 그래서 대화 화면의 Ctrl+C 가
|
|
62
|
+
* 턴만 끊고 세션은 사는데 언어 서버는 전부 죽고 풀도 비었다(2.0.0 6회차 사냥).
|
|
63
|
+
*/
|
|
64
|
+
if (!다시보낼까(신호)) return; // 남이 맡고 있다 — 그쪽 뜻이 먼저다
|
|
65
|
+
모두거두기();
|
|
66
|
+
process.removeListener(신호, 손);
|
|
67
|
+
try { process.kill(process.pid, 신호); } catch { process.exit(신호 === 'SIGINT' ? 130 : 143); }
|
|
68
|
+
};
|
|
69
|
+
process.on(신호, 손);
|
|
70
|
+
}
|
package/src/report.js
CHANGED
package/src/safety/audit.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// 감사 로그. 무엇을 언제 어떻게 했는지 전부 남긴다.
|
|
2
2
|
// 자율 실행을 사내에 설득할 때 이 파일이 근거가 된다.
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { appendFileSync, mkdirSync, existsSync, readFileSync
|
|
4
|
+
import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { 줄로끝나나, 본인만잠그기 } from '../jsonl.js';
|
|
5
6
|
import { 가리기 } from './secrets.js';
|
|
6
7
|
import { 첫이름 } from '../tools/label.js';
|
|
7
8
|
|
|
@@ -126,25 +127,13 @@ export class Audit {
|
|
|
126
127
|
* 개행이 아니면 개행부터 붙인다. 줄마다 재지 않는다 — 그 뒤로는 우리가 적은 줄이다.
|
|
127
128
|
*/
|
|
128
129
|
#줄끝봤나 = false;
|
|
129
|
-
#줄로끝나나() {
|
|
130
|
-
let fd = null;
|
|
131
|
-
try {
|
|
132
|
-
const 크기 = statSync(this.file).size;
|
|
133
|
-
if (!크기) return true;
|
|
134
|
-
fd = openSync(this.file, 'r');
|
|
135
|
-
const 한바이트 = Buffer.alloc(1);
|
|
136
|
-
readSync(fd, 한바이트, 0, 1, 크기 - 1);
|
|
137
|
-
return 한바이트[0] === 0x0a;
|
|
138
|
-
} catch { return true; } // 없거나 못 읽으면 붙일 반쪽도 없다
|
|
139
|
-
finally { if (fd != null) { try { closeSync(fd); } catch { /* 닫다 터져도 적기는 한다 */ } } }
|
|
140
|
-
}
|
|
141
130
|
|
|
142
131
|
write(kind, data) {
|
|
143
132
|
const rec = { at: new Date().toISOString(), session: this.session, kind, ...data };
|
|
144
133
|
try {
|
|
145
134
|
let 줄 = JSON.stringify(rec) + '\n';
|
|
146
135
|
if (!this.#줄끝봤나) {
|
|
147
|
-
if (
|
|
136
|
+
if (!줄로끝나나(this.file)) 줄 = '\n' + 줄;
|
|
148
137
|
this.#줄끝봤나 = true;
|
|
149
138
|
}
|
|
150
139
|
appendFileSync(this.file, 줄, 'utf8');
|
|
@@ -180,11 +169,7 @@ export class Audit {
|
|
|
180
169
|
#잠그기() {
|
|
181
170
|
if (this.#잠갔나) return;
|
|
182
171
|
this.#잠갔나 = true;
|
|
183
|
-
|
|
184
|
-
// 「잠근 척」 이다 — agent/store.js 와 같이 안 걸었다고 적는다.
|
|
185
|
-
if (process.platform === 'win32') { this.잠금 = { 못함: 'windows' }; return; }
|
|
186
|
-
try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
|
|
187
|
-
catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
|
|
172
|
+
this.잠금 = 본인만잠그기(this.file);
|
|
188
173
|
}
|
|
189
174
|
|
|
190
175
|
/** 기록이 새고 있나. 새고 있으면 `{수, 까닭}`, 멀쩡하면 null. */
|
package/src/safety/authcmd.js
CHANGED
|
@@ -49,7 +49,7 @@ import { isOffline } from './network.js';
|
|
|
49
49
|
import { 가리기 } from './secrets.js';
|
|
50
50
|
|
|
51
51
|
/** 기다려 주는 시간. 사내 로그인은 브라우저를 열고 사람을 기다린다. */
|
|
52
|
-
|
|
52
|
+
const 기본기다림 = 180000;
|
|
53
53
|
|
|
54
54
|
/** 수명을 안 알려 줄 때 몇 초짜리로 볼까. 짧게 잡는다 — 늦게 아는 것보다 낫다. */
|
|
55
55
|
export const 기본수명 = 3300;
|
package/src/safety/guard.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// 자율 실행의 울타리.
|
|
2
2
|
// 승인 프롬프트를 안 쓰는 대신 (1) 작업 범위 밖은 못 건드리고
|
|
3
3
|
// (2) 되돌릴 수 없는 명령만 막는다. 나머지는 전부 통과시킨다.
|
|
4
|
-
import { resolve, relative, isAbsolute, sep, dirname, basename } from 'node:path';
|
|
5
|
-
import { realpathSync, existsSync } from 'node:fs';
|
|
6
|
-
import { homedir } from 'node:os';
|
|
4
|
+
import { resolve, relative, isAbsolute, sep, dirname, basename, win32, posix } from 'node:path';
|
|
5
|
+
import { realpathSync, existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { homedir, userInfo } from 'node:os';
|
|
7
7
|
import { 내부살림 } from '../tools/fsutil.js';
|
|
8
8
|
|
|
9
9
|
export class ScopeError extends Error {}
|
|
@@ -110,8 +110,28 @@ export function makeScope(root) {
|
|
|
110
110
|
// Git Bash 꼴 절대 경로(/c/Users/…)는 윈도우에서 C:\Users\… 다. 안 풀면 지금
|
|
111
111
|
// 드라이브 밑의 \c\Users\… 로 읽혀, 안에 있는 파일이 '범위 밖' 으로 막힌다.
|
|
112
112
|
const 경로 = MSYS풀기(p);
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
let abs = isAbsolute(경로) ? resolve(경로) : resolve(base, 경로);
|
|
114
|
+
/*
|
|
115
|
+
* ── 글자로는 밖인데 **같은 자리를 다르게 적은 것** (2.0.2 · A5) ──────────────
|
|
116
|
+
*
|
|
117
|
+
* 윈도우는 한 폴더를 두 이름으로 적는다 — `C:\Users\RUNNER~1\…`(8.3) 와
|
|
118
|
+
* `C:\Users\runneradmin\…`. 뿌리를 짧은 이름으로 들고 있는데 모델이 긴 이름
|
|
119
|
+
* 절대경로를 주면(셸의 pwd · 오류 글 · 딴 도구의 답에서 베낀 것), 여기가 글자로만
|
|
120
|
+
* 견줘 **같은 폴더의 파일을 「작업 범위 밖」 으로** 막았다. 재 봤다 —
|
|
121
|
+
* `DEEL_A~1` 뿌리에서 `deel_a5_longfoldername_…\b.txt` 쓰기가 막혔다.
|
|
122
|
+
*
|
|
123
|
+
* 그래서 글자로 밖이면 한 번 더 **실제 자리끼리** 견준다. 실제로도 밖이면 그대로
|
|
124
|
+
* 막는다. 실제로는 안이면 받아 들이되, 돌려주는 경로는 **뿌리가 적힌 꼴로** 다시
|
|
125
|
+
* 붙인다 — 되돌리기 기록(rel)·화면 경로(show)·커밋이 다 뿌리의 꼴로 견주기 때문이다.
|
|
126
|
+
* 긴 이름 그대로 넘기면 여기만 지나고 아래에서 같은 병이 다시 난다.
|
|
127
|
+
*/
|
|
128
|
+
if (밖(base, abs)) {
|
|
129
|
+
const 실제 = 진짜자리(abs);
|
|
130
|
+
const 뿌리실제 = 진짜뿌리();
|
|
131
|
+
if (밖(뿌리실제, 실제)) throw new ScopeError(`작업 범위 밖입니다: ${p}\n 범위: ${base}`);
|
|
132
|
+
const 안쪽 = relative(같은꼴(뿌리실제), 같은꼴(실제));
|
|
133
|
+
abs = 안쪽 ? resolve(base, 안쪽) : base;
|
|
134
|
+
}
|
|
115
135
|
// 글자로는 안인데 링크를 따라가면 밖인 자리. 정션 하나로 울타리가 열린다.
|
|
116
136
|
//
|
|
117
137
|
// 여기 견주기도 한 모양으로 맞춘다. 맥에서 realpathSync 는 디스크에 있는
|
|
@@ -560,6 +580,36 @@ export function checkPaths(cmd, scope) {
|
|
|
560
580
|
return true;
|
|
561
581
|
}
|
|
562
582
|
|
|
583
|
+
/*
|
|
584
|
+
* ── `~이름` 이 가리키는 집 (2.0.2 · 388) ────────────────────────────────────
|
|
585
|
+
*
|
|
586
|
+
* 「내 집 옆자리」 로만 풀었다. 그런데 root 로 돌면 내 집이 `/root` 라 옆자리가 **뿌리**다 —
|
|
587
|
+
* `~admin` 이 `/admin` 이 됐다. 셸은 passwd 의 `/home/admin` 으로 가는데. 작업 폴더가 마침
|
|
588
|
+
* `/admin` 이면 남의 집을 「범위 안」 으로 읽는다.
|
|
589
|
+
*
|
|
590
|
+
* 유닉스에서는 passwd 에 적힌 집을 먼저 본다(셸이 보는 그 자리). 없으면(맥의 디렉터리 서비스 ·
|
|
591
|
+
* 윈도) 예전처럼 내 집 옆자리로 풀되, 그 옆자리가 뿌리면 관례 자리(`/home`)로 간다. 내 이름이면
|
|
592
|
+
* 내 집이다.
|
|
593
|
+
*/
|
|
594
|
+
export function 남의집(이름, { 집 = homedir(), 나 = null, passwd = null, platform = process.platform } = {}) {
|
|
595
|
+
let 내이름 = 나;
|
|
596
|
+
if (내이름 == null) { try { 내이름 = userInfo().username; } catch { 내이름 = ''; } }
|
|
597
|
+
if (이름 === 내이름) return 집;
|
|
598
|
+
if (platform !== 'win32') {
|
|
599
|
+
let 글 = passwd;
|
|
600
|
+
if (글 == null) { try { 글 = readFileSync('/etc/passwd', 'utf8'); } catch { 글 = ''; } }
|
|
601
|
+
for (const 줄 of String(글).split('\n')) {
|
|
602
|
+
const 칸 = 줄.split(':');
|
|
603
|
+
if (칸[0] === 이름 && 칸[5]) return 칸[5];
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// 판을 넘겨받았으면 경로 셈도 그 판으로 한다. 리눅스에서 윈도 판을 물으면 `C:\Users\me` 가
|
|
607
|
+
// 상대경로로 읽혀 작업 폴더 밑이 됐다(검사만 그 판을 만든다 — 실제로는 늘 이 PC 의 판이다).
|
|
608
|
+
const 길 = platform === 'win32' ? win32 : posix;
|
|
609
|
+
const 옆 = 길.dirname(집);
|
|
610
|
+
return 길.resolve(옆 === 길.parse(옆).root ? 길.join(옆, 'home') : 옆, 이름);
|
|
611
|
+
}
|
|
612
|
+
|
|
563
613
|
// 셸이 알아서 풀어 주는 자리표. 우리도 같이 풀어야 실제로 닿는 곳을 본다.
|
|
564
614
|
function 자리표풀기(s) {
|
|
565
615
|
return s
|
|
@@ -582,7 +632,7 @@ function 자리표풀기(s) {
|
|
|
582
632
|
* 남의 집은 어느 쪽으로 풀든 내 작업 폴더 밖이다. 이름은 글자·밑줄로 시작할 때만 받는다
|
|
583
633
|
* (`~1` · `~+` 은 셸의 다른 뜻). 가운데 물결(`./~backup` · `PROGRA~1`)은 `^` 에 안 닿는다.
|
|
584
634
|
*/
|
|
585
|
-
.replace(/^~([A-Za-z_][\w.-]*)(?=[/\\]|$)/, (전체, 이름) =>
|
|
635
|
+
.replace(/^~([A-Za-z_][\w.-]*)(?=[/\\]|$)/, (전체, 이름) => 남의집(이름))
|
|
586
636
|
/*
|
|
587
637
|
* 파워셸은 `$env:이름` 으로 적는다.
|
|
588
638
|
*
|
package/src/safety/hooks.js
CHANGED
package/src/safety/keystore.js
CHANGED
|
@@ -222,7 +222,7 @@ export function 잠그기(글, { 프로필id = null } = {}) {
|
|
|
222
222
|
|
|
223
223
|
if (process.platform === 'win32') {
|
|
224
224
|
const r = 파워셸실행(잠그는스크립트, Buffer.from(값, 'utf8').toString('base64'));
|
|
225
|
-
if (r.ok && r.out) return `dpapi:${r.out}
|
|
225
|
+
if (r.ok && r.out) return 됐다(`dpapi:${r.out}`);
|
|
226
226
|
못잠근까닭 = r.err || '파워셸이 잠그기를 못 마쳤습니다';
|
|
227
227
|
해보고졌나 = true;
|
|
228
228
|
return null;
|
|
@@ -230,7 +230,7 @@ export function 잠그기(글, { 프로필id = null } = {}) {
|
|
|
230
230
|
if (process.platform === 'darwin') {
|
|
231
231
|
const 이름 = 키체인이름(프로필id);
|
|
232
232
|
const r = 키체인넣기(값, 이름);
|
|
233
|
-
if (r.ok) return `keychain:${이름}
|
|
233
|
+
if (r.ok) return 됐다(`keychain:${이름}`);
|
|
234
234
|
못잠근까닭 = r.err || 'security 가 열쇠를 못 넣었습니다';
|
|
235
235
|
해보고졌나 = true;
|
|
236
236
|
return null;
|
|
@@ -282,7 +282,22 @@ let 못잠근까닭 = null;
|
|
|
282
282
|
*/
|
|
283
283
|
let 해보고졌나 = false;
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
/*
|
|
286
|
+
* ── 되고 나면 옛 실패를 **내려놓는다** (2.0.2 · 530) ──────────────────────
|
|
287
|
+
*
|
|
288
|
+
* 잠그기가 한 번 지면 그 까닭이 프로세스 내내 남았다. 뒤에 같은 판에서 잠그기가 되어도
|
|
289
|
+
* (파워셸이 잠깐 막혔다 풀림 · 키체인이 잠겼다 열림) 보관방식() 은 평문 값을 보면 여전히
|
|
290
|
+
* 「잠그려다 실패했습니다」 라고 적었다 — 지금은 되는 장치를 안 된다고 말하는 것이다.
|
|
291
|
+
* 되면 지운다. config.js 가 「몇 개를 왜 못 잠갔나」 에 쓰는 까닭은 그쪽이 **진 순간에**
|
|
292
|
+
* 받아 둔다(잠금옮기기).
|
|
293
|
+
*/
|
|
294
|
+
function 됐다(값) {
|
|
295
|
+
못잠근까닭 = null;
|
|
296
|
+
해보고졌나 = false;
|
|
297
|
+
return 값;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 마지막으로 잠그다 실패한 까닭. 한 번도 실패한 적이 없으면(또는 그 뒤로 됐으면) null. */
|
|
286
301
|
export function 잠그다실패한까닭() { return 못잠근까닭; }
|
|
287
302
|
|
|
288
303
|
/** 사람이 스스로 껐나. 껐으면 이 판에서는 **아무것도 안 해 본다.** */
|
package/src/safety/shellenv.js
CHANGED
|
@@ -116,7 +116,7 @@ function 이름마디들(이름) {
|
|
|
116
116
|
const 자격든주소 = /^[a-z][a-z0-9+.-]*:\/\/[^\s/?#@:]*:[^\s/?#@]+@/i;
|
|
117
117
|
|
|
118
118
|
/** 이 값이 자격이 든 주소인가. 이름으로는 못 거르는 DATABASE_URL 같은 것을 거른다. */
|
|
119
|
-
|
|
119
|
+
function 비밀값인가(값) {
|
|
120
120
|
return typeof 값 === 'string' && 자격든주소.test(값.trim());
|
|
121
121
|
}
|
|
122
122
|
|
package/src/safety/undo.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// 되돌리기. 승인 프롬프트를 안 쓰는 대신 이게 안전망이다.
|
|
2
2
|
// 파일을 고치기 전에 항상 이전 내용을 떠 놓고, /undo 로 턴 단위로 되돌린다.
|
|
3
3
|
import { join, dirname, basename, resolve, relative, isAbsolute, sep } from 'node:path';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, statSync,
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, appendFileSync, statSync, renameSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { 줄로끝나나, 본인만잠그기 } from '../jsonl.js';
|
|
5
6
|
import { looksBinary } from '../tools/encoding.js';
|
|
6
7
|
import { 살림폴더만들기 } from './audit.js';
|
|
7
8
|
import { 말 } from '../i18n/index.js';
|
|
@@ -86,6 +87,19 @@ export class History {
|
|
|
86
87
|
this.잠금 = null;
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/** 이 경로의 조상 가운데 **아직 없는** 폴더들(뿌리 기준 상대 경로, 깊은 것부터). 뿌리 밖으로는 안 올라간다. */
|
|
91
|
+
#없는폴더들(abs) {
|
|
92
|
+
const 들 = [];
|
|
93
|
+
let d = dirname(resolve(String(abs)));
|
|
94
|
+
for (let i = 0; i < 64 && !existsSync(d); i++) {
|
|
95
|
+
const 상대 = this.#상대(d);
|
|
96
|
+
if (상대 === null) break;
|
|
97
|
+
들.push(상대);
|
|
98
|
+
d = dirname(d);
|
|
99
|
+
}
|
|
100
|
+
return 들;
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
/** 새 기록에 적을 상대 경로. 뿌리 밖이면 null — 그런 기록은 되돌릴 때 안 받는다. */
|
|
90
104
|
#상대(abs) {
|
|
91
105
|
const a = resolve(String(abs));
|
|
@@ -120,23 +134,11 @@ export class History {
|
|
|
120
134
|
* 개행으로 끝내므로 한 번 본 것이 계속 맞다.
|
|
121
135
|
*/
|
|
122
136
|
#줄끝봤나 = false;
|
|
123
|
-
#줄로끝나나() {
|
|
124
|
-
let fd = null;
|
|
125
|
-
try {
|
|
126
|
-
const 크기 = statSync(this.file).size;
|
|
127
|
-
if (!크기) return true;
|
|
128
|
-
fd = openSync(this.file, 'r');
|
|
129
|
-
const 한바이트 = Buffer.alloc(1);
|
|
130
|
-
readSync(fd, 한바이트, 0, 1, 크기 - 1);
|
|
131
|
-
return 한바이트[0] === 0x0a;
|
|
132
|
-
} catch { return true; } // 없거나 못 읽으면 붙일 반쪽도 없다
|
|
133
|
-
finally { if (fd != null) { try { closeSync(fd); } catch { /* 닫다 터져도 적기는 한다 */ } } }
|
|
134
|
-
}
|
|
135
137
|
|
|
136
138
|
#덧붙이기(rec) {
|
|
137
139
|
let 줄 = 적을줄(rec) + '\n';
|
|
138
140
|
if (!this.#줄끝봤나) {
|
|
139
|
-
if (
|
|
141
|
+
if (!줄로끝나나(this.file)) 줄 = '\n' + 줄;
|
|
140
142
|
this.#줄끝봤나 = true;
|
|
141
143
|
}
|
|
142
144
|
appendFileSync(this.file, 줄, 'utf8');
|
|
@@ -166,11 +168,7 @@ export class History {
|
|
|
166
168
|
#잠그기() {
|
|
167
169
|
if (this.#잠갔나) return;
|
|
168
170
|
this.#잠갔나 = true;
|
|
169
|
-
|
|
170
|
-
// 「잠근 척」 이다 — agent/store.js 와 같이 안 걸었다고 적는다.
|
|
171
|
-
if (process.platform === 'win32') { this.잠금 = { 못함: 'windows' }; return; }
|
|
172
|
-
try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
|
|
173
|
-
catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
|
|
171
|
+
this.잠금 = 본인만잠그기(this.file);
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
/**
|
|
@@ -236,6 +234,22 @@ export class History {
|
|
|
236
234
|
* 그림·문서가 /undo 뒤에도 남았다. 이 표가 있으면 정말 없던 자리라 바이너리여도 지운다.
|
|
237
235
|
*/
|
|
238
236
|
if (없던) rec.없던 = true;
|
|
237
|
+
/*
|
|
238
|
+
* ── 이 파일과 **같이 생길 폴더**도 적는다 (2.0.2 · B3) ─────────────────────
|
|
239
|
+
*
|
|
240
|
+
* 폴더를 통째로 옮긴 판(`dirA` → `dirB`)을 되돌리면 파일은 `dirA` 로 돌아오고
|
|
241
|
+
* `dirB/f.txt` 는 「원래 없던 파일」 로 지워지는데, **빈 `dirB` 가 남았다.** 되돌린
|
|
242
|
+
* 뒤의 폴더가 되돌리기 전과 다르다. 그 폴더가 이번에 생겼다는 것을 아무 데도
|
|
243
|
+
* 안 적어서 지울 근거가 없었다.
|
|
244
|
+
*
|
|
245
|
+
* 원래 있던 빈 폴더에 파일을 만든 판과 가르려면 **뜰 때** 봐야 한다 — 되돌릴 때는
|
|
246
|
+
* 둘 다 「빈 폴더」 라 구별이 안 된다. 그래서 여기서 아직 없는 조상 폴더를 깊은
|
|
247
|
+
* 것부터 적어 두고, 되돌릴 때 그 폴더가 **비었을 때만** 지운다.
|
|
248
|
+
*/
|
|
249
|
+
if (없던) {
|
|
250
|
+
const 새폴더 = this.#없는폴더들(absPath);
|
|
251
|
+
if (새폴더.length) rec.새폴더 = 새폴더;
|
|
252
|
+
}
|
|
239
253
|
// 작업 폴더 기준 자리. 폴더를 옮기거나 복사해도 이 이력은 **그 폴더**를 되돌린다 (위 머리말).
|
|
240
254
|
const 상대 = this.#상대(absPath);
|
|
241
255
|
if (상대 !== null) rec.rel = 상대;
|
|
@@ -618,6 +632,16 @@ export class History {
|
|
|
618
632
|
}
|
|
619
633
|
rmSync(path, { force: true });
|
|
620
634
|
restored.push({ path, how: 말('undo.wayDeleted'), ok: true });
|
|
635
|
+
// 그 파일과 같이 생긴 폴더가 이제 비었으면 거둔다 (snapshot 의 새폴더 머리말). rmdirSync 는 **빈
|
|
636
|
+
// 폴더만** 지운다 — 그 뒤에 누가 넣은 것이 있으면 ENOTEMPTY 로 멈추고, 폴더가 아니면 ENOTDIR 다.
|
|
637
|
+
// 이력은 저장소에 딸려 올 수 있으니(파일 머리말) 폴더 자리도 밖·링크 너머면 안 건드린다.
|
|
638
|
+
// 파일이 아니라서 되돌린 수에는 안 센다.
|
|
639
|
+
for (const 상대 of rec.새폴더 ?? []) {
|
|
640
|
+
const d = this.#자리풀기(null, 상대);
|
|
641
|
+
if (!d || 링크밖(d)) break;
|
|
642
|
+
try { rmdirSync(d); } catch { break; }
|
|
643
|
+
restored.push({ path: d, how: '빈 폴더 지움 (이번에 새로 생긴 폴더)', ok: true, 폴더: true });
|
|
644
|
+
}
|
|
621
645
|
} else {
|
|
622
646
|
/*
|
|
623
647
|
* 담고 있던 폴더가 없어졌을 수 있다 — Move 로 폴더째 옮긴 경우다.
|
|
@@ -710,7 +734,7 @@ export class History {
|
|
|
710
734
|
// 이력에서 못 읽은 줄 수. 그만큼은 되돌릴 길이 애초에 없었다.
|
|
711
735
|
깨진줄: this.깨진줄,
|
|
712
736
|
// 화면·감사기록이 쓰는 수. **진짜로 되돌아간 것만** 센다.
|
|
713
|
-
되돌린수: restored.filter((x) => x.ok === true).length,
|
|
737
|
+
되돌린수: restored.filter((x) => x.ok === true && !x.폴더).length,
|
|
714
738
|
못한것: restored.filter((x) => x.ok === false),
|
|
715
739
|
turns: turns.length,
|
|
716
740
|
turnIds: turns.slice(),
|
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/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';
|