deel-local-cli 2.1.4 → 2.1.6
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 +4 -2
- package/README.md +4 -2
- package/bin/deel.js +23 -0
- package/package.json +1 -1
- package/src/config.js +42 -7
- package/src/reset.js +13 -3
- package/src/safety/guard.js +119 -18
- package/src/safety/keystore.js +97 -15
- package/src/safety/trust.js +45 -10
package/README.ko.md
CHANGED
|
@@ -323,7 +323,7 @@ deel --offline
|
|
|
323
323
|
무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
|
|
324
324
|
|
|
325
325
|
```
|
|
326
|
-
deel 2.1.
|
|
326
|
+
deel 2.1.6 ⌂ 이 안
|
|
327
327
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
328
328
|
```
|
|
329
329
|
|
|
@@ -1341,7 +1341,9 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1341
1341
|
|
|
1342
1342
|
| 판 | 무엇이 바뀌었나 |
|
|
1343
1343
|
|---|---|
|
|
1344
|
-
| **[2.1.
|
|
1344
|
+
| **[2.1.6](docs/ko/releases/2.1.md#216)** | 맥에서 제대로 돌게 — 길거나 손으로 바꾼 키체인 열쇠 · 잠긴 키체인, `/tmp` · `/var`(`/private` 링크) 아래 믿은 폴더를 알아봄, 파이프가 늦게 읽어도 64KB 넘는 출력이 안 잘림(맥 · 리눅스). 관문 CI 에 맥이 들어갔습니다 |
|
|
1345
|
+
| [2.1.5](docs/ko/releases/2.1.md#215) | 급한 보안 수정 — 셸 명령이 작업 폴더 밖으로 새던 자리를 막았습니다. 빗금 없는 `..`(`find .. -delete` · `rd /s /q ..`) · `$PWD` · `%CD%` · `$OLDPWD` · 먼저 옮긴 적 없는 `cd -` · `C:..` · 파워셸의 다른 이름(`ri` · `del` …) |
|
|
1346
|
+
| [2.1.4](docs/ko/releases/2.1.md#214) | 브라우저로 로그인 — `deel setup` 3번이 열쇠 없이 OpenRouter 계정 로그인(OAuth PKCE)으로 붙습니다. 열쇠 하나로 Claude · GPT · Grok · Kimi. SSH 는 `--no-browser`, `sk-or-` 열쇠를 알아보고, 긴 모델 목록은 이름으로 좁힙니다 |
|
|
1345
1347
|
| [2.1.3](docs/ko/releases/2.1.md#213) | 제품 기능 — 도구를 글로 부르는 모델도 알아듣고, `deel run --continue`·`--resume`·`--events` 와 결과의 바뀐 파일(`files`)이 생겼고, git 저장소면 스크립트가 바꾼 것도 `/undo` 로 돌아가고, 언어 서버 없이도 문법을 보고, Outline 이 많이 불리는 파일부터 싣고, MCP 가 주소로 붙어 자료 · 프롬프트까지 씁니다 |
|
|
1346
1348
|
| [2.1.2](docs/ko/releases/2.1.md#212) | 정책 · 작은 공백 · 속도 — confirm 이 읽기만 하지 않는 MCP 도구를 묻고, 에디터(ACP) 세션은 16개를 넘으면 내려놓았다가 다음 말에 되살리고, `--check-rounds`·`--check-timeout` 과 과제별 검사 설정이 `deel eval` 에 생겼고, 시작 때 읽는 파일이 164개에서 18개로 줄었습니다 |
|
|
1347
1349
|
| [2.1.1](docs/ko/releases/2.1.md#211) | 전체 검수에서 나온 것 — 한 칸 위를 물으면 `config explain` 이 열쇠를 보였고, 웹훅 주소·Sentry DSN 이 셸 환경 거르기와 가리기를 둘 다 지났고, `kill` 로 strict 를 지나 Jobs 서버를 껐고, Def·Refs 가 EUC-KR 을 못 읽었습니다 · `deel eval` 고침 |
|
package/README.md
CHANGED
|
@@ -396,7 +396,7 @@ deel --offline
|
|
|
396
396
|
The destination is printed at the top of every session:
|
|
397
397
|
|
|
398
398
|
```
|
|
399
|
-
deel 2.1.
|
|
399
|
+
deel 2.1.6 ⌂ inside
|
|
400
400
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
401
401
|
```
|
|
402
402
|
|
|
@@ -1424,7 +1424,9 @@ so one run tells you everything.
|
|
|
1424
1424
|
|
|
1425
1425
|
| Version | What changed |
|
|
1426
1426
|
|---|---|
|
|
1427
|
-
| **[2.1.
|
|
1427
|
+
| **[2.1.6](docs/en/releases/2.1.md#216)** | Works properly on macOS — long or hand-edited keychain keys, a locked keychain, folders under `/tmp` · `/var` (links to `/private`) recognized as trusted, and output over 64 KB no longer cut off when a pipe reads slowly (macOS and Linux). macOS joins the gate CI |
|
|
1428
|
+
| [2.1.5](docs/en/releases/2.1.md#215) | Urgent security fix — shell commands could reach outside the working folder. Now blocked: a bare `..` (`find .. -delete`, `rd /s /q ..`), `$PWD`, `%CD%`, `$OLDPWD`, a `cd -` with no earlier move, `C:..`, and PowerShell aliases (`ri`, `del` …) |
|
|
1429
|
+
| [2.1.4](docs/en/releases/2.1.md#214) | Log in with a browser — `deel setup` option 3 connects to OpenRouter by logging in (OAuth PKCE), no key to paste; one key for Claude, GPT, Grok and Kimi. `--no-browser` for SSH, `sk-or-` keys recognized, and long model lists narrow by name |
|
|
1428
1430
|
| [2.1.3](docs/en/releases/2.1.md#213) | Product features — models that write tool calls as text are understood, `deel run --continue`/`--resume`/`--events` and changed `files` in the result, `/undo` brings back what a script changed in a git repository, a syntax check without a language server, Outline loads the most imported files first, and MCP attaches by URL with resources and prompts |
|
|
1429
1431
|
| [2.1.2](docs/en/releases/2.1.md#212) | Policy, small gaps and speed — confirm asks before MCP tools that are not read-only, editor (ACP) sessions past 16 are let go and brought back on the next message, `--check-rounds`/`--check-timeout` and per-task check settings in `deel eval`, and startup reads 18 files instead of 164 |
|
|
1430
1432
|
| [2.1.1](docs/en/releases/2.1.md#211) | From a full review — `config explain` one level up showed keys, webhook URLs and Sentry DSNs passed both the shell-env filter and masking, `kill` stopped a Jobs server past strict, Def/Refs could not read EUC-KR, and `deel eval` fixes |
|
package/bin/deel.js
CHANGED
|
@@ -1292,10 +1292,33 @@ async function main() {
|
|
|
1292
1292
|
async function 끝내기(code) {
|
|
1293
1293
|
process.exitCode = code;
|
|
1294
1294
|
await closeConnections();
|
|
1295
|
+
await 다나가기(5000);
|
|
1295
1296
|
const 마지막수단 = setTimeout(() => process.exit(code), 400);
|
|
1296
1297
|
마지막수단.unref();
|
|
1297
1298
|
}
|
|
1298
1299
|
|
|
1300
|
+
/*
|
|
1301
|
+
* ── 마지막수단이 덜 나간 출력을 버렸다 (40회차 · 맥) ─────────────────────
|
|
1302
|
+
*
|
|
1303
|
+
* 맥·리눅스에서 파이프 쓰기는 비동기다(Node 문서 「A note on process I/O」). 받는 쪽이 늦게
|
|
1304
|
+
* 읽으면 글이 이벤트 루프에 남아 루프가 안 비고, 400ms 뒤 위 마지막수단의 process.exit 이
|
|
1305
|
+
* **남은 글을 버린다** — `deel sbom | 느린것` · `deel run --json | ssh …` 이 반쪽 JSON 을 받는다.
|
|
1306
|
+
* 같은 꼴의 고장이 맥 검사에서 셋(convert · hooks · mutate) 나왔다.
|
|
1307
|
+
*
|
|
1308
|
+
* 그래서 표준출력·표준오류가 **다 나간 뒤에** 마지막수단을 건다. 끝없이 기다리지는 않는다 —
|
|
1309
|
+
* 받는 쪽이 아예 안 읽으면 정한 시간 뒤 예전대로 간다. 빈 글 쓰기의 콜백은 앞의 글이 다
|
|
1310
|
+
* 나간 뒤에 불린다(쓰기는 차례대로 나간다). 닫힌 파이프(EPIPE)면 콜백이 오류와 함께 곧 온다.
|
|
1311
|
+
*/
|
|
1312
|
+
function 다나가기(최대) {
|
|
1313
|
+
const 하나 = (흐름) => new Promise((된) => {
|
|
1314
|
+
try { 흐름.write('', () => 된()); } catch { 된(); }
|
|
1315
|
+
});
|
|
1316
|
+
return Promise.race([
|
|
1317
|
+
Promise.all([하나(process.stdout), 하나(process.stderr)]),
|
|
1318
|
+
new Promise((된) => setTimeout(된, 최대).unref()),
|
|
1319
|
+
]);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1299
1322
|
main()
|
|
1300
1323
|
.then((code) => 끝내기(code ?? 0))
|
|
1301
1324
|
.catch(async (err) => {
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// 연결 프로필 저장/읽기. ~/.deel/config.json 위에 프로젝트의 .deel/config.json 을 겹친다
|
|
2
2
|
// (믿는 폴더일 때만 — safety/trust.js).
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join, resolve } from 'node:path';
|
|
5
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join, resolve, dirname, basename } from 'node:path';
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, rmSync, realpathSync } from 'node:fs';
|
|
6
6
|
import { 프록시정하기 } from './backend/proxy.js';
|
|
7
7
|
import { 셸정하기 } from './tools/shell.js';
|
|
8
8
|
import { 애저정하기 } from './backend/azure.js';
|
|
@@ -87,11 +87,24 @@ export function configPath(root = process.cwd()) {
|
|
|
87
87
|
* · 믿고 나면 제 설정의 permissions.allow 를 「저장소가 적은 것이라 걷어냈다」 고
|
|
88
88
|
* 거짓말했다. 실제로는 이 PC 파일로 읽혀 그대로 걸려 있었다.
|
|
89
89
|
*
|
|
90
|
-
* reset.js 의 같은자리와 같은 기준이다 — 윈도우는 대소문자를 안 가린다.
|
|
90
|
+
* reset.js 의 같은자리와 같은 기준이다 — 윈도우는 대소문자를 안 가린다. 그리고 글자 말고
|
|
91
|
+
* **폴더를 링크 너머 실제 자리로 푼 꼴**로도 견준다. 맥은 `/var` · `/tmp` 가 `/private/…` 을
|
|
92
|
+
* 가리키는 링크라, 집 폴더에서 켜도 DEEL_HOME 과 작업 폴더가 다른 글자로 와서 위 세 가지가
|
|
93
|
+
* 맥에서만 그대로 났다(40회차 맥 실측).
|
|
91
94
|
*/
|
|
92
95
|
export function 집설정파일인가(파일) {
|
|
93
|
-
const
|
|
94
|
-
|
|
96
|
+
const 고름 = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
|
|
97
|
+
const 꼴들 = (p) => {
|
|
98
|
+
const r = resolve(String(p ?? ''));
|
|
99
|
+
let 진짜 = r;
|
|
100
|
+
try { 진짜 = join(realpathSync.native(dirname(r)), basename(r)); } catch { /* 폴더가 없으면 글자로만 */ }
|
|
101
|
+
// 파일 자체가 집 설정을 가리키는 링크여도 같은 파일이다 (40회차 2차 눈).
|
|
102
|
+
let 끝까지 = 진짜;
|
|
103
|
+
try { 끝까지 = realpathSync.native(r); } catch { /* 파일이 없으면 폴더까지만 */ }
|
|
104
|
+
return [r, 진짜, 끝까지].map(고름);
|
|
105
|
+
};
|
|
106
|
+
const 집것 = 꼴들(join(userDir(), 'config.json'));
|
|
107
|
+
return 꼴들(파일).some((x) => 집것.includes(x));
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
/**
|
|
@@ -803,10 +816,18 @@ export function resolveKey(profile) {
|
|
|
803
816
|
const 전체열쇠 = profile?.출처 === '저장소' ? '' : process.env.DEEL_API_KEY;
|
|
804
817
|
const 값 = byName || 전체열쇠 || profile?.apiKey || '';
|
|
805
818
|
if (!잠긴것인가(값)) return 값;
|
|
819
|
+
return 잠긴열쇠쓰기(값);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* 잠긴 값을 풀어 쓴다. `풀개` 는 검사가 바꿔 끼우는 자리다 — 키체인이 없는 PC 에서도
|
|
824
|
+
* 「같은 이름표에서 다른 열쇠가 나오면 새 것을 쓴다」 를 잴 수 있게.
|
|
825
|
+
*/
|
|
826
|
+
export function 잠긴열쇠쓰기(값, { 풀개 = 풀기 } = {}) {
|
|
806
827
|
// 푸는 데 파워셸을 한 번 부른다(0.2초쯤). 한마디마다 부르면 그게 다 사람이
|
|
807
828
|
// 기다리는 시간이라, 판에 한 번만 풀고 들고 있는다.
|
|
808
829
|
if (푼것.has(값)) return 푼것.get(값);
|
|
809
|
-
const r =
|
|
830
|
+
const r = 풀개(값);
|
|
810
831
|
if (!r.ok) {
|
|
811
832
|
/*
|
|
812
833
|
* **실패는 안 담아 둔다.**
|
|
@@ -818,10 +839,24 @@ export function resolveKey(profile) {
|
|
|
818
839
|
열쇠탈 = r.why || '잠근 열쇠를 못 풀었습니다';
|
|
819
840
|
return '';
|
|
820
841
|
}
|
|
821
|
-
푼것.set(값, r.text);
|
|
842
|
+
if (푼것담나(값)) 푼것.set(값, r.text);
|
|
822
843
|
return r.text;
|
|
823
844
|
}
|
|
824
845
|
|
|
846
|
+
/*
|
|
847
|
+
* ── 키체인 이름표는 담아 두면 안 된다 (맥 실측 · 39회차) ──────────────────
|
|
848
|
+
*
|
|
849
|
+
* 담아 두기는 「같은 잠긴 값이면 같은 열쇠」 일 때만 맞다. DPAPI 값은 잠근 덩이
|
|
850
|
+
* **그 자체**라 열쇠가 바뀌면 값도 바뀌고, 새 값은 저절로 새로 푼다. 그런데 키체인
|
|
851
|
+
* 값 `keychain:<이름>` 은 자리의 **이름표**다. `deel setup` 으로 같은 프로필에 새
|
|
852
|
+
* 열쇠를 넣으면 `-U` 가 같은 자리를 덮고 이름표는 글자 하나 안 바뀐다. 그래서 떠 있는
|
|
853
|
+
* REPL·ACP 가 /model 이나 새 대화로 연결을 다시 지어도 **옛 열쇠**를 꺼내 보냈다 —
|
|
854
|
+
* 401 이고, 껐다 켜기 전에는 안 풀렸다.
|
|
855
|
+
*
|
|
856
|
+
* 키체인은 그때그때 읽는다. resolveKey 는 연결을 지을 때만 불린다(한마디마다가 아니다).
|
|
857
|
+
*/
|
|
858
|
+
export function 푼것담나(값) { return String(값 ?? '').startsWith('dpapi:'); }
|
|
859
|
+
|
|
825
860
|
export function upsert(cfg, profile) {
|
|
826
861
|
const i = cfg.profiles.findIndex((x) => x.id === profile.id);
|
|
827
862
|
if (i >= 0) cfg.profiles[i] = { ...cfg.profiles[i], ...profile };
|
package/src/reset.js
CHANGED
|
@@ -64,10 +64,20 @@ function 안에것(폴더, 거르기 = () => true) {
|
|
|
64
64
|
/** 여러 자리를 센 것을 하나로 모은다. 한 자리라도 못 셌으면 못 센 것이다. */
|
|
65
65
|
const 셈모으기 = (값들) => (값들.some((n) => n === null) ? null : 값들.reduce((a, n) => a + n, 0));
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* 두 경로가 같은 파일인가. 윈도우는 대소문자를 안 가린다.
|
|
69
|
+
*
|
|
70
|
+
* 글자로만 견주면 맥에서 갈렸다 — `/var` · `/tmp` 는 `/private/…` 을 가리키는 링크라 집
|
|
71
|
+
* 폴더에서 켜도 DEEL_HOME 과 작업 폴더가 서로 다른 글자로 온다(40회차 맥 실측). 그러면 한
|
|
72
|
+
* 파일을 둘로 세고, 연결 칸만 걷어야 할 집 설정을 「저장소 설정」 자리로도 지우러 간다. 윗자리를
|
|
73
|
+
* 진짜 경로로 푼 꼴(윗자리풀기)로도 견준다. 그리고 파일 자체가 링크면(저장소 설정이 집 설정을
|
|
74
|
+
* 가리키는 링크) 끝까지 푼 꼴(실경로)로도 — 같은 파일이면 한 파일로 다뤄야 집 설정의 사람 칸을
|
|
75
|
+
* 지키는 길(연결 칸만 걷기)로 간다(40회차 2차 눈). 지울 때는 여전히 자리 그 자체를 지운다.
|
|
76
|
+
*/
|
|
68
77
|
const 같은자리 = (a, b) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
78
|
+
const 꼴 = (p) => [resolve(p), 윗자리풀기(p), 실경로(p)].map((x) => (process.platform === 'win32' ? x.toLowerCase() : x));
|
|
79
|
+
const 저쪽 = 꼴(b);
|
|
80
|
+
return 꼴(a).some((x) => 저쪽.includes(x));
|
|
71
81
|
};
|
|
72
82
|
|
|
73
83
|
/**
|
package/src/safety/guard.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 승인 프롬프트를 안 쓰는 대신 (1) 작업 범위 밖은 못 건드리고
|
|
3
3
|
// (2) 되돌릴 수 없는 명령만 막는다. 나머지는 전부 통과시킨다.
|
|
4
4
|
import { resolve, relative, isAbsolute, sep, dirname, basename, win32, posix } from 'node:path';
|
|
5
|
-
import { realpathSync,
|
|
5
|
+
import { realpathSync, readFileSync, lstatSync } from 'node:fs';
|
|
6
6
|
import { homedir, userInfo } from 'node:os';
|
|
7
7
|
import { 내부살림 } from '../tools/fsutil.js';
|
|
8
8
|
|
|
@@ -351,7 +351,13 @@ const BLOCKED = [
|
|
|
351
351
|
* 우리는 「빗금으로 **끝나는** 것」 만 뿌리로 봤다. 위 rm 규칙은 이미
|
|
352
352
|
* 뿌리목표 로 별을 받고 있었다. 한쪽만 받으면 그건 규칙이 아니라 우연이다.
|
|
353
353
|
*/
|
|
354
|
-
|
|
354
|
+
/*
|
|
355
|
+
* 파워셸의 **다른 이름**도 같은 명령이다 (2.1.5 · 2.0.0 울타리 역공격에서 남김).
|
|
356
|
+
* `ri` · `rd` · `rmdir` · `del` · `erase` 는 파워셸에서 전부 Remove-Item 이다. 위 rd · del 규칙은
|
|
357
|
+
* cmd 의 `/s` · `/q` 만 보고, 여기는 긴 이름만 봐서 `ri -Recurse -Force C:\` 가 이 목록을 지나갔다
|
|
358
|
+
* (checkPaths 가 범위 밖으로 막아 줘서 여태 새지는 않았다 — 이 목록은 그와 따로 서는 겹이다).
|
|
359
|
+
*/
|
|
360
|
+
re: new RegExp(String.raw`(?<![\w-])(?:Remove-Item|ri|rd|rmdir|del|erase)\b(?=[^|;&]*\s-r[a-z]*\b)[^|;&]*\s${윈뿌리목표}(?=\s|$)`, 'i'),
|
|
355
361
|
why: '드라이브나 집 폴더를 통째로 지웁니다',
|
|
356
362
|
},
|
|
357
363
|
/*
|
|
@@ -560,7 +566,11 @@ export function checkPaths(cmd, scope) {
|
|
|
560
566
|
if (봐주는자리(t)) continue;
|
|
561
567
|
// cd 가 가리킨 자리는 위에서 **옮겨 간 자리 기준**으로 이미 봤다. 여기서
|
|
562
568
|
// 또 보면 뿌리 기준으로 풀려서 `cd src/a && cd ../..` 가 밖으로 읽힌다.
|
|
563
|
-
|
|
569
|
+
//
|
|
570
|
+
// 본 **횟수만큼만** 건너뛴다 (2.1.5). 글자로 건너뛰었더니, 빗금 없는 `..` 를 자리로 치자마자
|
|
571
|
+
// `cd src; cd ..; find .. -delete` 의 뒤 `..` 가 앞 cd 의 `..` 덕에 검사를 비켰다.
|
|
572
|
+
const 남은번 = 옮긴자리.get(t) ?? 0;
|
|
573
|
+
if (남은번 > 0) { 옮긴자리.set(t, 남은번 - 1); continue; }
|
|
564
574
|
// 어디인지 모르는 이전 폴더는 밖으로 본다 (경로낱말 의 `~-` 머리말).
|
|
565
575
|
if (/^~-(?=[/\\]|$)/.test(t)) {
|
|
566
576
|
throw new ScopeError(`\`~-\` 는 셸의 이전 폴더(OLDPWD)라 어디인지 알 수 없어 작업 범위 밖으로 봅니다: ${t}`
|
|
@@ -620,6 +630,25 @@ function 자리표풀기(s) {
|
|
|
620
630
|
* 따로 본다 — 어디인지 모르는 자리라 풀 값이 없다.)
|
|
621
631
|
*/
|
|
622
632
|
.replace(/^~\+(?=[/\\]|$)/, '.')
|
|
633
|
+
/*
|
|
634
|
+
* `$PWD` · `%CD%` 도 **지금 폴더**다 — `~+` 와 같은 대접 (2.1.5).
|
|
635
|
+
*
|
|
636
|
+
* 자리표가 든 낱말은 아래 경로낱말() 이 「못 푼 것」 으로 보고 자리 검사를 건너뛴다. 이 둘은
|
|
637
|
+
* 값을 아는데도(셸은 작업 폴더에서 뜬다) 모르는 것으로 쳐서 그대로 새었다 —
|
|
638
|
+
*
|
|
639
|
+
* rm -rf ../옆프로젝트 막힘
|
|
640
|
+
* rm -rf $PWD/../옆프로젝트 통과 ← 같은 폴더
|
|
641
|
+
* rd /s /q %CD%\.. 통과 ← 작업 폴더의 부모 통째로
|
|
642
|
+
*
|
|
643
|
+
* 파워셸은 이름의 크기를 안 가려서(`$pwd`) 크기를 안 본다. 따옴표에 싼 `$(pwd)` 도 같은 값이다
|
|
644
|
+
* (안 싼 것은 괄호에서 잘려 `/../x` 가 절대경로로 걸린다). 파워셸의 `$pwd.Path` · `$pwd.ProviderPath`,
|
|
645
|
+
* 링크를 푼 `$(pwd -P)`, 옛 꼴 `` `pwd` `` 도 같은 자리다(Gemini 리뷰 — 넷 다 새고 있었다).
|
|
646
|
+
*/
|
|
647
|
+
.replace(/^(?:\$\{PWD\}|\$PWD(?:\.(?:Provider)?Path)?|%CD%|\$\(pwd(?:\s+-[LP])?\)|`pwd(?:\s+-[LP])?`)(?=[/\\]|$)/i, '.')
|
|
648
|
+
/*
|
|
649
|
+
* `$OLDPWD` 는 `~-` 와 같은 자리(이전 폴더)다 — 어디인지 모르니 `~-` 로 바꿔 같은 길로 막는다.
|
|
650
|
+
*/
|
|
651
|
+
.replace(/^(?:\$\{OLDPWD\}|\$OLDPWD)(?=[/\\]|$)/i, '~-')
|
|
623
652
|
/*
|
|
624
653
|
* `~이름` 은 **그 사람의 집**이다 (사냥5 R5-M1).
|
|
625
654
|
*
|
|
@@ -698,6 +727,11 @@ function 자리표풀기(s) {
|
|
|
698
727
|
* 글로브(`*` `?`)는 일부러 안 넘긴다. `rm /Users/남/*` 은 진짜 경로이고,
|
|
699
728
|
* 그건 계속 걸려야 한다.
|
|
700
729
|
*/
|
|
730
|
+
/** 그 이름에 무엇이 놓였나. 가리키는 곳이 없는 링크도 놓인 것이다. 못 물어보면 놓였다고 친다. */
|
|
731
|
+
export function 놓였나(경로) {
|
|
732
|
+
try { lstatSync(경로); return true; } catch (e) { return !(e?.code === 'ENOENT' || e?.code === 'ENOTDIR'); }
|
|
733
|
+
}
|
|
734
|
+
|
|
701
735
|
export function 코드조각인가(낱말) {
|
|
702
736
|
// 따옴표가 낱말 **안**에 섞였다 = 코드를 가운데서 자른 것이다.
|
|
703
737
|
// 파일 이름에 따옴표를 쓰는 사람은 없다시피 하고, 있어도 셸에서 못 쓴다.
|
|
@@ -746,8 +780,14 @@ export function 코드조각인가(낱말) {
|
|
|
746
780
|
* 한 번 막히고, 사람이 보고 다시 친다. 경로를 코드 조각으로 잘못 보면
|
|
747
781
|
* 울타리가 없는 것과 같고, 그건 아무 표시도 안 난다. 모를 때는 좁은
|
|
748
782
|
* 쪽으로 간다.
|
|
783
|
+
*
|
|
784
|
+
* 「있다」 는 **그 이름에 무엇이 놓였나**로 잰다(lstat). existsSync 는 링크를
|
|
785
|
+
* 따라가서, 가리키는 곳이 없는 링크를 「없다」 고 했다. 맥 뿌리의
|
|
786
|
+
* `/.VolumeIcon.icns` 가 그 꼴이라 readdir 에는 나오는데 코드 조각으로 넘어갔다
|
|
787
|
+
* (40회차 맥 실측). 없다는 답(ENOENT·ENOTDIR)만 「없다」 고, 못 물어본 것은 위
|
|
788
|
+
* 까닭대로 경로로 친다.
|
|
749
789
|
*/
|
|
750
|
-
|
|
790
|
+
return !놓였나(낱말);
|
|
751
791
|
}
|
|
752
792
|
return false;
|
|
753
793
|
}
|
|
@@ -938,6 +978,29 @@ export function 봐주는자리(경로) {
|
|
|
938
978
|
// (`git commit --message=../notes` · `git log --format=%h/%s`). curl 의 `--data=@파일` 은 파일을 읽으니 안 뺀다.
|
|
939
979
|
const 글받는옵션 = /^(?:message|format|pretty|title|body|subject|description|grep|author|committer|label|comment|notes?)$/i;
|
|
940
980
|
|
|
981
|
+
/*
|
|
982
|
+
* ── 빗금 없는 `..` 도 자리다 (2.1.5) ────────────────────────────────────
|
|
983
|
+
*
|
|
984
|
+
* 자리로 치는 것은 빗금이 든 낱말과 드라이브 한 칸(`C:`)뿐이었다. 그래서 **부모 폴더 그 자체**가
|
|
985
|
+
* 후보에도 못 들었다. 기본(auto) 모드에서는 셸 명령을 안 묻고 이 울타리가 마지막 문인데 —
|
|
986
|
+
*
|
|
987
|
+
* rm -rf ../* 막힘
|
|
988
|
+
* find .. -delete 통과 ← 작업 폴더의 부모가 통째로 비었다 (재 봤다)
|
|
989
|
+
* rd /s /q .. 통과 ← cmd
|
|
990
|
+
* Remove-Item -Recurse .. 통과 ← 파워셸
|
|
991
|
+
* git -C .. clean -fdx 통과 ←
|
|
992
|
+
* tar czf o.tgz .. 통과 ← 작업 폴더가 집 바로 밑이면 ~/.ssh 까지 묶여 읽힌다
|
|
993
|
+
*
|
|
994
|
+
* GNU rm 은 `..` 를 스스로 거절해서(`rm -rf ..`) 여태 안 보였을 뿐이다. find · cmd · 파워셸 · git 은
|
|
995
|
+
* 안 거절한다. 드라이브의 **지금 폴더 기준** 꼴(`C:..` · `C:.`)도 같다 — 윈도에서 `C:..` 는 그
|
|
996
|
+
* 드라이브의 지금 폴더의 부모다.
|
|
997
|
+
*
|
|
998
|
+
* `..` 가 **가운데** 든 것(`HEAD..main` · `{1..5}`)은 부모가 아니다. 낱말 **전체**가 `..` 일 때만.
|
|
999
|
+
*/
|
|
1000
|
+
function 자리꼴인가(s) {
|
|
1001
|
+
return s.includes('/') || s.includes('\\') || s === '..' || /^[a-zA-Z]:(?:\.{1,2})?$/.test(s);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
941
1004
|
/** 명령줄에서 경로처럼 보이는 낱말만 골라낸다. */
|
|
942
1005
|
export function 경로낱말(cmd) {
|
|
943
1006
|
const out = [];
|
|
@@ -1034,7 +1097,7 @@ export function 경로낱말(cmd) {
|
|
|
1034
1097
|
*/
|
|
1035
1098
|
if (/^~-(?=[/\\]|$)/.test(풀린)) { out.push(풀린); continue; }
|
|
1036
1099
|
const 살림이름 = 풀린 === '.deel' || 풀린.toLowerCase() === '.deel';
|
|
1037
|
-
const 경로같나 =
|
|
1100
|
+
const 경로같나 = 자리꼴인가(풀린) || 살림이름;
|
|
1038
1101
|
if (!경로같나) continue;
|
|
1039
1102
|
/*
|
|
1040
1103
|
* 남은 자리표가 있으면(우리가 못 푼 것) **어디로 갈지 모른다.** 울타리
|
|
@@ -1083,7 +1146,7 @@ export function 경로낱말(cmd) {
|
|
|
1083
1146
|
if (!/["'`]/.test(날것)) continue; // 따옴표가 없으면 위에서 이미 봤다
|
|
1084
1147
|
const 뗀것 = 자리표풀기(날것.replace(/["'`]/g, ''));
|
|
1085
1148
|
if (!뗀것 || 뗀것.startsWith('-')) continue;
|
|
1086
|
-
if (
|
|
1149
|
+
if (!자리꼴인가(뗀것)) continue;
|
|
1087
1150
|
if (/[$%]/.test(뗀것)) { if (내부살림(뗀것)) out.push(뗀것); continue; }
|
|
1088
1151
|
if (코드조각인가(뗀것)) continue;
|
|
1089
1152
|
if (!out.includes(뗀것)) out.push(뗀것);
|
|
@@ -1159,7 +1222,8 @@ function 낱말들(마디) {
|
|
|
1159
1222
|
function 갈자리(뒤) {
|
|
1160
1223
|
for (const w of 뒤) {
|
|
1161
1224
|
if (!w.글) continue;
|
|
1162
|
-
|
|
1225
|
+
// `-` 하나는 옵션이 아니라 이전 폴더다 (2.1.5). 옵션으로 건너뛰어 `cd - && rm -rf *` 가 안 보였다.
|
|
1226
|
+
if (w.글.startsWith('-') && w.글 !== '-') continue;
|
|
1163
1227
|
if (/^\/d$/i.test(w.글)) continue;
|
|
1164
1228
|
return w.글;
|
|
1165
1229
|
}
|
|
@@ -1169,15 +1233,23 @@ function 갈자리(뒤) {
|
|
|
1169
1233
|
/**
|
|
1170
1234
|
* 폴더를 옮기는 명령을 따라가며, 옮겨 간 자리가 울타리 안인지 본다.
|
|
1171
1235
|
*
|
|
1172
|
-
* @returns {
|
|
1173
|
-
* 보지 않는다 — 뿌리 기준으로 다시 풀면 `cd src/a && cd ../..` 가 밖이 된다.
|
|
1236
|
+
* @returns {Map<string, number>} 여기서 이미 본 cd 목적지와 그 횟수. 부르는 쪽은 그만큼은
|
|
1237
|
+
* 두 번 보지 않는다 — 뿌리 기준으로 다시 풀면 `cd src/a && cd ../..` 가 밖이 된다.
|
|
1174
1238
|
*/
|
|
1175
|
-
function 폴더옮김검사(cmd, scope, 시작 = null, 깊이 = 0, 본것 = new
|
|
1239
|
+
function 폴더옮김검사(cmd, scope, 시작 = null, 깊이 = 0, 본것 = new Map()) {
|
|
1176
1240
|
let 여기 = 시작 ?? scope.root;
|
|
1177
1241
|
const 쌓은것 = [];
|
|
1242
|
+
// 이 명령 안에서 cd 로 떠나온 자리. `cd -` 가 가는 곳이다 — 없으면 셸이 물려받은 OLDPWD 라 모른다.
|
|
1243
|
+
let 이전 = null;
|
|
1244
|
+
// 어디인지 모르는 자리로 옮겼으면 그 적은 글자(`-` · `~-` · `$OLDPWD`). 뒤에 명령이 오면 막는다.
|
|
1245
|
+
let 모르는자리 = null;
|
|
1178
1246
|
for (const 마디 of 마디들(cmd)) {
|
|
1179
1247
|
const 낱말 = 낱말들(마디);
|
|
1180
1248
|
if (!낱말.length) continue;
|
|
1249
|
+
if (모르는자리 != null) {
|
|
1250
|
+
throw new ScopeError(`\`cd ${모르는자리}\` 는 셸의 이전 폴더(OLDPWD)로 가는데 어디인지 알 수 없어, 그 뒤 명령을 작업 범위 밖에서 도는 것으로 봅니다`
|
|
1251
|
+
+ `\n 이 명령 안에 있습니다: ${cmd.slice(0, 120)}`);
|
|
1252
|
+
}
|
|
1181
1253
|
/*
|
|
1182
1254
|
* ── cmd 에서 `cd..` 는 `cd ..` 다 (사냥8) ────────────────────────────
|
|
1183
1255
|
*
|
|
@@ -1214,25 +1286,53 @@ function 폴더옮김검사(cmd, scope, 시작 = null, 깊이 = 0, 본것 = new
|
|
|
1214
1286
|
for (const w of 낱말.slice(1)) if (w.쌌나) 폴더옮김검사(w.글, scope, 여기, 깊이 + 1, 본것);
|
|
1215
1287
|
continue;
|
|
1216
1288
|
}
|
|
1217
|
-
if (되돌리기.test(첫)) {
|
|
1289
|
+
if (되돌리기.test(첫)) {
|
|
1290
|
+
const 꺼낸 = 쌓은것.pop();
|
|
1291
|
+
if (꺼낸 != null) { 이전 = 여기; 여기 = 꺼낸; }
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1218
1294
|
if (!폴더옮김.test(첫)) continue;
|
|
1219
1295
|
|
|
1220
1296
|
const 목적 = 붙은목적 ?? 갈자리(낱말.slice(1));
|
|
1221
|
-
// 인자 없는 cd
|
|
1222
|
-
//
|
|
1223
|
-
if (목적 == null
|
|
1297
|
+
// 인자 없는 cd 는 셸마다 뜻이 다르다(bash 는 집 · cmd 는 지금 자리를 찍기만). 모르는 것을
|
|
1298
|
+
// 막으면 멀쩡한 명령이 죽는다.
|
|
1299
|
+
if (목적 == null) continue;
|
|
1224
1300
|
const 풀린 = 자리표풀기(목적);
|
|
1301
|
+
/*
|
|
1302
|
+
* ── `cd -` · `cd ~-` · `cd $OLDPWD` 는 이전 폴더로 간다 (2.1.5) ─────────────
|
|
1303
|
+
*
|
|
1304
|
+
* 이 명령 안에서 먼저 옮겼으면 그 떠나온 자리다 — `cd src && make && cd -` 는 뿌리로 돌아온다.
|
|
1305
|
+
* 먼저 옮긴 적이 없으면 셸이 물려받은 OLDPWD 라 어디인지 모른다. 여태 `cd -` 를 건너뛰어
|
|
1306
|
+
* `cd - && rm -rf *` 가 그냥 지나갔다. 그 **뒤에 오는 명령**을 밖에서 도는 것으로 본다 —
|
|
1307
|
+
* `cd -` 하나만 있는 것은 옮기고 끝이라 그대로 둔다(아래 모르는자리).
|
|
1308
|
+
*
|
|
1309
|
+
* 그 **아래**(`cd ~-/sub` · `cd $OLDPWD/sub`)도 같은 자리에서 시작한다 — `^~-$` 로 딱 그 꼴만 봤더니
|
|
1310
|
+
* 아래로 붙인 것은 작업 폴더 밑의 `./~-/sub` 라는 이름으로 읽혀 지나갔다(Gemini 리뷰).
|
|
1311
|
+
*/
|
|
1312
|
+
const 이전아래 = /^~-(?=[/\\]|$)/.test(풀린);
|
|
1313
|
+
if (목적 === '-' || 이전아래) {
|
|
1314
|
+
// 경로낱말 은 `~-`(· `$OLDPWD`)를 밖으로 올린다. 여기서 풀어 준 것은 checkPaths 가 한 번 비킨다.
|
|
1315
|
+
본것.set(풀린, (본것.get(풀린) ?? 0) + 1);
|
|
1316
|
+
if (이전 == null) { 모르는자리 = 목적; continue; }
|
|
1317
|
+
if (목적 === '-' || 풀린 === '~-') { [여기, 이전] = [이전, 여기]; continue; }
|
|
1318
|
+
// 이전 폴더 아래로 — 아래의 범위 검사를 그대로 받는다.
|
|
1319
|
+
}
|
|
1225
1320
|
// 못 푼 자리표가 남아 있으면(`$BUILD_DIR`) 역시 어디로 갈지 모른다.
|
|
1226
1321
|
if (/[$%]/.test(풀린)) continue;
|
|
1227
1322
|
|
|
1228
|
-
const 열린 = MSYS풀기(풀린);
|
|
1229
|
-
const abs = isAbsolute(열린) ? resolve(열린) : resolve(여기, 열린);
|
|
1230
|
-
|
|
1323
|
+
const 열린 = MSYS풀기(이전아래 ? 풀린.replace(/^~-[/\\]*/, '') : 풀린);
|
|
1324
|
+
const abs = isAbsolute(열린) ? resolve(열린) : resolve(이전아래 ? 이전 : 여기, 열린);
|
|
1325
|
+
/*
|
|
1326
|
+
* 비킬 낱말로 적는 것은 **따로 선 목적지**뿐이다. `cd..` 처럼 붙여 쓴 것은 경로낱말 에 `..` 로 안
|
|
1327
|
+
* 나오므로, 적어 두면 남은 한 번이 뒤의 딴 `..` 를 비켜 준다 — `cd src && cd.. && find .. -delete`
|
|
1328
|
+
* (Gemini 리뷰). 이전아래 는 위에서 이미 적었다.
|
|
1329
|
+
*/
|
|
1330
|
+
if (!붙은목적 && !이전아래) 본것.set(풀린, (본것.get(풀린) ?? 0) + 1);
|
|
1231
1331
|
|
|
1232
1332
|
// 프로그램이 있는 자리로 들어가는 것은 자료를 만지는 일이 아니다.
|
|
1233
1333
|
// 푼 값이 아니라 **적힌 글자**로 본다 — 윈도우에서 resolve('/usr/bin') 은
|
|
1234
1334
|
// `C:\usr\bin` 이 되어 목록에 안 걸린다 (checkPaths 도 낱말로 본다).
|
|
1235
|
-
if (봐주는자리(풀린)) { 여기 = abs; continue; }
|
|
1335
|
+
if (봐주는자리(풀린)) { 이전 = 여기; 여기 = abs; continue; }
|
|
1236
1336
|
try { scope.resolve(abs); }
|
|
1237
1337
|
catch (e) {
|
|
1238
1338
|
if (e instanceof ScopeError) {
|
|
@@ -1251,6 +1351,7 @@ function 폴더옮김검사(cmd, scope, 시작 = null, 깊이 = 0, 본것 = new
|
|
|
1251
1351
|
if (이유) throw new BlockedError(`${이유}\n 이 명령 안에 있습니다: ${cmd.slice(0, 120)}`);
|
|
1252
1352
|
|
|
1253
1353
|
if (/^(pushd|push-location)$/i.test(첫)) 쌓은것.push(여기);
|
|
1354
|
+
이전 = 여기;
|
|
1254
1355
|
여기 = abs;
|
|
1255
1356
|
}
|
|
1256
1357
|
return 본것;
|
package/src/safety/keystore.js
CHANGED
|
@@ -159,15 +159,100 @@ $u = [Security.Cryptography.ProtectedData]::Unprotect($b, $null, 'CurrentUser')
|
|
|
159
159
|
[Console]::Out.Write([Convert]::ToBase64String($u))
|
|
160
160
|
`;
|
|
161
161
|
|
|
162
|
+
/*
|
|
163
|
+
* ── `security -i` 한 줄에는 끝이 있다 (531 · 2026-09-29 맥 실측) ─────────
|
|
164
|
+
*
|
|
165
|
+
* 열쇠를 명령줄에 안 올리려고 `security -i` 의 stdin 으로 한 줄을 넣는다. 그런데
|
|
166
|
+
* 그 도구는 한 줄을 정해진 크기의 버퍼로 읽고, 넘치면 **앞 토막을 한 명령으로,
|
|
167
|
+
* 뒤 토막을 또 한 명령으로** 읽는다. 잰 것: 2000자 열쇠는 잠기고 풀렸고, 4000자
|
|
168
|
+
* 열쇠는 `unknown command "xFaT…"` 로 못 잠갔다 — 뒤 토막이 딴 명령이 됐다.
|
|
169
|
+
* 앞 토막은 잘린 열쇠를 달고 그대로 돌았을 수 있다.
|
|
170
|
+
*
|
|
171
|
+
* 그래서 줄을 **넣기 전에** 잰다. 넘치면 넣지 않고 못 잠갔다고 말한다 — 이 파일
|
|
172
|
+
* 머리말대로 잠근 척하지 않는다. 한도는 바이트로 잰다(버퍼가 바이트다 · 한글
|
|
173
|
+
* 프로필 이름은 한 글자가 3바이트).
|
|
174
|
+
*
|
|
175
|
+
* 이름·계정도 같은 줄에 들어간다. 빈칸이 있으면 낱말이 갈리고(usage 오류로 못 잠금),
|
|
176
|
+
* 줄바꿈이 있으면 다음 줄이 딴 명령으로 읽힌다 — DEEL_KEYCHAIN_NAME 에 실제로 넣어
|
|
177
|
+
* 쟀다. 따옴표·역빗금은 이 도구가 어떻게 읽는지 우리가 장담 못 하니 같이 거절한다.
|
|
178
|
+
* 조용히 기본 이름으로 바꾸지 않는다 — DEEL_KEYCHAIN_NAME 은 검사가 사람의 진짜
|
|
179
|
+
* 열쇠를 안 덮으려고 주는 자리라, 바꿔 넣으면 그 자리를 덮는다(키체인이름 머리말).
|
|
180
|
+
*/
|
|
181
|
+
export const 키체인줄한도 = 4000;
|
|
182
|
+
const 키체인시한 = 20000;
|
|
183
|
+
const 줄을가르는글자 = /[\s"'\\\p{Cc}]/u;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `security -i` 에 넣을 한 줄을 짓는다. 넣을 수 없으면 왜인지 준다.
|
|
187
|
+
*
|
|
188
|
+
* 짓기만 하고 안 돌린다 — 맥이 없는 자리에서도 검사가 이 판단을 잴 수 있게.
|
|
189
|
+
*
|
|
190
|
+
* @returns {{ok: true, 줄: string} | {ok: false, 왜: string}}
|
|
191
|
+
*/
|
|
192
|
+
export function 키체인넣을줄(글, 이름, 계정) {
|
|
193
|
+
// 빈 열쇠는 `-w -U` 가 되어 `-U` 를 열쇠로 읽는다(2차 눈). 잠그기() 가 먼저 거르지만 여기서도 안 짓는다.
|
|
194
|
+
if (!String(글 ?? '')) return { ok: false, 왜: '넣을 열쇠가 비어 있습니다' };
|
|
195
|
+
for (const [무엇, 값] of [['키체인 이름', 이름], ['계정 이름', 계정]]) {
|
|
196
|
+
const s = String(값 ?? '');
|
|
197
|
+
if (!s) return { ok: false, 왜: `${무엇}이 비어 있습니다` };
|
|
198
|
+
if (줄을가르는글자.test(s)) {
|
|
199
|
+
const 어디서 = 무엇 === '키체인 이름' && process.env.DEEL_KEYCHAIN_NAME ? ' (DEEL_KEYCHAIN_NAME)' : '';
|
|
200
|
+
return { ok: false, 왜: `${무엇} ${JSON.stringify(s)}${어디서} 에 빈칸·줄바꿈·따옴표를 쓸 수 없습니다 — security -i 가 그 자리에서 줄을 가릅니다` };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 값 자체는 base64 로 넣는다. 키체인 도구가 줄바꿈·따옴표를 만나면 거기서 끊긴다.
|
|
204
|
+
const 값 = Buffer.from(String(글 ?? ''), 'utf8').toString('base64');
|
|
205
|
+
const 줄 = `add-generic-password -a ${계정} -s ${이름} -w ${값} -U\n`;
|
|
206
|
+
const 크기 = Buffer.byteLength(줄, 'utf8');
|
|
207
|
+
if (크기 > 키체인줄한도) {
|
|
208
|
+
return { ok: false, 왜: `열쇠가 너무 길어 키체인에 못 넣습니다 (명령 한 줄 ${크기}바이트 · 한도 ${키체인줄한도}) — security -i 는 넘친 뒤 토막을 딴 명령으로 읽습니다` };
|
|
209
|
+
}
|
|
210
|
+
return { ok: true, 줄 };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/*
|
|
214
|
+
* ── 키체인에 든 것이 우리 꼴이 아니어도 「풀었다」 고 했다 (531 실측) ────
|
|
215
|
+
*
|
|
216
|
+
* `Buffer.from(x, 'base64')` 는 base64 가 아닌 글자를 **던지지 않고 건너뛴다.**
|
|
217
|
+
* 그래서 여기 있던 catch 는 한 번도 안 걸렸고, 사람이 키체인 앱에서 값을 손으로
|
|
218
|
+
* 바꿨거나 같은 이름에 다른 프로그램이 넣은 값이면 ok:true 와 함께 깨진 글자가
|
|
219
|
+
* 나갔다. 그 글자가 게이트웨이로 가고 사람은 401 만 본다 — 풀기() 머리말이 막으려던
|
|
220
|
+
* 바로 그 401 이다.
|
|
221
|
+
*
|
|
222
|
+
* 우리가 넣는 꼴은 하나다: UTF-8 글을 표준 base64 로. 다시 지었을 때 글자 하나
|
|
223
|
+
* 안 다르고, 풀린 바이트가 온전한 UTF-8 일 때만 우리 것으로 친다.
|
|
224
|
+
*/
|
|
225
|
+
export function 키체인값풀기(찍힌것) {
|
|
226
|
+
const s = String(찍힌것 ?? '').trim();
|
|
227
|
+
const 아님 = { ok: false, text: '', 왜: '키체인의 값이 deel 이 넣은 꼴(base64)이 아닙니다 — 키체인 앱에서 손으로 바꿨다면 deel setup 으로 다시 넣으세요' };
|
|
228
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(s)) return 아님;
|
|
229
|
+
const 바이트 = Buffer.from(s, 'base64');
|
|
230
|
+
if (바이트.toString('base64') !== s) return 아님;
|
|
231
|
+
try { return { ok: true, text: new TextDecoder('utf-8', { fatal: true }).decode(바이트), 왜: '' }; }
|
|
232
|
+
catch { return 아님; }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/*
|
|
236
|
+
* ── 잠긴 키체인은 20초 서 있다가 `spawnSync security ETIMEDOUT` 한 줄 (531 실측) ──
|
|
237
|
+
*
|
|
238
|
+
* SSH 로 들어온 맥처럼 창을 띄울 데가 없는 자리에서 로그인 키체인이 잠겨 있으면,
|
|
239
|
+
* security 는 풀어 달라는 창을 못 띄우고 그대로 기다린다. 우리 시한에 잘리고 사람에게는
|
|
240
|
+
* node 의 오류 이름만 갔다. 무엇이 왜 서 있었는지와 무엇을 하면 되는지를 준다.
|
|
241
|
+
*/
|
|
242
|
+
export function 키체인오류말(err) {
|
|
243
|
+
if (err?.code === 'ETIMEDOUT') {
|
|
244
|
+
return `키체인이 ${키체인시한 / 1000}초 안에 답하지 않았습니다 — 잠겨 있을 수 있습니다(SSH 로 들어온 맥에서 흔합니다). security unlock-keychain 으로 연 뒤 다시 하세요`;
|
|
245
|
+
}
|
|
246
|
+
return err?.message ?? String(err);
|
|
247
|
+
}
|
|
248
|
+
|
|
162
249
|
/** 맥 키체인. 넣을 때도 명령줄에 안 올린다 — `security -i` 는 명령을 stdin 으로 받는다. */
|
|
163
250
|
function 키체인넣기(글, 이름) {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
const 값 = Buffer.from(글, 'utf8').toString('base64');
|
|
167
|
-
const 명령 = `add-generic-password -a ${계정} -s ${이름} -w ${값} -U\n`;
|
|
251
|
+
const 지은것 = 키체인넣을줄(글, 이름, userInfo().username);
|
|
252
|
+
if (!지은것.ok) return { ok: false, err: 지은것.왜 };
|
|
168
253
|
마지막인자 = ['security', '-i'];
|
|
169
|
-
const r = spawnSync('security', ['-i'], { input:
|
|
170
|
-
if (r.error) return { ok: false, err: r.error
|
|
254
|
+
const r = spawnSync('security', ['-i'], { input: 지은것.줄, encoding: 'utf8', timeout: 키체인시한 });
|
|
255
|
+
if (r.error) return { ok: false, err: 키체인오류말(r.error) };
|
|
171
256
|
return { ok: r.status === 0, err: (r.stderr ?? '').trim() };
|
|
172
257
|
}
|
|
173
258
|
|
|
@@ -175,15 +260,12 @@ function 키체인읽기(이름) {
|
|
|
175
260
|
const 계정 = userInfo().username;
|
|
176
261
|
마지막인자 = ['security', 'find-generic-password', '-a', 계정, '-s', 이름, '-w'];
|
|
177
262
|
const r = spawnSync('security', ['find-generic-password', '-a', 계정, '-s', 이름, '-w'], {
|
|
178
|
-
encoding: 'utf8', timeout:
|
|
263
|
+
encoding: 'utf8', timeout: 키체인시한,
|
|
179
264
|
});
|
|
180
|
-
if (r.error) return { ok: false, text: '', err: r.error
|
|
265
|
+
if (r.error) return { ok: false, text: '', err: 키체인오류말(r.error) };
|
|
181
266
|
if (r.status !== 0) return { ok: false, text: '', err: (r.stderr ?? '').trim() || '키체인에 없습니다' };
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
} catch (err) {
|
|
185
|
-
return { ok: false, text: '', err: err.message };
|
|
186
|
-
}
|
|
267
|
+
const 풀림 = 키체인값풀기(r.stdout);
|
|
268
|
+
return 풀림.ok ? { ok: true, text: 풀림.text } : { ok: false, text: '', err: 풀림.왜 };
|
|
187
269
|
}
|
|
188
270
|
|
|
189
271
|
/**
|
|
@@ -475,8 +557,8 @@ export function 잠금지우기(값 = null) {
|
|
|
475
557
|
// 그대로 남는다 — 이 함수 머리말이 막으려던 그 상태다.
|
|
476
558
|
const 인자 = ['delete-generic-password', '-a', 계정, '-s', (m ? m[2] : '') || 키체인이름()];
|
|
477
559
|
마지막인자 = ['security', ...인자];
|
|
478
|
-
const r = spawnSync('security', 인자, { encoding: 'utf8', timeout:
|
|
479
|
-
if (r.error) return { 지움: false, 방식: 'keychain', 왜: r.error
|
|
560
|
+
const r = spawnSync('security', 인자, { encoding: 'utf8', timeout: 키체인시한 });
|
|
561
|
+
if (r.error) return { 지움: false, 방식: 'keychain', 왜: 키체인오류말(r.error) };
|
|
480
562
|
// 없는 것을 지우라고 해도 실패로 온다. 그건 탈이 아니라 이미 없는 것이다.
|
|
481
563
|
if (r.status !== 0) {
|
|
482
564
|
const 말 = (r.stderr ?? '').trim();
|
package/src/safety/trust.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
// `~/.deel/trusted.json` — 사용자 자리다. 프로젝트 안에 두면 저장소가 제
|
|
45
45
|
// 신뢰를 제가 적는 셈이라 아무 뜻이 없다.
|
|
46
46
|
import { homedir } from 'node:os';
|
|
47
|
-
import { join, resolve, dirname, isAbsolute, parse as 경로쪼개기 } from 'node:path';
|
|
47
|
+
import { join, resolve, dirname, basename, isAbsolute, parse as 경로쪼개기 } from 'node:path';
|
|
48
48
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, rmSync, openSync, closeSync, statSync, realpathSync } from 'node:fs';
|
|
49
49
|
import { c, mark } from '../ui/ansi.js';
|
|
50
50
|
import { 말 } from '../i18n/index.js';
|
|
@@ -146,8 +146,21 @@ export function 믿는목록({ env = process.env } = {}) {
|
|
|
146
146
|
*/
|
|
147
147
|
function 실제자리들(p) {
|
|
148
148
|
const 풀린 = resolve(p);
|
|
149
|
+
/*
|
|
150
|
+
* 없는 자리는 **있는 데까지 올라가** 푼 뒤 나머지 이름을 붙인다. 통째로 글자만 쓰면, 맥에서
|
|
151
|
+
* `/private/tmp/repo` 로 믿어 둔 폴더를 지운 뒤 `/tmp/repo` 로 빼려 할 때 두 글자가 안 맞아
|
|
152
|
+
* 못 뺐다(40회차 2차 눈). reset.js 의 윗자리풀기와 같은 풀기다.
|
|
153
|
+
*/
|
|
149
154
|
let 진짜 = 풀린;
|
|
150
|
-
|
|
155
|
+
const 꼬리 = [];
|
|
156
|
+
for (let 위 = 풀린; ;) {
|
|
157
|
+
try { 진짜 = join(realpathSync.native(위), ...꼬리); break; } catch {
|
|
158
|
+
const 더위 = dirname(위);
|
|
159
|
+
if (더위 === 위) break;
|
|
160
|
+
꼬리.unshift(basename(위));
|
|
161
|
+
위 = 더위;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
151
164
|
return 진짜 === 풀린 ? [풀린] : [풀린, 진짜];
|
|
152
165
|
}
|
|
153
166
|
|
|
@@ -175,17 +188,39 @@ export function 너무넓은자리(폴더, { env = process.env, platform = proce
|
|
|
175
188
|
*/
|
|
176
189
|
export function 믿나(폴더, { env = process.env, platform = process.platform } = {}) {
|
|
177
190
|
if (env.DEEL_TRUST_ALL === '1') return true; // 검사·컨테이너용. 문서에 안 적는다.
|
|
178
|
-
const
|
|
191
|
+
const 나들 = 견줄꼴들(폴더, platform);
|
|
179
192
|
for (const 것 of 목록읽기(env)) {
|
|
180
193
|
// 목록에 적힌 넓은 자리는 읽을 때도 안 믿는다 (위 너무넓은자리 머리말).
|
|
181
194
|
// 파일은 안 고친다 — 적힌 것을 조용히 지우면 무엇이 왜 빠졌는지가 안 남는다.
|
|
182
195
|
if (너무넓은자리(것, { env, platform })) continue;
|
|
183
|
-
const
|
|
184
|
-
if (나 === 그것 || 나.startsWith(그것 + '/')) return true;
|
|
196
|
+
const 그것들 = 견줄꼴들(것, platform);
|
|
197
|
+
if (나들.some((나) => 그것들.some((그것) => 나 === 그것 || 나.startsWith(그것 + '/')))) return true;
|
|
185
198
|
}
|
|
186
199
|
return false;
|
|
187
200
|
}
|
|
188
201
|
|
|
202
|
+
/*
|
|
203
|
+
* ── 같은 폴더를 두 글자로 부른다 (40회차 맥 실측) ──────────────────────
|
|
204
|
+
*
|
|
205
|
+
* 믿나() 는 적힌 글자로만 견줬다. 맥은 `/var` · `/tmp` 가 `/private/…` 을 가리키는
|
|
206
|
+
* 링크라, `/var/…/proj` 를 믿어 놓고 그 안에서 켜면 `process.cwd()` 가 링크를 따라간
|
|
207
|
+
* `/private/var/…/proj` 를 준다 — 같은 폴더를 두고 「안 믿는 폴더」 라며 프로젝트 설정을
|
|
208
|
+
* 통째로 버렸다. `--root /tmp/proj` 로 켜는 사람이 그대로 겪는다.
|
|
209
|
+
*
|
|
210
|
+
* 그래서 적힌 글자와 실제 자리(realpath)를 **양쪽 다** 견준다. 하위 폴더 규칙(`/` 붙여
|
|
211
|
+
* 견주기)은 그대로다 — 링크를 따라가 같은 파일이 나오는 자리만 같은 자리로 친다.
|
|
212
|
+
* 믿기·안믿기의 「이미 있나」·「뺄 줄」 도 같은 자로 잰다. 다른 자를 쓰면 믿긴 폴더를
|
|
213
|
+
* 못 빼거나, 이미 있는 줄을 또 적는다.
|
|
214
|
+
*/
|
|
215
|
+
function 견줄꼴들(p, platform) {
|
|
216
|
+
return [...new Set(실제자리들(String(p ?? '')).map((x) => 고른경로(x, platform)))];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function 같은자리인가(가, 나, platform) {
|
|
220
|
+
const 나들 = 견줄꼴들(나, platform);
|
|
221
|
+
return 견줄꼴들(가, platform).some((x) => 나들.includes(x));
|
|
222
|
+
}
|
|
223
|
+
|
|
189
224
|
/** 목록 파일의 글을 목록으로. 못 풀면 던진다 — 던진 것을 어떻게 칠지는 부르는 쪽이 정한다. */
|
|
190
225
|
function 목록풀기(글) {
|
|
191
226
|
const raw = JSON.parse(BOM떼기(글));
|
|
@@ -285,13 +320,12 @@ export function 믿기(폴더, { env = process.env, platform = process.platform,
|
|
|
285
320
|
if (너무넓은자리(resolve(String(폴더 ?? '')), { env, platform })) {
|
|
286
321
|
return { ok: false, 넓음: true, 왜: 말('trust.refuseWide') };
|
|
287
322
|
}
|
|
288
|
-
const 나 = 고른경로(폴더, platform);
|
|
289
323
|
try {
|
|
290
324
|
// 받아 놓고 안 넘기면 기본값(5초)을 기다린다. 짧게 기다려 보려고 준 수가 아무 일도 안 하면,
|
|
291
325
|
// 그 수를 준 쪽(검사·배치)은 잠금을 지키는지 재고 있다고 믿으면서 5초를 서 있는다.
|
|
292
326
|
return 잠그고(env, () => {
|
|
293
327
|
const 것들 = 쓸목록읽기(env);
|
|
294
|
-
if (것들.some((x) =>
|
|
328
|
+
if (것들.some((x) => 같은자리인가(x, 폴더, platform))) return { ok: true, 이미: true, 자리: 신뢰자리(env) };
|
|
295
329
|
것들.push(resolve(String(폴더)));
|
|
296
330
|
return { ok: true, 이미: false, 자리: 쓰기(것들, env) };
|
|
297
331
|
}, 기다림);
|
|
@@ -312,7 +346,6 @@ export function 믿기(폴더, { env = process.env, platform = process.platform,
|
|
|
312
346
|
* 두 자리가 다른 자를 쓰면 어느 쪽이든 거짓말이 된다.
|
|
313
347
|
*/
|
|
314
348
|
export function 안믿기(폴더, { env = process.env, platform = process.platform } = {}) {
|
|
315
|
-
const 나 = 고른경로(폴더, platform);
|
|
316
349
|
let 남길것;
|
|
317
350
|
let 뺐나 = false;
|
|
318
351
|
let 자리 = 신뢰자리(env);
|
|
@@ -320,12 +353,14 @@ export function 안믿기(폴더, { env = process.env, platform = process.platfo
|
|
|
320
353
|
try {
|
|
321
354
|
잠그고(env, () => {
|
|
322
355
|
const 것들 = 쓸목록읽기(env);
|
|
323
|
-
남길것 = 것들.filter((x) =>
|
|
356
|
+
남길것 = 것들.filter((x) => !같은자리인가(x, 폴더, platform));
|
|
324
357
|
뺐나 = 남길것.length !== 것들.length;
|
|
325
358
|
if (뺐나) 자리 = 쓰기(남길것, env);
|
|
326
359
|
});
|
|
327
360
|
} catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
|
|
328
|
-
const
|
|
361
|
+
const 나들 = 견줄꼴들(폴더, platform);
|
|
362
|
+
const 위 = 남길것.find((x) => !너무넓은자리(x, { env, platform })
|
|
363
|
+
&& 견줄꼴들(x, platform).some((그것) => 나들.some((나) => 나.startsWith(그것 + '/')))) ?? null;
|
|
329
364
|
return { ok: true, 뺐나, 자리, 위폴더: 위, 아직믿김: 믿나(폴더, { env, platform }) };
|
|
330
365
|
}
|
|
331
366
|
|