deel-local-cli 2.0.0 → 2.0.1
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 +5 -4
- package/README.md +5 -4
- package/package.json +1 -1
- package/src/commands/common.js +2 -2
- package/src/commands/extend.js +0 -1
- package/src/commands/model.js +1 -2
- package/src/config.js +2 -2
- package/src/configexplain.js +2 -1
- package/src/doctor.js +1 -2
- package/src/safety/guard.js +25 -1
- package/src/tools/doc2md.js +25 -3
- package/src/tools/fastgrep.js +64 -9
- package/src/tools/index.js +56 -5
- package/src/tools/jobs.js +4 -1
- package/src/tools/pdf.js +1 -3
- package/src/tools/verify.js +17 -3
- package/src/tools/webfetch.js +11 -1
package/README.ko.md
CHANGED
|
@@ -316,7 +316,7 @@ deel --offline
|
|
|
316
316
|
무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
|
|
317
317
|
|
|
318
318
|
```
|
|
319
|
-
deel 2.0.
|
|
319
|
+
deel 2.0.1 ⌂ 이 안
|
|
320
320
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
321
321
|
```
|
|
322
322
|
|
|
@@ -1240,7 +1240,7 @@ deel stats # 이 폴더에서 실제로 무엇을 했나 (.de
|
|
|
1240
1240
|
## 개발
|
|
1241
1241
|
|
|
1242
1242
|
```bash
|
|
1243
|
-
npm test 전체 검증 (13,
|
|
1243
|
+
npm test 전체 검증 (13,941항목 — 몇몇은 터미널에 따라 갈립니다)
|
|
1244
1244
|
npm run coverage 검사가 소스의 어디를 밟았는지
|
|
1245
1245
|
npm run verify 반입·통신 검증만
|
|
1246
1246
|
npm run bench 편집 성공률 측정
|
|
@@ -1297,7 +1297,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1297
1297
|
| `exitcode` · `doorparity` | 7 · 19 | 화면에 적은 종료코드 표가 진짜인가 · 문 **네 개**가 같은 것을 주는가 |
|
|
1298
1298
|
| `no-bundle` | 28 | 배포 묶음에 남의 것이 안 섞였는가 · 검사 파일 위생 |
|
|
1299
1299
|
| `edit-bench` | 15건 | 편집 성공률 |
|
|
1300
|
-
| `mutate` | 어긋 1,
|
|
1300
|
+
| `mutate` | 어긋 1,662개 | **검사가 정말 지키는가** — 지켜야 할 줄을 일부러 어긋내고 빨개지는지 본다 |
|
|
1301
1301
|
|
|
1302
1302
|
> **자세히** — 어디를 밟았는지 · 폴더 구조
|
|
1303
1303
|
>
|
|
@@ -1309,7 +1309,8 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1309
1309
|
|
|
1310
1310
|
| 판 | 무엇이 바뀌었나 |
|
|
1311
1311
|
|---|---|
|
|
1312
|
-
| **[2.0.
|
|
1312
|
+
| **[2.0.1](docs/ko/releases/2.0.md#201)** | 울타리 하나가 안 걸렸습니다 — `sudo -u root bash` 가 `curl … \| bash` 막이를 지나갔습니다 — 그리고 관문 넷이 빨개질 수 없었습니다 |
|
|
1313
|
+
| [2.0.0](docs/ko/releases/2.0.md#200) | 있다고 적힌 울타리마다 실제로 걸리는지 다시 봤습니다 — 바뀐 것 전부·어디를 어떻게 고쳤나·어떻게 찾았나(CHA) |
|
|
1313
1314
|
|
|
1314
1315
|
<p align="center">
|
|
1315
1316
|
<picture>
|
package/README.md
CHANGED
|
@@ -389,7 +389,7 @@ deel --offline
|
|
|
389
389
|
The destination is printed at the top of every session:
|
|
390
390
|
|
|
391
391
|
```
|
|
392
|
-
deel 2.0.
|
|
392
|
+
deel 2.0.1 ⌂ inside
|
|
393
393
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
394
394
|
```
|
|
395
395
|
|
|
@@ -1334,7 +1334,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
|
|
|
1334
1334
|
## Development
|
|
1335
1335
|
|
|
1336
1336
|
```bash
|
|
1337
|
-
npm test Full suite (13,
|
|
1337
|
+
npm test Full suite (13,941 checks; a few are TTY-dependent)
|
|
1338
1338
|
npm run coverage Which lines the tests actually execute
|
|
1339
1339
|
npm run verify Import + network checks only
|
|
1340
1340
|
npm run bench Edit success rate
|
|
@@ -1378,7 +1378,7 @@ so one run tells you everything.
|
|
|
1378
1378
|
| `exitcode` · `doorparity` | 7 · 19 | The printed exit-code table is real · all **four** doors hand out the same thing |
|
|
1379
1379
|
| `no-bundle` | 28 | Nothing foreign in the published package; test-file hygiene |
|
|
1380
1380
|
| `edit-bench` | 15 cases | Edit success rate |
|
|
1381
|
-
| `mutate` | 1,
|
|
1381
|
+
| `mutate` | 1,662 mutants | **Whether the tests actually guard** — break the line on purpose, check it turns red |
|
|
1382
1382
|
|
|
1383
1383
|
> **More** — Coverage · Layout
|
|
1384
1384
|
>
|
|
@@ -1390,7 +1390,8 @@ so one run tells you everything.
|
|
|
1390
1390
|
|
|
1391
1391
|
| Version | What changed |
|
|
1392
1392
|
|---|---|
|
|
1393
|
-
| **[2.0.
|
|
1393
|
+
| **[2.0.1](docs/en/releases/2.0.md#201)** | One fence in 2.0.0 did not hold — `sudo -u root bash` walked through the `curl … \| bash` guard — and four gates could not go red |
|
|
1394
|
+
| [2.0.0](docs/en/releases/2.0.md#200) | Every fence we said was there, checked for whether it actually holds — everything that changed, where and how it was fixed, and how it was found (CHA) |
|
|
1394
1395
|
|
|
1395
1396
|
<p align="center">
|
|
1396
1397
|
<picture>
|
package/package.json
CHANGED
package/src/commands/common.js
CHANGED
|
@@ -28,8 +28,8 @@ export const COMMANDS = Object.fromEntries(Object.entries(명령들).map(([이
|
|
|
28
28
|
*
|
|
29
29
|
* @returns {boolean} 남겼나
|
|
30
30
|
*/
|
|
31
|
-
export function 설정남기기(cfg
|
|
32
|
-
const r = 저장시도(cfg
|
|
31
|
+
export function 설정남기기(cfg) {
|
|
32
|
+
const r = 저장시도(cfg);
|
|
33
33
|
if (!r.ok) say(` ${mark.warn} ${c.yellow(말('common.cfgSaveFailed', { 왜: clip(r.왜, 70) }))}`);
|
|
34
34
|
return r.ok;
|
|
35
35
|
}
|
package/src/commands/extend.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { existsSync, statSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { c, say, rule, pad, mark, clip } from '../ui/ansi.js';
|
|
6
|
-
import { 말 } from '../i18n/index.js';
|
|
7
6
|
import { discover } from '../skills/discover.js';
|
|
8
7
|
import { install, list, remove, pack } from '../plugins/manage.js';
|
|
9
8
|
import { spin } from '../ui/spinner.js';
|
package/src/commands/model.js
CHANGED
|
@@ -9,7 +9,6 @@ import { load, resolveKey, upsert } from '../config.js';
|
|
|
9
9
|
import { 받기설정, 잊기 as 받은열쇠잊기 } from '../safety/authcmd.js';
|
|
10
10
|
// 열쇠받기 명령을 정책이 못박아 뒀을 수 있다 — 받기설정 이 그 값을 같이 본다.
|
|
11
11
|
import { 정책읽기 } from '../safety/policy.js';
|
|
12
|
-
import { 말 } from '../i18n/index.js';
|
|
13
12
|
import { 더할머리, 규격이름 } from '../backend/adapter.js';
|
|
14
13
|
import { 규격맞추기, 규격갈래 } from '../agent/session.js';
|
|
15
14
|
import { 잠잠기본, 무소식기본 } from '../backend/http.js';
|
|
@@ -43,7 +42,7 @@ export async function 출력상한(session, arg = '') {
|
|
|
43
42
|
|
|
44
43
|
if (말 === 'auto' || 말 === '자동') {
|
|
45
44
|
session.conn.maxTokens = null;
|
|
46
|
-
|
|
45
|
+
if (prof) { delete prof.maxTokens; 설정남기기(cfg); }
|
|
47
46
|
say(` ${mark.ok} 직접 정한 값을 지웠습니다. ${c.gray('이번 대화에 바로 먹습니다.')}`);
|
|
48
47
|
/*
|
|
49
48
|
* 프로필을 못 찾으면 다음에 켤 때 그 값이 되살아난다. 숨기면 안 된다.
|
package/src/config.js
CHANGED
|
@@ -637,8 +637,8 @@ export function save(cfg) {
|
|
|
637
637
|
*
|
|
638
638
|
* @returns {{ok: true, 자리: string} | {ok: false, 왜: string}}
|
|
639
639
|
*/
|
|
640
|
-
export function 저장시도(cfg
|
|
641
|
-
try { return { ok: true, 자리: save(cfg
|
|
640
|
+
export function 저장시도(cfg) {
|
|
641
|
+
try { return { ok: true, 자리: save(cfg) }; }
|
|
642
642
|
catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
|
|
643
643
|
}
|
|
644
644
|
|
package/src/configexplain.js
CHANGED
|
@@ -123,7 +123,8 @@ export function 설명(칸, { root = process.cwd(), env = process.env } = {}) {
|
|
|
123
123
|
}
|
|
124
124
|
// 물려받은 이름(`constructor` · `toString`)으로 파고들면 **없는 칸이
|
|
125
125
|
// 있는 것이 된다.** 여기서 막아야 아래 세 층이 다 안전하다.
|
|
126
|
-
|
|
126
|
+
// 위 루프 머리에서 여기가 object 임을 이미 봤다 — 또 보지 않는다.
|
|
127
|
+
여기 = Object.hasOwn(여기, 이름) ? 여기[이름] : undefined;
|
|
127
128
|
}
|
|
128
129
|
return 여기;
|
|
129
130
|
};
|
package/src/doctor.js
CHANGED
|
@@ -69,13 +69,12 @@ function 주소살펴보기(주소) {
|
|
|
69
69
|
* 이 자리의 조건을 다 본다. 화면 그리기는 안 한다 — 부르는 쪽이 그린다.
|
|
70
70
|
*
|
|
71
71
|
* @param {object} o
|
|
72
|
-
* @param {object|null} o.cfg 읽은 설정
|
|
73
72
|
* @param {object|null} o.prof 고른 프로필
|
|
74
73
|
* @param {string} o.root 작업 폴더
|
|
75
74
|
* @param {boolean} o.바깥가도되나 네트워크를 실제로 두드려도 되나
|
|
76
75
|
*/
|
|
77
76
|
export async function 진찰(o = {}) {
|
|
78
|
-
const {
|
|
77
|
+
const { prof = null, root = process.cwd(), 바깥가도되나 = true, 설정자리 = null, 설정탈 = null } = o;
|
|
79
78
|
const 줄들 = [];
|
|
80
79
|
|
|
81
80
|
// ── 1. 이 프로그램이 도는 자리 ───────────────────────────────────────
|
package/src/safety/guard.js
CHANGED
|
@@ -405,8 +405,32 @@ const BLOCKED = [
|
|
|
405
405
|
*
|
|
406
406
|
* 감싸는 낱말 앞에도 자리를 받고, 셸 이름 앞의 자리는 따옴표 안이면 빈칸도
|
|
407
407
|
* 받는다. 끝은 낱말째여야 한다 — 그래야 `fish.py` 나 `hash.sh` 를 안 문다.
|
|
408
|
+
*
|
|
409
|
+
* ── 앞머리 인자 자리가 **되돌이(ReDoS)** 였다 ─────────────────────────
|
|
410
|
+
*
|
|
411
|
+
* `\S+=\S+` 로 두었더니 `!==!` 한 조각을 등호 자리마다 다르게 쪼갤 수
|
|
412
|
+
* 있었다. 그런 조각이 늘어선 글이 오면 갈래가 두 배씩 불어나 이 울타리
|
|
413
|
+
* 하나가 프로그램을 멈춰 세운다 — **막으려고 둔 자리가 막히는** 꼴이다.
|
|
414
|
+
* 여기 들어오는 것은 모델이 지어낸 명령줄이라 길이도 안 정해져 있다.
|
|
415
|
+
*
|
|
416
|
+
* 인자 꼴을 **하나씩 적어 두는 것을 그만둔다.** 적어 둔 꼴만 받으면
|
|
417
|
+
* 안 적은 꼴에서 통째로 빠져나간다 — 2차 눈이 그 자리를 찾아 줬다.
|
|
418
|
+
*
|
|
419
|
+
* curl … | sudo -E bash 막힘
|
|
420
|
+
* curl … | sudo -u root bash 통과 ← `root` 가 어느 꼴도 아니다
|
|
421
|
+
*
|
|
422
|
+
* `-u root` 처럼 **값이 떨어진 옵션**이 오면 거기서 맞추기가 멈춘다.
|
|
423
|
+
* 이런 옵션은 얼마든지 있다(`-C dir` · `--user x`). 꼴을 늘려 쫓아가는
|
|
424
|
+
* 대신 **셸이 아닌 낱말은 무엇이든 건너뛴다** 로 뒤집는다.
|
|
425
|
+
*
|
|
426
|
+
* 이러면 ReDoS 도 같이 없어진다 — `\S+\s+` 는 빈칸으로 쪼개는 길이
|
|
427
|
+
* 하나뿐이고, 앞에 붙인 부정 전방탐색은 자리를 안 먹는다. 여러 갈래를
|
|
428
|
+
* 늘어놓아 생기던 겹침 자체가 사라진다.
|
|
429
|
+
*
|
|
430
|
+
* 감싸는 낱말(`sudo`·`env`…)은 **그대로 못 박아 둔다.** 그것까지 풀면
|
|
431
|
+
* `curl … | grep foo bash` 같은 것이 걸려 거짓 경고가 된다.
|
|
408
432
|
*/
|
|
409
|
-
re: /\b(?:curl|wget|fetch|aria2c)\b[^|]*\|\s*(?:["']?(?:[^|"'\s]*[\\/])?(?:sudo|doas|env|nohup|command)["']?\s+(
|
|
433
|
+
re: /\b(?:curl|wget|fetch|aria2c)\b[^|]*\|\s*(?:["']?(?:[^|"'\s]*[\\/])?(?:sudo|doas|env|nohup|command)["']?\s+(?:(?!["']?(?:[^|"'\n]*[\\/])?(?:ba|z|k|da|c|tc|fi)?sh(?:\.exe)?(?=["'\s]|$))\S+\s+)*)?["']?(?:[^|"'\n]*[\\/])?(?:ba|z|k|da|c|tc|fi)?sh(?:\.exe)?(?=["'\s]|$)/i,
|
|
410
434
|
why: '받은 스크립트를 그대로 실행합니다',
|
|
411
435
|
},
|
|
412
436
|
{
|
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/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
|
|
|
@@ -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/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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';
|
|
@@ -1034,15 +1034,59 @@ function pdf읽기(abs, ctx) {
|
|
|
1034
1034
|
* { 복사깨짐: 까닭 } 닿은 자리가 반쯤 됐다 — 실패다
|
|
1035
1035
|
* { 원본남음: 까닭 } 닿은 자리는 다 됐고 원본이 남았다 — 실패가 아니다
|
|
1036
1036
|
*
|
|
1037
|
+
* 안 베낀 것이 있으면 `건너뜀` 이 같이 온다. 그때는 언제나 `원본남음` 과 함께다
|
|
1038
|
+
* (바로 아래 머리말).
|
|
1039
|
+
*
|
|
1037
1040
|
* fs 를 밖에서 넣을 수 있게 해 둔 것은 검사 때문이다. 진짜 EXDEV 와 진짜
|
|
1038
1041
|
* '지우기만 깨짐' 은 어느 PC 에서나 똑같이 만들어 낼 수 없다.
|
|
1039
1042
|
*/
|
|
1040
|
-
|
|
1043
|
+
/*
|
|
1044
|
+
* ── cpSync 는 윈도우에서 프로세스를 **통째로** 죽인다 ─────────────────────
|
|
1045
|
+
*
|
|
1046
|
+
* Node 22 + 윈도우에서 경로에 한글이 들어 있으면 cpSync 가 0xC0000409 로
|
|
1047
|
+
* 프로세스를 끝낸다. try 로 못 잡는다 — 예외가 아니라 네이티브 크래시다.
|
|
1048
|
+
* 아래 `복사깨짐` 갈래는 아예 돌지 못하고, 옮기던 사람은 아무 말도 못 듣는다.
|
|
1049
|
+
* 한국어로 쓰는 도구라 폴더 이름에 한글은 늘 있다.
|
|
1050
|
+
*
|
|
1051
|
+
* fsutil.js 의 copyDir 이 **이미 같은 까닭으로** 있었다 — 거기 머리말에
|
|
1052
|
+
* 0xC0000409 까지 적혀 있다. 그런데 옮기기만 cpSync 로 남아 있었다.
|
|
1053
|
+
* 한 자리를 고칠 때 같은 병이 다른 자리에도 있나를 안 물었던 것이다.
|
|
1054
|
+
*
|
|
1055
|
+
* copyDir 은 링크를 안 따라가고 건너뛴 것을 돌려준다. 그 편이 맞다 —
|
|
1056
|
+
* 따라가면 링크 너머의 진짜 파일이 사본이 되고, 원본을 지울 때 링크만
|
|
1057
|
+
* 사라진다. 건너뛴 것은 아래에서 사람에게 그대로 말한다.
|
|
1058
|
+
*/
|
|
1059
|
+
function 안전복사(앞, 뒤) {
|
|
1060
|
+
if (statSync(앞).isDirectory()) return copyDir(앞, 뒤);
|
|
1061
|
+
mkdirSync(dirname(뒤), { recursive: true });
|
|
1062
|
+
copyFileSync(앞, 뒤);
|
|
1063
|
+
return { skipped: [] };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export function 복사해옮기기(앞, 뒤, { 복사 = 안전복사, 지우기 = rmSync } = {}) {
|
|
1067
|
+
let 건너뜀 = [];
|
|
1041
1068
|
try {
|
|
1042
|
-
복사(앞, 뒤, { recursive: true });
|
|
1069
|
+
건너뜀 = 복사(앞, 뒤, { recursive: true })?.skipped ?? [];
|
|
1043
1070
|
} catch (err) {
|
|
1044
1071
|
return { 복사깨짐: err.message };
|
|
1045
1072
|
}
|
|
1073
|
+
/*
|
|
1074
|
+
* ── 안 베낀 것이 있으면 원본을 **안 지운다** (2차 눈) ────────────────
|
|
1075
|
+
*
|
|
1076
|
+
* copyDir 은 링크를 안 따라가고 건너뛴다. 그런데 그 뒤에서 원본 폴더를
|
|
1077
|
+
* 통째로 지우면 **베끼지도 않은 링크가 영영 없어진다.** 옮긴 자리에도
|
|
1078
|
+
* 없고 헌 자리에도 없다 — 재어 봤더니 실제로 그렇게 사라졌다.
|
|
1079
|
+
*
|
|
1080
|
+
* 게다가 화면은 「링크는 안 옮겼습니다」 라고 말할 참이었다. 안 옮긴 것이
|
|
1081
|
+
* 아니라 **지운** 것인데. 옮기기가 파일을 없애는 꼴이라 여기서 멈춘다.
|
|
1082
|
+
*
|
|
1083
|
+
* 못 옮긴 것이 하나라도 있으면 원본을 그대로 둔다. 무엇이 남았는지 말하고
|
|
1084
|
+
* 사람이 고르게 한다 — 안전망(/undo)도 못 되살리는 것을 조용히 지우는
|
|
1085
|
+
* 것보다 낫다.
|
|
1086
|
+
*/
|
|
1087
|
+
if (건너뜀.length) {
|
|
1088
|
+
return { 건너뜀, 원본남음: `링크 ${건너뜀.length}개를 안 베껴, 지우면 없어지므로 원본을 그대로 뒀습니다` };
|
|
1089
|
+
}
|
|
1046
1090
|
try {
|
|
1047
1091
|
지우기(앞, { recursive: true, force: true });
|
|
1048
1092
|
} catch (err) {
|
|
@@ -1240,6 +1284,8 @@ async function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
1240
1284
|
}
|
|
1241
1285
|
|
|
1242
1286
|
let 원본남음 = null;
|
|
1287
|
+
/** 드라이브를 넘는 복사에서 안 따라간 링크. 옮겼다는 말과 같이 적어 준다. */
|
|
1288
|
+
let 건너뛴링크 = [];
|
|
1243
1289
|
/*
|
|
1244
1290
|
* 옮기는 함수는 갈아 끼울 수 있게 둔다. rename 이 EBUSY 로 깨지는 판(다른
|
|
1245
1291
|
* 프로그램이 잡고 있는 파일)은 검사에서 파일 시스템으로 못 만든다
|
|
@@ -1269,6 +1315,7 @@ async function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
1269
1315
|
* 복사 도중에 끊기면 양쪽에 반씩 남기 때문이다.
|
|
1270
1316
|
*/
|
|
1271
1317
|
const 벌어진일 = 복사해옮기기(앞, 뒤);
|
|
1318
|
+
건너뛴링크 = 벌어진일.건너뜀 ?? [];
|
|
1272
1319
|
if (벌어진일.복사깨짐) {
|
|
1273
1320
|
// 닿은 자리를 여기서 지우지 않는다 — 이미 있던 폴더로 옮기는 중이었으면
|
|
1274
1321
|
// 남의 파일까지 지운다. 어디에 반쯤 남았는지만 정확히 말한다.
|
|
@@ -1288,7 +1335,11 @@ async function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
1288
1335
|
+ (되돌리기반쪽
|
|
1289
1336
|
? `\n(파일이 ${훑은것.상한.toLocaleString('en-US')}개를 넘어 되돌리기에는 앞부분만 떴습니다 — 옮기기는 전부 됐지만 /undo 는 다 못 되돌립니다)`
|
|
1290
1337
|
: '')
|
|
1291
|
-
+ (
|
|
1338
|
+
+ (건너뛴링크.length
|
|
1339
|
+
? `\n(링크 ${건너뛴링크.length}개는 안 옮겼습니다 — 드라이브를 넘는 복사라 링크는 따라가지 않습니다:`
|
|
1340
|
+
+ ` ${건너뛴링크.slice(0, 3).map((p) => ctx.scope.show(p)).join(' · ')}${건너뛴링크.length > 3 ? ' 외' : ''}).`
|
|
1341
|
+
+ `\n 그래서 원본도 안 지웠습니다 — ${ctx.scope.show(앞)} 에 그대로 있습니다.`
|
|
1342
|
+
: 원본남음
|
|
1292
1343
|
? `\n(닿은 자리에는 다 옮겼는데 원본을 못 지웠습니다 — 지금 ${ctx.scope.show(앞)} 에도 그대로 있습니다: ${원본남음})`
|
|
1293
1344
|
: '')
|
|
1294
1345
|
+ (이름못되돌림
|
package/src/tools/jobs.js
CHANGED
|
@@ -313,7 +313,10 @@ export function 무리끊기(pid, { 곧장 = false, 늦게 = 800 } = {}) {
|
|
|
313
313
|
* 기다림은 최선이 아니라 모두가 내는 값**이라 짧게 못 박는다. SIGKILL 은
|
|
314
314
|
* 그대로 나가므로, 신호를 무시하는 손자를 살려 보내는 일은 없다.
|
|
315
315
|
*/
|
|
316
|
-
|
|
316
|
+
// 검사도 이 값을 쓴다 (test/jobs.test.js 의 「상한이 유닉스 갈래에서도
|
|
317
|
+
// 지켜진다」). 거기서 400 을 손으로 적어 두면, 이 값을 고친 날 그 검사는
|
|
318
|
+
// **조용히 아무것도 안 가르는 검사**가 된다.
|
|
319
|
+
export const 못재고기다릴최대 = 400;
|
|
317
320
|
|
|
318
321
|
/** 무리끊기 의 **기다릴 수 있는** 판. 까닭은 바로 위 머리말에. */
|
|
319
322
|
export async function 무리끊기기다려(pid, { 곧장 = false, 늦게 = 800 } = {}) {
|
package/src/tools/pdf.js
CHANGED
|
@@ -1070,16 +1070,14 @@ function 글꼴만들기(문, 글꼴사전) {
|
|
|
1070
1070
|
const 너비 = 너비표읽기(문, 안, 두바이트);
|
|
1071
1071
|
|
|
1072
1072
|
// ① /ToUnicode 가 있으면 그것이 가장 정확하다.
|
|
1073
|
-
let 표 = new Map();
|
|
1074
1073
|
let 폭 = 두바이트 ? 2 : 1;
|
|
1075
1074
|
const 투 = 문.풀기(안.ToUnicode);
|
|
1076
1075
|
if (투?.날것) {
|
|
1077
1076
|
const r = 흐름풀기(투, (x) => 문.풀기(x));
|
|
1078
1077
|
if (r.ok) {
|
|
1079
1078
|
const 읽은것 = 유니코드표읽기(r.자료.toString('latin1'));
|
|
1080
|
-
표 = 읽은것.표;
|
|
1081
1079
|
if (읽은것.폭) 폭 = 읽은것.폭;
|
|
1082
|
-
if (
|
|
1080
|
+
if (읽은것.표.size) return { 폭, 표: 읽은것.표, 읽나: true, 너비 };
|
|
1083
1081
|
}
|
|
1084
1082
|
}
|
|
1085
1083
|
|
package/src/tools/verify.js
CHANGED
|
@@ -97,10 +97,16 @@ export function html보기(글, { 있는파일 = () => true } = {}) {
|
|
|
97
97
|
const 탈 = [];
|
|
98
98
|
const 쌓임 = [];
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
/*
|
|
101
|
+
* 주석과 script/style 안엣것은 빼고 본다 — 그 안의 `<` 는 태그가 아니다.
|
|
102
|
+
*
|
|
103
|
+
* 닫는 태그 무늬는 아래 참조 훑기와 **같은 잣대**여야 한다. 한쪽만 넓히면
|
|
104
|
+
* 같은 글을 두 자리가 다르게 읽는다 — 19회차에 아래만 고치고 여기를 지나칠
|
|
105
|
+
* 뻔했다. 한 자리를 고칠 때 같은 파일에 같은 병이 또 있나를 먼저 본다.
|
|
106
|
+
*/
|
|
101
107
|
const 뼈 = String(글)
|
|
102
108
|
.replace(/<!--[\s\S]*?-->/g, '')
|
|
103
|
-
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s
|
|
109
|
+
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1(?:[\s/][^>]*)?>/gi, '<$1></$1>');
|
|
104
110
|
|
|
105
111
|
for (const m of 뼈.matchAll(/<(\/?)([a-zA-Z!][a-zA-Z0-9-]*)([^>]*)>/g)) {
|
|
106
112
|
const 닫는가 = m[1] === '/';
|
|
@@ -165,7 +171,15 @@ export function html보기(글, { 있는파일 = () => true } = {}) {
|
|
|
165
171
|
*/
|
|
166
172
|
const 참조볼글 = String(글)
|
|
167
173
|
.replace(/<!--[\s\S]*?-->/g, '')
|
|
168
|
-
|
|
174
|
+
/*
|
|
175
|
+
* 닫는 태그에 속성이 붙어도 브라우저는 닫는 것으로 본다 — webfetch.js 와 같은 잣대.
|
|
176
|
+
*
|
|
177
|
+
* 그런데 그 닫는 태그를 **그대로 남기면** 안 된다. `</script src="x.js">`
|
|
178
|
+
* 의 src 가 아래 참조 훑기에 걸려 「x.js 가 없습니다」 라는 **거짓 탈**이
|
|
179
|
+
* 된다(2차 눈이 재현해 줬다). 여는 태그는 진짜 참조라 남기고, 닫는
|
|
180
|
+
* 태그는 이름만 남겨 껍데기로 적는다.
|
|
181
|
+
*/
|
|
182
|
+
.replace(/(<(script|style)\b[^>]*>)[\s\S]*?<\/\2(?:[\s/][^>]*)?>/gi, '$1</$2>');
|
|
169
183
|
for (const m of 참조볼글
|
|
170
184
|
.matchAll(/(?<![-\w.])(?:src|href)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi)) {
|
|
171
185
|
const 곳 = m[1] ?? m[2];
|
package/src/tools/webfetch.js
CHANGED
|
@@ -213,7 +213,17 @@ function 태그벗기기(html) {
|
|
|
213
213
|
* 진짜 `</script>` 까지 본문을 통째로 먹고 `<!--` 만 남겼다. 브라우저는 **먼저 열린 쪽**을
|
|
214
214
|
* 따른다 — 한 무늬의 갈래로 두면 정규식도 왼쪽에서 먼저 걸리는 쪽을 따른다.
|
|
215
215
|
*/
|
|
216
|
-
|
|
216
|
+
/*
|
|
217
|
+
* 닫는 태그는 `</script>` 만이 아니다 (2.0.0 CodeQL js/bad-tag-filter).
|
|
218
|
+
* 브라우저는 `</script foo>` 도, 줄바꿈이 낀 `</script\n>` 도, 빗금으로
|
|
219
|
+
* 바로 닫는 `</script/>` 도 닫는 것으로 본다. `\s*>` 로만 두면 그런 꼴을
|
|
220
|
+
* 못 잡고 **스크립트 속이 글로 새어** 모델에게 그대로 간다.
|
|
221
|
+
*
|
|
222
|
+
* 빗금 갈래를 빼먹었더니 아래 「안 닫힌 것은 끝까지」 가 그 뒤를 통째로
|
|
223
|
+
* 먹어 **본문이 통째로 사라졌다**(2차 눈이 재현해 줬다). 못 걸러 새는
|
|
224
|
+
* 것만 흠이 아니라, 멀쩡한 글을 지우는 것도 같은 무게의 흠이다.
|
|
225
|
+
*/
|
|
226
|
+
글 = 글.replace(/<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script(?:[\s/][^>]*)?>|<style\b[\s\S]*?<\/style(?:[\s/][^>]*)?>/gi, ' ');
|
|
217
227
|
if (글 === 전) break;
|
|
218
228
|
}
|
|
219
229
|
/*
|