deel-local-cli 1.17.9 → 1.19.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 +7 -3
- package/README.md +7 -3
- package/package.json +2 -1
- package/src/agent/loop.js +7 -3
- package/src/agent/route.js +515 -29
- package/src/agent/session.js +81 -7
- package/src/backend/adapter.js +3 -7
- package/src/backend/cachemark.js +139 -13
- package/src/backend/http.js +12 -1
- package/src/backend/mcp.js +36 -0
- package/src/backend/probe.js +2 -2
- package/src/backend/vision.js +110 -1
- package/src/backend/wire.js +53 -3
- package/src/commands.js +14 -6
- package/src/i18n/en.js +0 -6
- package/src/i18n/ja.js +0 -6
- package/src/i18n/ko.js +0 -6
- package/src/i18n/zh.js +0 -6
- package/src/oneshot.js +3 -0
- package/src/repl.js +44 -2
- package/src/safety/guard.js +106 -3
- package/src/safety/secrets.js +296 -2
- package/src/skills/discover.js +79 -3
- package/src/tools/clipboard.js +29 -2
- package/src/tools/fsutil.js +25 -4
- package/src/tools/index.js +18 -1
- package/src/tools//355/231/225/354/235/270/353/262/225.js +71 -1
package/src/commands.js
CHANGED
|
@@ -12,7 +12,7 @@ import { allowEndpoint } from './safety/network.js';
|
|
|
12
12
|
import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
|
|
13
13
|
import { 주소가리기 } from './safety/secrets.js';
|
|
14
14
|
import { pick, confirm } from './ui/prompt.js';
|
|
15
|
-
import { load,
|
|
15
|
+
import { load, 저장시도, resolveKey, upsert, 열쇠보관, configPath } from './config.js';
|
|
16
16
|
import { 지금상태 as 지금열쇠상태, 받기설정, 잊기 as 받은열쇠잊기 } from './safety/authcmd.js';
|
|
17
17
|
// 열쇠받기 명령을 정책이 못박아 뒀을 수 있다 — 받기설정 이 그 값을 같이 본다.
|
|
18
18
|
import { 정책읽기 } from './safety/policy.js';
|
|
@@ -1142,7 +1142,9 @@ export async function handle(line, session, ctx) {
|
|
|
1142
1142
|
const cfg2 = load();
|
|
1143
1143
|
const ps = toProfiles(found, cfg2.profiles);
|
|
1144
1144
|
for (const x of ps) upsert(cfg2, x);
|
|
1145
|
-
|
|
1145
|
+
// 설정남기기() 를 안 쓰고 있었다 — 못 남기면 명령이 그 자리에서 죽고,
|
|
1146
|
+
// 남겼는데도 뭐가 틀리면 「등록했습니다」 만 보고 다음에 없는 것을 본다.
|
|
1147
|
+
if (!설정남기기(cfg2)) return { handled: true };
|
|
1146
1148
|
say(` ${mark.ok} ${ps.length}개 등록했습니다. ${c.cyan('/model')} 로 고르세요.`);
|
|
1147
1149
|
} else {
|
|
1148
1150
|
say(` ${c.gray('등록하려면')} ${c.cyan('/scan save')} ${c.gray('고르려면')} ${c.cyan('/model')}`);
|
|
@@ -1829,11 +1831,16 @@ function showLevel(session, arg) {
|
|
|
1829
1831
|
return;
|
|
1830
1832
|
}
|
|
1831
1833
|
session.level = 골라진;
|
|
1834
|
+
/*
|
|
1835
|
+
* 못 남기면 말한다. 바로 아래가 `✓ 개발자` 를 찍는데, 못 남긴 판에서는
|
|
1836
|
+
* 그 말이 이번 판에서만 참이다 — 설정남기기() 가 아홉 자리에서 없앤 그
|
|
1837
|
+
* 꼴인데 이 자리만 옛 모양으로 남아 있었다.
|
|
1838
|
+
*/
|
|
1832
1839
|
try {
|
|
1833
1840
|
const cfg = load();
|
|
1834
1841
|
cfg.level = 골라진;
|
|
1835
|
-
|
|
1836
|
-
} catch { /* 못
|
|
1842
|
+
설정남기기(cfg);
|
|
1843
|
+
} catch { /* 설정을 못 읽는 것은 위에서 따로 말한다 */ }
|
|
1837
1844
|
const lv = LEVELS[골라진];
|
|
1838
1845
|
say('');
|
|
1839
1846
|
say(` ${mark.ok} ${c.bold(lv.name)} ${c.gray('— ' + lv.hint)}`);
|
|
@@ -2845,11 +2852,12 @@ async function ctxLength(session, arg = '') {
|
|
|
2845
2852
|
session.conn.ctx = 값;
|
|
2846
2853
|
const cfg = load();
|
|
2847
2854
|
const prof = cfg.profiles.find((p) => p.id === cfg.active) ?? cfg.profiles[0];
|
|
2848
|
-
|
|
2855
|
+
// 못 남기면 아래 「프로필에 저장했습니다」 가 거짓말이 된다. 살아있는지 들고 간다.
|
|
2856
|
+
const 남김 = prof ? (prof.ctx = 값, 설정남기기(cfg)) : false;
|
|
2849
2857
|
const b = session.breakdown();
|
|
2850
2858
|
say(` ${mark.ok} 컨텍스트 ${c.bold(값.toLocaleString())} 토큰 ${c.gray(`(${fmtSize(값)}) — ${어디서}`)}`);
|
|
2851
2859
|
say(` ${c.gray('지금 찬 양')} ${c.white(b.used.toLocaleString())} ${c.gray('· 남음')} ${c.white(b.left.toLocaleString())}`);
|
|
2852
|
-
if (
|
|
2860
|
+
if (남김) say(` ${c.gray('프로필에 저장했습니다. 다음에 켤 때도 이 값입니다.')}`);
|
|
2853
2861
|
say('');
|
|
2854
2862
|
};
|
|
2855
2863
|
|
package/src/i18n/en.js
CHANGED
|
@@ -426,7 +426,6 @@ export const en = {
|
|
|
426
426
|
'sum.justDone': '{n} just now',
|
|
427
427
|
'sum.noChange': 'unchanged',
|
|
428
428
|
'sum.verified': '{n} checked',
|
|
429
|
-
'sum.readAs': 'read via {쓴것}',
|
|
430
429
|
'sum.encGuess': '{인코딩} (guessed)',
|
|
431
430
|
'sum.linesOf': '{준}/{전체} lines',
|
|
432
431
|
'sum.countOf': '{n}/{전체}',
|
|
@@ -496,7 +495,6 @@ export const en = {
|
|
|
496
495
|
'paste.pasted': 'Pasted {n} lines — Enter sends them all at once',
|
|
497
496
|
'run.thinking': 'thinking…',
|
|
498
497
|
'run.thinkingChars': 'thinking… {n} chars',
|
|
499
|
-
'run.turnFoot': '{초}s · {n} tool calls · ↑{입력} ↓{출력}',
|
|
500
498
|
'run.bye': 'Bye.',
|
|
501
499
|
'run.byeStats': '{n} model calls · {초}s in tools · ↑{입력} ↓{출력}',
|
|
502
500
|
'run.notMade': '— was not created',
|
|
@@ -596,9 +594,5 @@ export const en = {
|
|
|
596
594
|
'undo.wayDeleted': 'deleted (did not exist before)',
|
|
597
595
|
|
|
598
596
|
// ── Common ────────────────────────────────────────────────────────────
|
|
599
|
-
'common.unknownCommand': 'Unknown command',
|
|
600
|
-
'common.seeHelp': 'Run /help for the list',
|
|
601
|
-
'common.yes': 'yes',
|
|
602
|
-
'common.no': 'no',
|
|
603
597
|
'common.cfgSaveFailed': 'Could not write the setting to disk ({왜}) — it applies now but reverts on restart.',
|
|
604
598
|
};
|
package/src/i18n/ja.js
CHANGED
|
@@ -386,7 +386,6 @@ export const ja = {
|
|
|
386
386
|
'sum.justDone': 'たった今{n}個',
|
|
387
387
|
'sum.noChange': '変更なし',
|
|
388
388
|
'sum.verified': '確認{n}個',
|
|
389
|
-
'sum.readAs': '{쓴것} で変換して読み込み',
|
|
390
389
|
'sum.encGuess': '{인코딩}(推定)',
|
|
391
390
|
'sum.linesOf': '{준}/{전체}行',
|
|
392
391
|
'sum.countOf': '{n}/{전체}個',
|
|
@@ -454,7 +453,6 @@ export const ja = {
|
|
|
454
453
|
'paste.pasted': '{n}行を貼り付けました — Enter で一度に送ります',
|
|
455
454
|
'run.thinking': '考え中…',
|
|
456
455
|
'run.thinkingChars': '考え中… {n}文字',
|
|
457
|
-
'run.turnFoot': '{초}秒 · ツール{n}回 · ↑{입력} ↓{출력}',
|
|
458
456
|
'run.bye': '終了します。',
|
|
459
457
|
'run.byeStats': 'モデル呼び出し{n}回 · ツール時間{초}秒 · ↑{입력} ↓{출력}',
|
|
460
458
|
'run.notMade': '— 作られませんでした',
|
|
@@ -549,9 +547,5 @@ export const ja = {
|
|
|
549
547
|
'undo.wayRestored': '戻しました',
|
|
550
548
|
'undo.wayDeleted': '削除(元々なかったファイル)',
|
|
551
549
|
|
|
552
|
-
'common.unknownCommand': '知らないコマンド',
|
|
553
|
-
'common.seeHelp': '/help で一覧が出ます',
|
|
554
|
-
'common.yes': 'はい',
|
|
555
|
-
'common.no': 'いいえ',
|
|
556
550
|
'common.cfgSaveFailed': '設定をファイルに保存できませんでした({왜})— 今回は効きますが、開き直すと元に戻ります。',
|
|
557
551
|
};
|
package/src/i18n/ko.js
CHANGED
|
@@ -445,7 +445,6 @@ export const ko = {
|
|
|
445
445
|
'sum.justDone': '방금 {n}개',
|
|
446
446
|
'sum.noChange': '그대로 (바뀐 것 없음)',
|
|
447
447
|
'sum.verified': '확인 {n}개',
|
|
448
|
-
'sum.readAs': '{쓴것} 로 바꿔 읽음',
|
|
449
448
|
'sum.encGuess': '{인코딩} (추정)',
|
|
450
449
|
'sum.linesOf': '{준}/{전체}줄',
|
|
451
450
|
'sum.countOf': '{n}/{전체}개',
|
|
@@ -542,7 +541,6 @@ export const ko = {
|
|
|
542
541
|
'paste.pasted': '{n}줄을 붙여넣었습니다 — Enter 로 한 번에 보냅니다',
|
|
543
542
|
'run.thinking': '생각 중…',
|
|
544
543
|
'run.thinkingChars': '생각 중… {n}자',
|
|
545
|
-
'run.turnFoot': '{초}초 · 도구 {n}회 · ↑{입력} ↓{출력}',
|
|
546
544
|
'run.bye': '끝냅니다.',
|
|
547
545
|
'run.byeStats': '모델 호출 {n}회 · 도구 시간 {초}초 · ↑{입력} ↓{출력}',
|
|
548
546
|
'run.notMade': '— 만들어지지 않았습니다',
|
|
@@ -643,9 +641,5 @@ export const ko = {
|
|
|
643
641
|
'undo.wayDeleted': '삭제됨(원래 없던 파일)',
|
|
644
642
|
|
|
645
643
|
// ── 자주 나오는 것 ────────────────────────────────────────────────────
|
|
646
|
-
'common.unknownCommand': '모르는 명령입니다',
|
|
647
|
-
'common.seeHelp': '명령 목록은 /help',
|
|
648
|
-
'common.yes': '네',
|
|
649
|
-
'common.no': '아니요',
|
|
650
644
|
'common.cfgSaveFailed': '설정을 파일에 남기지 못했습니다 ({왜}) — 이번 판에는 먹지만 다시 켜면 돌아갑니다.',
|
|
651
645
|
};
|
package/src/i18n/zh.js
CHANGED
|
@@ -386,7 +386,6 @@ export const zh = {
|
|
|
386
386
|
'sum.justDone': '刚完成{n}个',
|
|
387
387
|
'sum.noChange': '没有变化',
|
|
388
388
|
'sum.verified': '核对{n}项',
|
|
389
|
-
'sum.readAs': '用 {쓴것} 转换后读取',
|
|
390
389
|
'sum.encGuess': '{인코딩}(推测)',
|
|
391
390
|
'sum.linesOf': '{준}/{전체}行',
|
|
392
391
|
'sum.countOf': '{n}/{전체}个',
|
|
@@ -454,7 +453,6 @@ export const zh = {
|
|
|
454
453
|
'paste.pasted': '已粘贴 {n} 行 — 按 Enter 一次发送',
|
|
455
454
|
'run.thinking': '思考中…',
|
|
456
455
|
'run.thinkingChars': '思考中… {n}字',
|
|
457
|
-
'run.turnFoot': '{초}秒 · 工具{n}次 · ↑{입력} ↓{출력}',
|
|
458
456
|
'run.bye': '结束。',
|
|
459
457
|
'run.byeStats': '模型调用{n}次 · 工具耗时{초}秒 · ↑{입력} ↓{출력}',
|
|
460
458
|
'run.notMade': '— 没有被创建',
|
|
@@ -549,9 +547,5 @@ export const zh = {
|
|
|
549
547
|
'undo.wayRestored': '已还原',
|
|
550
548
|
'undo.wayDeleted': '已删除(原本不存在的文件)',
|
|
551
549
|
|
|
552
|
-
'common.unknownCommand': '不认识的命令',
|
|
553
|
-
'common.seeHelp': '用 /help 看命令列表',
|
|
554
|
-
'common.yes': '是',
|
|
555
|
-
'common.no': '否',
|
|
556
550
|
'common.cfgSaveFailed': '设置没能写入文件({왜})— 本次有效,重开就会恢复原样。',
|
|
557
551
|
};
|
package/src/oneshot.js
CHANGED
|
@@ -506,6 +506,9 @@ export async function runOnce(opts = {}) {
|
|
|
506
506
|
session.routed = 골라진.mode;
|
|
507
507
|
const w = getWork(골라진.mode);
|
|
508
508
|
곁(` ${c.hcyan(w.glyph)} ${c.gray(`${w.name} (${w.en}) — 말 속에 ${골라진.why} 가 있어서`)}`);
|
|
509
|
+
} else if (골라진.일부러) {
|
|
510
|
+
// 일부러 안 보낸 자리만 말한다. 대화 화면과 같은 규칙이다 (repl.js 참고).
|
|
511
|
+
곁(` ${c.gray(`◇ 종합 그대로 — ${골라진.why}`)}`);
|
|
509
512
|
}
|
|
510
513
|
}
|
|
511
514
|
|
package/src/repl.js
CHANGED
|
@@ -26,7 +26,7 @@ import { 언어서버있나 } from './tools/index.js';
|
|
|
26
26
|
import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
|
|
27
27
|
import { History } from './safety/undo.js';
|
|
28
28
|
import { Audit, 열쇠묻기 } from './safety/audit.js';
|
|
29
|
-
import { activeProfile, load, resolveKey, save as saveCfg, homeDir, 잠금소식, 열쇠탈소식, 프로젝트설정소식 } from './config.js';
|
|
29
|
+
import { activeProfile, load, resolveKey, save as saveCfg, 저장시도, homeDir, 잠금소식, 열쇠탈소식, 프로젝트설정소식 } from './config.js';
|
|
30
30
|
import { 프로젝트설정줄들 } from './safety/trust.js';
|
|
31
31
|
import { 남길것읽기 } from './safety/shellenv.js';
|
|
32
32
|
import { discover } from './skills/discover.js';
|
|
@@ -1439,7 +1439,29 @@ export async function chatLoop(opts = {}) {
|
|
|
1439
1439
|
// 알아낸 값은 프로필에 남긴다. 다음에 켤 때 화면이 곧바로 맞게 뜬다.
|
|
1440
1440
|
if (prof.ctx !== r.value) {
|
|
1441
1441
|
prof.ctx = r.value;
|
|
1442
|
-
|
|
1442
|
+
/*
|
|
1443
|
+
* 못 남기면 **말한다.**
|
|
1444
|
+
*
|
|
1445
|
+
* 여기는 `try { … saveCfg(cfg2) } catch {}` 였다. 그런데 바로 다음
|
|
1446
|
+
* 줄이 「컨텍스트를 32,768 → 128,000 로 맞췄습니다」 를 찍는다.
|
|
1447
|
+
* 홈이 읽기 전용이거나 디스크가 차 있으면 그 말이 **그 자리에서만**
|
|
1448
|
+
* 참이고, 다음에 켜면 옛 값으로 돌아가 있다. 사람은 왜 그런지 알
|
|
1449
|
+
* 길이 없다 — commands.js 의 설정남기기() 가 아홉 자리에서 없앤
|
|
1450
|
+
* 바로 그 꼴인데, 이 자리만 옛 모양으로 남아 있었다.
|
|
1451
|
+
*/
|
|
1452
|
+
let 남김 = { ok: true };
|
|
1453
|
+
const cfg2 = (() => { try { return load(); } catch (e) { 남김 = { ok: false, 왜: e?.message ?? String(e) }; return null; } })();
|
|
1454
|
+
const t = cfg2?.profiles?.find((p) => p.id === prof.id);
|
|
1455
|
+
/*
|
|
1456
|
+
* 프로필을 못 찾으면 **그것도 못 남긴 것이다.** 처음 고칠 때 `if (t)`
|
|
1457
|
+
* 하나로 끝냈더니, 못 찾은 판에서는 아무것도 안 남기고 `남김.ok` 가
|
|
1458
|
+
* 처음값 그대로 참이라 경고도 안 나갔다 — 없애려던 그 모양이 한 갈래에
|
|
1459
|
+
* 그대로 남아 있었다(2차 리뷰가 짚었다). 디스크의 설정이 이 판과 어긋난
|
|
1460
|
+
* 때(다른 창에서 프로필을 지운 뒤)가 그 자리다.
|
|
1461
|
+
*/
|
|
1462
|
+
if (t) { t.ctx = r.value; 남김 = 저장시도(cfg2); }
|
|
1463
|
+
else if (cfg2) 남김 = { ok: false, 왜: '설정에서 이 프로필을 못 찾았습니다' };
|
|
1464
|
+
if (!남김.ok) 길이경고.push(옮긴말('common.cfgSaveFailed', { 왜: String(남김.왜 ?? '').slice(0, 70) }));
|
|
1443
1465
|
}
|
|
1444
1466
|
if (전 !== r.value) 길이알림.push(`컨텍스트를 ${전.toLocaleString()} ${c.gray('→')} ${c.white(r.value.toLocaleString())} 로 맞췄습니다 ${c.gray('(' + (r.source ?? '서버') + '에서 읽음)')}`);
|
|
1445
1467
|
if (r.max && r.loaded && r.max > r.loaded) {
|
|
@@ -1515,6 +1537,14 @@ export async function chatLoop(opts = {}) {
|
|
|
1515
1537
|
}
|
|
1516
1538
|
// 안 뜬 것은 조용히 빠지면 안 된다. "왜 그 도구가 없지" 를 영영 알 수 없다.
|
|
1517
1539
|
for (const m of mcp붙임.못한것) warn.push(`MCP ${c.white(m.이름)} 을 못 붙였습니다 — ${m.왜}`);
|
|
1540
|
+
/*
|
|
1541
|
+
* 이 폴더에 스킬을 적어 뒀는데 안 믿는 폴더라 안 읽었다. **말해야 한다** —
|
|
1542
|
+
* 제가 적은 스킬이 왜 안 뜨는지 모르는 것이 두 번째로 나쁜 일이다
|
|
1543
|
+
* (safety/hooks.js 의 안믿음 과 같은 방식).
|
|
1544
|
+
*/
|
|
1545
|
+
if (found.안믿음) {
|
|
1546
|
+
warn.push(`이 폴더의 스킬은 안 읽었습니다 — 믿는 폴더가 아닙니다. 읽게 하려면 ${c.cyan('deel trust')}`);
|
|
1547
|
+
}
|
|
1518
1548
|
if (!conn.tools) warn.push(옮긴말('run.noTools'));
|
|
1519
1549
|
if (!conn.streaming) warn.push(옮긴말('run.noStream'));
|
|
1520
1550
|
warn.push(...길이경고);
|
|
@@ -1728,6 +1758,18 @@ export async function chatLoop(opts = {}) {
|
|
|
1728
1758
|
} else {
|
|
1729
1759
|
say(` ${c.gray('다르면')} ${c.cyan('/code')} ${c.gray('처럼 직접 고르세요. 그때부터는 안 바뀝니다.')}`);
|
|
1730
1760
|
}
|
|
1761
|
+
} else if (골라진.일부러) {
|
|
1762
|
+
/*
|
|
1763
|
+
* 골랐어야 할 만한 신호가 있었는데 **일부러 안 보낸** 자리다.
|
|
1764
|
+
*
|
|
1765
|
+
* 「설계 신호가 있었지만 고치라는 말이라 안 보냈다」 와 「그냥
|
|
1766
|
+
* 모르겠다」 는 사람에게 아주 다른 말이다. 앞엣것을 안 찍으면,
|
|
1767
|
+
* 설계를 기대한 사람은 종합으로 도는 화면을 보고 자동 모드가
|
|
1768
|
+
* 고장 났다고 여긴다. 뒤엣것은 안 찍는다 — 보통 한마디가 다
|
|
1769
|
+
* 그쪽이라 매번 뜨면 소음이다. 가르는 일은 route 가 한다.
|
|
1770
|
+
*/
|
|
1771
|
+
say('');
|
|
1772
|
+
say(` ${c.gray('◇ 종합 그대로 —')} ${c.gray(골라진.why)}`);
|
|
1731
1773
|
}
|
|
1732
1774
|
}
|
|
1733
1775
|
|
package/src/safety/guard.js
CHANGED
|
@@ -150,11 +150,50 @@ const BLOCKED = [
|
|
|
150
150
|
// 가운데 `(-?[a-z]*\s*)*` 는 양쪽 다 빈 문자열을 허용해서, 안 맞는 긴 입력이
|
|
151
151
|
// 들어오면 되돌아가는 경우의 수가 기하급수로 늘어난다 — guard 검사 자체가
|
|
152
152
|
// 멈춰 버린다(ReDoS). 각 겹이 최소 한 글자는 먹게 고쳐서 그 길을 막는다.
|
|
153
|
+
/*
|
|
154
|
+
* ── 우리가 적어 둔 어순만 막고 있었다 ──────────────────────────────────
|
|
155
|
+
*
|
|
156
|
+
* 옛 규칙은 두 가지를 못박았다 — 플래그가 **경로보다 앞**에 오고, 드라이브
|
|
157
|
+
* 뒤가 **역빗금**일 것. 둘 다 그냥 우리가 예로 적은 모양이었지 파워셸이
|
|
158
|
+
* 요구하는 것이 아니다.
|
|
159
|
+
*
|
|
160
|
+
* Remove-Item -Recurse -Force C:\ 막힘 ← 예로 적어 둔 모양
|
|
161
|
+
* Remove-Item C:\ -Recurse -Force 통과 ← 파워셸의 흔한 어순
|
|
162
|
+
* Remove-Item -Recurse -Force C:/ 통과 ← 파워셸은 빗금도 받는다
|
|
163
|
+
* Remove-Item ~ -Recurse -Force 통과 ←
|
|
164
|
+
*
|
|
165
|
+
* 셋 다 드라이브나 집을 통째로 민다. 「막았다」 가 아니라 「내가 적은
|
|
166
|
+
* 모양으로 적은 사람만 막았다」 였다.
|
|
167
|
+
*
|
|
168
|
+
* 그래서 어순을 안 따진다. `-Recurse` 가 이 마디 안에 **있기만** 하면
|
|
169
|
+
* 되고(앞보기), 목표는 마디 어디에 있어도 된다. 뒤에 `\s|$` 를 붙여
|
|
170
|
+
* 「드라이브 **뿌리**」 일 때만 잡는다 — `C:\proj\tmp` 는 그대로 통과한다.
|
|
171
|
+
*
|
|
172
|
+
* 되돌아가는 길이 폭발하지 않게 별(`*`)은 하나씩만 둔다. 옛 주석이 적어
|
|
173
|
+
* 둔 ReDoS 자리를 다시 만들면 안 된다 — guard 검사 자체가 멈춰 선다.
|
|
174
|
+
*/
|
|
153
175
|
{
|
|
154
|
-
|
|
176
|
+
/*
|
|
177
|
+
* `-r[a-z]*` 인 까닭: 파워셸은 헷갈리지 않는 한 매개변수를 **줄여서**
|
|
178
|
+
* 받는다. Remove-Item 에서 `r` 로 시작하는 것은 `-Recurse` 하나뿐이라
|
|
179
|
+
* `-rec` · `-recur` 가 다 통한다. `-(?:Recurse|r)\b` 로 못박아 뒀더니
|
|
180
|
+
* 그 사이 철자가 전부 새어 나갔다.
|
|
181
|
+
*
|
|
182
|
+
* 목표에 `[\\/]` 한 갈래를 더 둔 까닭: 파워셸에서 `/` 나 `\` 하나만
|
|
183
|
+
* 적으면 **지금 드라이브의 뿌리**다. 둘 다 2차 리뷰가 짚었다.
|
|
184
|
+
*/
|
|
185
|
+
re: /Remove-Item\b(?=[^|;&]*\s-r[a-z]*\b)[^|;&]*\s(?:(?:\$env:USERPROFILE|\$HOME|[a-z]:|~)[\\/]?|[\\/])(?=\s|$)/i,
|
|
155
186
|
why: '드라이브나 집 폴더를 통째로 지웁니다',
|
|
156
187
|
},
|
|
157
|
-
|
|
188
|
+
/*
|
|
189
|
+
* `--force` 만 보고 있었다. 짧은 꼴 `-f` 는 그대로 나갔다 —
|
|
190
|
+
* `git push -f origin main` 은 사람이 실제로 더 많이 치는 쪽이다.
|
|
191
|
+
* 긴 꼴만 막으면 「막았다」 가 아니라 「긴 꼴로 적은 사람만 막았다」 다.
|
|
192
|
+
*/
|
|
193
|
+
{
|
|
194
|
+
re: /git\s+push\b[^|;&]*(?:--force(?!-with-lease)|\s-[a-z]*f[a-z]*\b)/i,
|
|
195
|
+
why: '원격 이력을 덮어씁니다 (--force-with-lease 를 쓰세요)',
|
|
196
|
+
},
|
|
158
197
|
// 낱말만 보면 안 된다. `node scripts/shutdown.js` 나 `npm run reboot` 이
|
|
159
198
|
// '시스템을 끕니다' 로 막히고 감사기록에까지 남았다. 명령의 **첫 낱말**일 때만 본다.
|
|
160
199
|
{ re: /(^|[|;&]\s*)(sudo\s+)?(shutdown|reboot|halt|poweroff)\b/i, why: '시스템을 끕니다' },
|
|
@@ -186,10 +225,74 @@ const BLOCKED = [
|
|
|
186
225
|
{ re: /\biwr\b[^|]*\|\s*iex\b/i, why: '받은 스크립트를 그대로 실행합니다' },
|
|
187
226
|
];
|
|
188
227
|
|
|
228
|
+
/*
|
|
229
|
+
* ── 뒤에 한 마디만 붙이면 울타리가 통째로 열렸다 ────────────────────────
|
|
230
|
+
*
|
|
231
|
+
* 위 규칙 여럿이 `\s*$` 로 **글 끝**에 못을 박는다. `rm -rf /` 가 글 끝일
|
|
232
|
+
* 때만 걸린다는 뜻이다. 그런데 셸은 한 줄에 여러 마디를 넣는다.
|
|
233
|
+
*
|
|
234
|
+
* rm -rf / 막힘
|
|
235
|
+
* rm -rf / ; echo done 통과 ←
|
|
236
|
+
* rm -rf / && echo done 통과 ←
|
|
237
|
+
* rm -rf ~ ; ls 통과 ←
|
|
238
|
+
* del /f /s /q C:\ & echo done 통과 ←
|
|
239
|
+
* Remove-Item -Recurse -Force C:\ ; ... 통과 ←
|
|
240
|
+
*
|
|
241
|
+
* 셸은 두 마디를 **둘 다** 실행한다. 즉 `; echo done` 여덟 글자로 이 파일이
|
|
242
|
+
* 파는 약속이 통째로 깨졌다. 앞에 붙이는 것은 막혔고(`echo hi && rm -rf /`
|
|
243
|
+
* 는 걸린다) 뒤에 붙이는 것만 샜다 — 한쪽만 막힌 울타리라 더 안 보였다.
|
|
244
|
+
*
|
|
245
|
+
* 그래서 **마디로 갈라 각 마디를 따로 본다.** 규칙은 그대로 두고 보는 자리만
|
|
246
|
+
* 늘린다 — 전체 글도 여전히 본다. 막는 쪽으로만 늘어나므로 여태 통과하던
|
|
247
|
+
* 것이 새로 막히는 일은 「위험한 것이 마디 안에 있었다」 뿐이다.
|
|
248
|
+
*
|
|
249
|
+
* 따옴표 안은 안 가린다. 가려서 얻는 것(`echo "rm -rf / ; x"` 를 안 막기)보다
|
|
250
|
+
* 잃는 것(`sh -c "rm -rf / ; x"` 를 못 막기)이 크다. 이 목록은 되돌릴 수
|
|
251
|
+
* 없는 것만 담으므로, 애매하면 막고 사람이 다시 적는 편이 낫다.
|
|
252
|
+
*/
|
|
253
|
+
const 마디로 = (s) => s
|
|
254
|
+
.split(/(?:\|\||&&|[;&|\n\r])+/)
|
|
255
|
+
.map((x) => x.trim())
|
|
256
|
+
.filter(Boolean);
|
|
257
|
+
|
|
189
258
|
export function checkCommand(cmd) {
|
|
190
259
|
const s = String(cmd);
|
|
260
|
+
/*
|
|
261
|
+
* ── 세지 말고 늘 같이 본다 ──────────────────────────────────────────
|
|
262
|
+
*
|
|
263
|
+
* 여기는 `마디로(s).length > 1` 일 때만 마디를 봤다. 2차 리뷰가 그 틈을
|
|
264
|
+
* 짚었다 — `rm -rf / ;` 는 마디가 **하나**다. 뒤가 빈 마디라 걸러지기
|
|
265
|
+
* 때문이다. 그래서 마디 목록이 통째로 버려지고, 남은 전체 글은 `\s*$` 에
|
|
266
|
+
* 안 걸린다. 「뒤에 한 마디 붙이면 샌다」 를 고치면서 **반 마디 붙이면
|
|
267
|
+
* 새는** 자리를 새로 만든 셈이다.
|
|
268
|
+
*
|
|
269
|
+
* 세는 것을 없앤다. 마디가 하나뿐이면 그 마디는 앞뒤 빈칸만 뺀 전체 글이라
|
|
270
|
+
* 더 볼 것도 덜 볼 것도 없다.
|
|
271
|
+
*/
|
|
272
|
+
/*
|
|
273
|
+
* ── 따옴표를 **떼고도** 한 번 본다 ──────────────────────────────────
|
|
274
|
+
*
|
|
275
|
+
* 위 머리말은 「따옴표 안은 안 가린다」 고 적어 뒀다. 그런데 **안 가리는
|
|
276
|
+
* 것과 떼고 보는 것은 다르다.** 안 가리기만 했지 떼 보지는 않아서,
|
|
277
|
+
* 경로에 따옴표를 두르면 규칙이 그대로 빗나갔다 — 셸에서는 뜻이 하나도
|
|
278
|
+
* 안 바뀌는데.
|
|
279
|
+
*
|
|
280
|
+
* rm -rf / 막힘
|
|
281
|
+
* rm -rf "/" 통과 ← 같은 일을 한다
|
|
282
|
+
* Remove-Item -Recurse "C:\" 통과 ←
|
|
283
|
+
*
|
|
284
|
+
* 그래서 따옴표를 뺀 꼴도 볼 것에 넣는다. 막는 쪽으로만 늘어난다 —
|
|
285
|
+
* `echo "결과" > out.txt` 처럼 따옴표를 떼도 아무 규칙에 안 걸리는 것은
|
|
286
|
+
* 그대로 통과한다. 반대로 `sh -c "rm -rf /"` 는 이제 걸린다. 이 목록은
|
|
287
|
+
* 되돌릴 수 없는 것만 담으므로, 애매하면 막고 사람이 다시 적는 편이 낫다.
|
|
288
|
+
*/
|
|
289
|
+
const 따옴표뺀 = (x) => x.replace(/["'`]/g, '');
|
|
290
|
+
const 마디들 = 마디로(s);
|
|
291
|
+
const 볼것 = [...new Set([s, ...마디들, 따옴표뺀(s), ...마디들.map(따옴표뺀)])];
|
|
191
292
|
for (const b of BLOCKED) {
|
|
192
|
-
|
|
293
|
+
for (const 하나 of 볼것) {
|
|
294
|
+
if (b.re.test(하나)) throw new BlockedError(`${b.why}\n 막힌 명령: ${s.slice(0, 120)}`);
|
|
295
|
+
}
|
|
193
296
|
}
|
|
194
297
|
return true;
|
|
195
298
|
}
|